Autenticação
A API externa da Owem Pay utiliza um modelo de segurança em três camadas principais de autenticação -- API Key + Secret, assinatura HMAC-SHA512 por requisição (apenas em POST) e whitelist de IP obrigatória -- executadas dentro de um pipeline maior de plugs que também valida Content-Type, aplica rate limiting e implementa idempotência.
Visão Geral do Pipeline
A requisição HTTP passa pelos plugs abaixo, nesta ordem. Qualquer plug pode interromper o pipeline com um erro terminal (halt):
POST /api/external/...
│
├─ 1. Content-Type ──────── Não é application/json nem multipart/form-data? → 415
├─ 2. X-Key-Case (KeyCase) ─ Converte params/response snake_case ↔ camelCase (opcional)
├─ 3. API Key + Secret ───── Credenciais ausentes/inválidas? → 401 | API Key inativa? → 401 | API Key expirada? → 401
├─ 4. IP Whitelist ───────── Whitelist vazia? → 403 "ip whitelist required" | IP fora? → 403 "ip not allowed" | Conta inativa? → 403
├─ 5. HMAC-SHA512 (POST) ─── Header `hmac` ausente? → 401 | Assinatura inválida? → 401 | Body inválido? → 400 | API Key sem secret? → 403
├─ 6. Rate Limiter (ETS) ─── Mais de 90.000 req/min por IP? → 429 Retry-After: 60
├─ 7. Idempotency (POST) ─── Chave > 256 chars? → 400 | Replay em 24h? → retorna body cacheado + X-Idempotent-Replay: true
└─ 8. RequirePermission ───── API Key sem a permissão exigida pela rota? → 403
│
└─ Requisição aceita → Controller/lógica de negócioPipeline por método HTTP
GET /balanceusa apenasContent-Type → KeyCase → API Key + Secret → IP Whitelist → RequirePermission-- não há rate limiter, não há HMAC, não há idempotência (é polling de alta frequência autorizado).- Outros
GET/DELETEusamContent-Type → KeyCase → API Key + Secret → IP Whitelist → Rate Limiter → RequirePermission-- sem HMAC e sem idempotência. POSTusa o pipeline completo acima (todos os 8 plugs).
Camada 1 -- API Key + Secret
Todas as requisições devem incluir o header Authorization. A API aceita dois formatos equivalentes -- o ApiKey scheme nativo ou o HTTP Basic Authentication. Ambos são validados pelo mesmo plug (ApiKeyAuth) e têm o mesmo comportamento. Escolha o que for mais conveniente para o seu cliente HTTP.
Formato recomendado -- ApiKey scheme
Authorization: ApiKey {client_id}:{client_secret}Formato alternativo -- HTTP Basic
Authorization: Basic {base64(client_id:client_secret)}Exemplo em Bash:
CLIENT_ID="cli_a1b2c3d4e5f6"
CLIENT_SECRET="sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01"
# Equivalente a Authorization: ApiKey cli_a1b2...:sk_0123...
BASIC=$(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64)
curl -X GET https://api.owem.com.br/api/external/balance \
-H "Authorization: Basic $BASIC"Quando usar Basic vs ApiKey
Use Basic se o seu cliente HTTP (biblioteca, gateway, proxy) já monta credenciais via base64 automaticamente (quase todos fazem). Use ApiKey se prefere enviar o secret em texto puro dentro do header -- ambos caem no mesmo parser no backend.
Campos da credencial
| Componente | Descrição | Prefixo |
|---|---|---|
client_id | Identificador público da API Key | cli_ |
client_secret | Chave secreta (armazenamos apenas o hash) | sk_ |
O secret nunca é armazenado em texto puro. Quando uma requisição chega, o secret enviado é comparado com o hash armazenado. Se não corresponder, a requisição é rejeitada antes de chegar à lógica de negócio.
API Key pode expirar
Apesar de na prática a maioria das API Keys serem criadas sem data de expiração, o campo expires_at existe no schema. Se uma chave for configurada com expires_at no passado, a autenticação falha com 401 API key has expired. Também é possível revogar (marcar como inativa): retorna 401 API key is inactive. Isso substitui a informação anterior desta documentação que afirmava que API Keys eram permanentes.
Camada 2 -- HMAC-SHA512
Requisições transacionais (POST, PUT, PATCH) exigem assinatura HMAC-SHA512 do body no header hmac. A validação usa comparação em tempo constante (constant-time comparison) para impedir ataques de timing.
Veja HMAC-SHA512 para exemplos de implementação em 6 linguagens.
Camada 3 -- IP Whitelist
Toda API Key deve ter pelo menos um IP na whitelist -- mesmo uma API Key recém-criada com credenciais válidas é rejeitada enquanto a whitelist estiver vazia:
{
"error": {
"status": 403,
"message": "IP whitelist required. Configure at least one allowed IP to use this API key."
}
}Depois que a whitelist tem pelo menos uma entrada, requisições vindas de IPs fora da lista recebem:
{
"error": {
"status": 403,
"message": "Request IP not in API key whitelist"
}
}Formatos aceitos
| Formato | Exemplo | Comentário |
|---|---|---|
| IPv4 individual | 203.0.113.45 | Apenas um endpoint público |
| Notação CIDR (IPv4) | 203.0.113.0/24 | Sub-rede /24 inteira (256 IPs). Use /32 para um único host |
| CIDR agregado | 172.20.16.0/20 | Range privado (exemplo) -- aceito literalmente |
IPv4 vs IPv6
O backend normaliza endereços ::ffff:A.B.C.D (IPv4 mapeado em IPv6, usado pelo load balancer do GKE) para o endereço IPv4 correspondente antes de comparar com a whitelist. Você não precisa incluir a forma IPv6-mapeada; basta cadastrar o IPv4 literal. Para clientes que saem exclusivamente via IPv6, cadastre o endereço IPv6 completo em notação padrão (ex: 2001:db8::1).
Formato da string
A whitelist espera strings exatas. Um espaço antes/depois, uma máscara errada (/28 quando o range tem 256 IPs), ou um IP em notação com zeros à esquerda (203.000.113.045) rejeita requisições silenciosamente sem nenhum aviso no response além do 403 padrão. Sempre valide no Merchant Portal usando um IP de teste antes de colocar em produção.
Configure a whitelist no Merchant Portal ao criar ou editar a API Key.
Headers
Headers obrigatórios
| Header | Valor | Obrigatório |
|---|---|---|
Authorization | ApiKey {client_id}:{client_secret} ou Basic {base64(client_id:client_secret)} | Sim -- todas as requisições |
Content-Type | application/json (ou multipart/form-data em uploads) | Sim -- POST, PUT, PATCH com body. Enviar application/x-www-form-urlencoded (default do curl -d sem -H) retorna 415 Unsupported Media Type |
hmac | Assinatura HMAC-SHA512 do body em hexadecimal lowercase | Sim -- apenas POST em /api/external/* |
Headers opcionais
| Header | Valor | Efeito |
|---|---|---|
Idempotency-Key | Chave única ≤ 256 chars (UUID v4 recomendado) | Deduplica replays em 24h. Só funciona em POST -- em GET/DELETE o header é silenciosamente ignorado (não retorna erro) |
X-Key-Case | camelCase | Converte camelCase dos request params para snake_case (entrada) e snake_case para camelCase das chaves de toda a response JSON (saída). Útil para clientes em JavaScript, TypeScript ou Kotlin |
X-Forwarded-For | IP(s) separados por vírgula | Respeitado apenas quando a conexão TCP direta vem de proxy confiável (load balancer do GKE). Ignorado em conexões diretas do cliente |
Idempotency-Key -- resposta do servidor
Quando enviar o header Idempotency-Key, o servidor ecoa o mesmo valor de volta em Idempotency-Key no response e, caso a requisição seja um replay de uma já processada nas últimas 24 horas (mesma chave + mesmo método HTTP + mesmo path), adiciona também o header X-Idempotent-Replay: true e retorna o body cacheado tal como foi retornado na primeira execução (mesmo status HTTP, mesmo body byte-a-byte). O cache é escopado por (método, path, chave) -- usar a mesma chave em endpoints diferentes não causa colisão. Chaves com mais de 256 caracteres são rejeitadas com 400 Idempotency-Key must be at most 256 characters. Apenas respostas 2xx são cacheadas -- respostas de erro (4xx/5xx) permitem retry com a mesma chave.
X-Key-Case -- conversão para camelCase
Se o seu stack trabalha em camelCase (JS/TS, Kotlin, Swift), envie X-Key-Case: camelCase e a API aceita request body em camelCase (ex: externalId, pixKey) e devolve a response com chaves em camelCase. Sem esse header, a API permanece em snake_case (ex: external_id, pix_key). O header pode ser enviado em qualquer endpoint /api/external/* -- não precisa ser sempre o mesmo valor por API Key.
HMAC assina o body antes da conversão interna
Se você envia X-Key-Case: camelCase junto com um POST que exige HMAC, assine o body exatamente como vai trafegar na rede (i.e., em camelCase se for essa a serialização que você está usando). O HMAC é calculado no servidor sobre o body recebido, não sobre a forma snake_case interna.
Exemplo Completo
CLIENT_ID="cli_a1b2c3d4e5f6"
CLIENT_SECRET="sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01"
# Consulta de saldo (GET -- sem HMAC)
curl -X GET https://api.owem.com.br/api/external/balance \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"
# PIX Cash-Out (POST -- com HMAC + Idempotency-Key)
# IMPORTANTE: chaves em ordem alfabética (amount < description < pix_key < pix_key_type).
# O servidor reordena alfabeticamente antes de calcular o HMAC esperado — ver /hmac.
BODY='{"amount":3000,"description":"Pagamento","pix_key":"12345678901","pix_key_type":"cpf"}'
HMAC=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$CLIENT_SECRET" | awk '{print $2}')
curl -X POST https://api.owem.com.br/api/external/pix/cash-out \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-H "Idempotency-Key: cashout-order-9876" \
-d "$BODY"Proteções Adicionais
| Proteção | Descrição |
|---|---|
| Rate Limiting (backend) | 90.000 req/min (1.500 req/s) por IP em todo /api/external/*, exceto GET /balance que não possui rate limit -- permitido polling de alta frequência. Chave do limiter: (IP, janela de 60s). Quando o limite é excedido, o servidor responde 429 Too Many Requests com header Retry-After: 60. Em todas as respostas 2xx que passam pelo limiter, o header x-ratelimit-remaining é anexado com o número de requests restantes na janela |
| Rate Limiting (Cloud Armor) | Regras por API Key no load balancer Google Cloud (tipicamente 3.000/min por chave). Configurado por merchant. Opera antes do backend, portanto um 429 desta camada não chega a incrementar o contador do backend |
| Rate Limiting (auth) | 5 req/min em endpoints de autenticação de admin/merchant (não se aplica ao /api/external/*) |
| DICT quota per-merchant (ClientLimiter) | Específico do POST /pix/cash-out quando resolve PIX key via DICT. Default 120 req/min por merchant. Excedido: retorna HTTP 202 com status: "queued" e dispara webhook pix.payout.queued -- retry automático por até 120 min. Ver PIX Cash-Out (por chave) |
| Cloud Armor (WAF) | Firewall de aplicação protegendo o cluster com regras OWASP (XSS, SQLi, LFI, RFI, RCE) |
| HTTPS + TLS 1.2+ | Criptografia obrigatória em todas as conexões |
| HSTS | Navegadores forçados a usar HTTPS |
Headers de rate limiting na resposta
Sempre que um request atravessa o plug do rate limiter, a resposta inclui:
| Header | Aparece em | Valor |
|---|---|---|
x-ratelimit-remaining | Respostas 2xx (após passar pelo limiter) | Número inteiro: requests restantes na janela atual de 60s, escopado por IP |
Retry-After | Apenas em 429 Too Many Requests | 60 (sempre, em segundos) -- aguarde antes de tentar novamente |
Como o limiter conta "janelas"
O limiter usa janelas fixas de 60 segundos, não sliding window. A chave ETS é (IP, floor(now_ms / 60000)). Isto significa que, em teoria, um cliente pode acumular até 180.000 requests em 60 segundos reais se fizer 90.000 no final de uma janela e 90.000 no começo da próxima. Na prática, esse burst é imperceptível e o rate estável de 1.500 req/s é o que importa.
Por que HMAC-SHA512 e não mTLS?
O mTLS (mutual TLS) autentica a conexão, não o conteúdo. Se a conexão está autenticada, todas as requisições passam sem validação individual.
O HMAC valida cada requisição separadamente. Mesmo dentro de uma conexão válida, qualquer alteração no payload faz a requisição ser rejeitada.
| Aspecto | mTLS | HMAC-SHA512 |
|---|---|---|
| Valida | Canal TLS | Payload da requisição |
| Gestão | Certificados X.509 (emissão, rotação, revogação, CRL/OCSP) | Gera par, atualiza, invalida |
| Risco operacional | Certificados expirados -- causa frequente de incidentes | Chave é string simples |
| Integridade do conteúdo | Não | Sim |
O TLS já garante criptografia do transporte. O HMAC adiciona integridade e autenticidade do payload -- algo que o mTLS por si só não cobre.
Respostas de Erro
A API tem 4 formatos distintos de erro
Dependendo de qual plug do pipeline rejeita a requisição, o shape do body JSON é diferente. Sempre inspecione o shape antes de fazer o parsing do erro no cliente. Em ordem de aparição no pipeline:
{"error": {status, message}}--Content-Type(415),ApiKeyAuth(401/403),Idempotency(400),RateLimiter(429).{"error": "forbidden", "message": "..."}-- apenasRequirePermission(403).{"worked": false, "detail": "..."}-- apenasHmacValidation(400, 401, 403).{"errors": {atom: "msg"}}--FallbackController(qualquer erro de negócio 4xx/5xx).
Camada 0 -- Content-Type (plug RequireJsonContentType)
Antes de qualquer autenticação, POST/PUT/PATCH sem um Content-Type aceito (application/json ou multipart/form-data) é bloqueado.
415 -- Content-Type não suportado
{
"error": {
"status": 415,
"message": "Unsupported Media Type. Expected Content-Type: application/json",
"hint": "Add header: -H 'Content-Type: application/json'"
}
}Armadilha comum com curl -d
curl -d '{"...":""}' URL (sem -H) envia Content-Type: application/x-www-form-urlencoded por default. O Plug.Parsers então trata o JSON como se fosse uma única chave de form, e o controller reclama de "campos obrigatórios ausentes". O plug RequireJsonContentType evita esse ruído devolvendo 415 com a dica.
Camada 1 -- API Key + IP Whitelist (plug ApiKeyAuth)
Erros de credenciais ausentes, inválidas, API Key inativa/expirada ou IP fora da whitelist vêm no formato {"error": {status, message}}:
401 -- Credenciais Ausentes
{
"error": {
"status": 401,
"message": "Missing API key credentials. Use Authorization: ApiKey <client_id>:<client_secret>"
}
}401 -- Credenciais Inválidas
{
"error": {
"status": 401,
"message": "Invalid API key credentials"
}
}401 -- API Key Inativa
{
"error": {
"status": 401,
"message": "API key is inactive"
}
}401 -- API Key Expirada
{
"error": {
"status": 401,
"message": "API key has expired"
}
}403 -- IP Whitelist Vazia
{
"error": {
"status": 403,
"message": "IP whitelist required. Configure at least one allowed IP to use this API key."
}
}403 -- IP não Autorizado
{
"error": {
"status": 403,
"message": "Request IP not in API key whitelist"
}
}403 -- Conta Inativa
{
"error": {
"status": 403,
"message": "Account is not active"
}
}403 -- Permissão Insuficiente
Este 403 vem de outro plug
Este erro é emitido pelo plug RequirePermission que roda depois do ApiKeyAuth (já passou a autenticação da API Key, o check de IP whitelist e o rate limiter), já dentro do controller. Por isso o shape do JSON é diferente dos outros 401/403 desta camada: usa {"error": "forbidden", "message": "..."} (string em vez de objeto).
{
"error": "forbidden",
"message": "API key lacks permission: transfer:write"
}Camada 2 -- Validação HMAC-SHA512 (plug HMAC)
Erros emitidos pelo plug HmacValidation sempre vêm no formato {"worked": false, "detail": "..."} com diferentes códigos HTTP conforme a causa:
401 -- Assinatura inválida ou header ausente
{
"worked": false,
"detail": "Invalid HMAC signature"
}{
"worked": false,
"detail": "Missing HMAC header"
}400 -- Body ausente ou JSON inválido
{
"worked": false,
"detail": "Request body is required for HMAC validation"
}{
"worked": false,
"detail": "Request body must be valid JSON for HMAC validation"
}403 -- API Key sem HMAC secret configurado
{
"worked": false,
"detail": "HMAC secret not configured for this API key"
}Camada 3 -- Erros de Negócio (FallbackController)
Depois da autenticação, erros de validação, parâmetros faltantes, recursos não encontrados e regras de negócio vêm no formato {"errors": {atom: "msg"}}:
400 -- Requisição Inválida
{
"errors": {
"bad_request": {
"amount": ["is required"]
}
}
}404 -- Recurso Não Encontrado
{
"errors": {
"not_found": "Transaction not found"
}
}401 -- Não Autorizado (regra de negócio)
{
"errors": {
"unauthorized": "invalid credentials"
}
}422 -- Entidade Não Processável
{
"errors": {
"unprocessable_entity": "Invalid PIX key format"
}
}Camada 4 -- Rate Limiting (plug RateLimiter ou Cloud Armor)
429 -- Rate Limit Excedido
Body do 429 vindo do backend (plug RateLimiter):
{
"error": {
"status": 429,
"message": "Too many requests. Please try again later."
}
}Headers incluídos na resposta 429:
| Header | Valor |
|---|---|
Retry-After | 60 (segundos a aguardar antes de retry) |
Como reagir ao 429
- Backoff exponencial: comece com 60s (valor do
Retry-After), dobre a cada retry subsequente até um teto razoável (ex: 5 min). - Nunca ignore o header: mesmo que o seu cliente tenha sua própria estratégia de retry, o
Retry-Afteré a fonte de verdade canônica para este endpoint. - Se 429 for do Cloud Armor (camada acima do backend), o body pode ter shape diferente -- o status 429 com
Retry-After: 60continua sendo o padrão para qualquer camada.
Camada 5 -- Idempotency (plug Idempotency)
400 -- Idempotency-Key muito longa
{
"error": {
"status": 400,
"message": "Idempotency-Key must be at most 256 characters"
}
}Permissões (Permissions)
Cada API Key possui uma lista de permissões que determinam quais endpoints podem ser acessados. Se a API Key não possuir a permissão necessária, a requisição é rejeitada com 403 Forbidden.
Como cadastrar a API Key
- Acesse o painel administrativo em core.owem.com.br
- Navegue até Segurança → API Keys
- Clique em Criar API Key
- Preencha o nome, selecione a conta e adicione os IPs na whitelist
- Marque as permissões necessárias (veja tabela abaixo)
- Clique em salvar. O
client_ideclient_secretserão exibidos uma única vez -- copie e armazene com segurança
Para editar permissões de uma API Key existente, clique no ícone de permissões na listagem de API Keys.
Obrigatório para enviar PIX
Para realizar operações de PIX Cash-Out (envio de PIX), a API Key deve ter a permissão transfer:write. Sem essa permissão, todas as tentativas de envio retornam 403 Forbidden com a mensagem API key lacks permission: transfer:write.
Permissões mínimas recomendadas para operação completa:
- Cash-In (receber):
pix:write+transfer:read - Cash-Out (enviar):
transfer:write+transfer:read - Consultas:
transfer:read+account:read+statement:read - Webhooks:
account:write+account:read
Permissões Disponíveis
| Permissão | Descrição |
|---|---|
pix:write | Gerar QR Code (Cash-In) |
pix:read | Listar chaves PIX |
transfer:write | Enviar PIX (Cash-Out) |
transfer:read | Consultar transações (por ID, E2E, Tag, External ID), comprovante, listar transações |
payment:write | Solicitar devolução (refund) |
payment:read | Listar e consultar MED |
account:write | Criar e remover webhooks |
account:read | Consultar saldo, listar webhooks, validar CPF |
statement:read | Consultar extrato |
Permissões por Endpoint
| Endpoint | Método | Permissão |
|---|---|---|
/pix/cash-in | POST | pix:write |
/pix/cash-out | POST | transfer:write |
/pix/refund | POST | payment:write |
/cpf/validate | POST | account:read |
/webhooks | POST | account:write |
/webhooks | GET | account:read |
/webhooks/:id | DELETE | account:write |
/balance | GET | account:read |
/transactions | GET | transfer:read |
/transactions/:id | GET | transfer:read |
/transactions/e2e/:e2e_id | GET | transfer:read |
/transactions/tag/:tag | GET | transfer:read |
/transactions/ref/:external_id | GET | transfer:read |
/transactions/:id/receipt | GET | transfer:read |
/pix/keys | GET | pix:read |
/med | GET | payment:read |
/med/:id | GET | payment:read |
/statement | GET | statement:read |
Resposta de Erro -- 403 (Permissão Insuficiente)
Veja o formato em Camada 1 -- 403 Permissão Insuficiente.
As permissões são configuradas na criação da API Key pelo Merchant Portal ou pela API de administração.
Segurança
- Nunca exponha o
client_secretem código frontend ou repositórios públicos - Utilize variáveis de ambiente no seu servidor
- A API Key pode expirar se o campo
expires_atestiver configurado; caso contrário, permanece válida até ser revogada manualmente no Merchant Portal - Configure IPs permitidos na whitelist -- uma whitelist vazia bloqueia a chave com
403 IP whitelist required