Skip to content

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ócio

Pipeline por método HTTP

  • GET /balance usa apenas Content-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 / DELETE usam Content-Type → KeyCase → API Key + Secret → IP Whitelist → Rate Limiter → RequirePermission -- sem HMAC e sem idempotência.
  • POST usa 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:

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

ComponenteDescriçãoPrefixo
client_idIdentificador público da API Keycli_
client_secretChave 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:

json
{
  "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:

json
{
  "error": {
    "status": 403,
    "message": "Request IP not in API key whitelist"
  }
}

Formatos aceitos

FormatoExemploComentário
IPv4 individual203.0.113.45Apenas um endpoint público
Notação CIDR (IPv4)203.0.113.0/24Sub-rede /24 inteira (256 IPs). Use /32 para um único host
CIDR agregado172.20.16.0/20Range 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

HeaderValorObrigatório
AuthorizationApiKey {client_id}:{client_secret} ou Basic {base64(client_id:client_secret)}Sim -- todas as requisições
Content-Typeapplication/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
hmacAssinatura HMAC-SHA512 do body em hexadecimal lowercaseSim -- apenas POST em /api/external/*

Headers opcionais

HeaderValorEfeito
Idempotency-KeyChave ú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-CasecamelCaseConverte 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-ForIP(s) separados por vírgulaRespeitado 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

bash
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çãoDescriçã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
HSTSNavegadores forçados a usar HTTPS

Headers de rate limiting na resposta

Sempre que um request atravessa o plug do rate limiter, a resposta inclui:

HeaderAparece emValor
x-ratelimit-remainingRespostas 2xx (após passar pelo limiter)Número inteiro: requests restantes na janela atual de 60s, escopado por IP
Retry-AfterApenas em 429 Too Many Requests60 (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.

AspectomTLSHMAC-SHA512
ValidaCanal TLSPayload da requisição
GestãoCertificados X.509 (emissão, rotação, revogação, CRL/OCSP)Gera par, atualiza, invalida
Risco operacionalCertificados expirados -- causa frequente de incidentesChave é string simples
Integridade do conteúdoNãoSim

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:

  1. {"error": {status, message}} -- Content-Type (415), ApiKeyAuth (401/403), Idempotency (400), RateLimiter (429).
  2. {"error": "forbidden", "message": "..."} -- apenas RequirePermission (403).
  3. {"worked": false, "detail": "..."} -- apenas HmacValidation (400, 401, 403).
  4. {"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

json
{
  "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

json
{
  "error": {
    "status": 401,
    "message": "Missing API key credentials. Use Authorization: ApiKey <client_id>:<client_secret>"
  }
}

401 -- Credenciais Inválidas

json
{
  "error": {
    "status": 401,
    "message": "Invalid API key credentials"
  }
}

401 -- API Key Inativa

json
{
  "error": {
    "status": 401,
    "message": "API key is inactive"
  }
}

401 -- API Key Expirada

json
{
  "error": {
    "status": 401,
    "message": "API key has expired"
  }
}

403 -- IP Whitelist Vazia

json
{
  "error": {
    "status": 403,
    "message": "IP whitelist required. Configure at least one allowed IP to use this API key."
  }
}

403 -- IP não Autorizado

json
{
  "error": {
    "status": 403,
    "message": "Request IP not in API key whitelist"
  }
}

403 -- Conta Inativa

json
{
  "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).

json
{
  "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

json
{
  "worked": false,
  "detail": "Invalid HMAC signature"
}
json
{
  "worked": false,
  "detail": "Missing HMAC header"
}

400 -- Body ausente ou JSON inválido

json
{
  "worked": false,
  "detail": "Request body is required for HMAC validation"
}
json
{
  "worked": false,
  "detail": "Request body must be valid JSON for HMAC validation"
}

403 -- API Key sem HMAC secret configurado

json
{
  "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

json
{
  "errors": {
    "bad_request": {
      "amount": ["is required"]
    }
  }
}

404 -- Recurso Não Encontrado

json
{
  "errors": {
    "not_found": "Transaction not found"
  }
}

401 -- Não Autorizado (regra de negócio)

json
{
  "errors": {
    "unauthorized": "invalid credentials"
  }
}

422 -- Entidade Não Processável

json
{
  "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):

json
{
  "error": {
    "status": 429,
    "message": "Too many requests. Please try again later."
  }
}

Headers incluídos na resposta 429:

HeaderValor
Retry-After60 (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: 60 continua sendo o padrão para qualquer camada.

Camada 5 -- Idempotency (plug Idempotency)

400 -- Idempotency-Key muito longa

json
{
  "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

  1. Acesse o painel administrativo em core.owem.com.br
  2. Navegue até Segurança → API Keys
  3. Clique em Criar API Key
  4. Preencha o nome, selecione a conta e adicione os IPs na whitelist
  5. Marque as permissões necessárias (veja tabela abaixo)
  6. Clique em salvar. O client_id e client_secret serã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ãoDescrição
pix:writeGerar QR Code (Cash-In)
pix:readListar chaves PIX
transfer:writeEnviar PIX (Cash-Out)
transfer:readConsultar transações (por ID, E2E, Tag, External ID), comprovante, listar transações
payment:writeSolicitar devolução (refund)
payment:readListar e consultar MED
account:writeCriar e remover webhooks
account:readConsultar saldo, listar webhooks, validar CPF
statement:readConsultar extrato

Permissões por Endpoint

EndpointMétodoPermissão
/pix/cash-inPOSTpix:write
/pix/cash-outPOSTtransfer:write
/pix/refundPOSTpayment:write
/cpf/validatePOSTaccount:read
/webhooksPOSTaccount:write
/webhooksGETaccount:read
/webhooks/:idDELETEaccount:write
/balanceGETaccount:read
/transactionsGETtransfer:read
/transactions/:idGETtransfer:read
/transactions/e2e/:e2e_idGETtransfer:read
/transactions/tag/:tagGETtransfer:read
/transactions/ref/:external_idGETtransfer:read
/transactions/:id/receiptGETtransfer:read
/pix/keysGETpix:read
/medGETpayment:read
/med/:idGETpayment:read
/statementGETstatement: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_secret em 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_at estiver 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

Owem Pay Instituição de Pagamento — ISPB 37839059