Skip to content

Autenticacion

La API externa de Owem Pay utiliza un modelo de seguridad en tres capas principales de autenticacion -- API Key + Secret, firma HMAC-SHA512 por solicitud (solo en POST) y whitelist de IP obligatoria -- ejecutadas dentro de un pipeline mayor de plugs que tambien valida Content-Type, aplica rate limiting e implementa idempotencia.

Vision General del Pipeline

La solicitud HTTP pasa por los plugs abajo, en este orden. Cualquier plug puede interrumpir el pipeline con un error terminal (halt):

POST /api/external/...

  ├─ 1. Content-Type ──────── No es application/json ni multipart/form-data? → 415
  ├─ 2. X-Key-Case (KeyCase) ─ Convierte params/response snake_case ↔ camelCase (opcional)
  ├─ 3. API Key + Secret ───── Credenciales ausentes/invalidas? → 401 | API Key inactiva? → 401 | API Key expirada? → 401
  ├─ 4. IP Whitelist ───────── Whitelist vacia? → 403 "ip whitelist required" | IP fuera? → 403 "ip not allowed" | Cuenta inactiva? → 403
  ├─ 5. HMAC-SHA512 (POST) ─── Header `hmac` ausente? → 401 | Firma invalida? → 401 | Body invalido? → 400 | API Key sin secret? → 403
  ├─ 6. Rate Limiter (ETS) ─── Mas de 90.000 req/min por IP? → 429 Retry-After: 60
  ├─ 7. Idempotency (POST) ─── Clave > 256 chars? → 400 | Replay en 24h? → retorna body cacheado + X-Idempotent-Replay: true
  └─ 8. RequirePermission ───── API Key sin el permiso exigido por la ruta? → 403

       └─ Solicitud aceptada → Controller/logica de negocio

Pipeline por metodo HTTP

  • GET /balance usa solo Content-Type → KeyCase → API Key + Secret → IP Whitelist → RequirePermission -- no hay rate limiter, no hay HMAC, no hay idempotencia (es polling de alta frecuencia autorizado).
  • Otros GET / DELETE usan Content-Type → KeyCase → API Key + Secret → IP Whitelist → Rate Limiter → RequirePermission -- sin HMAC y sin idempotencia.
  • POST usa el pipeline completo arriba (todos los 8 plugs).

Capa 1 -- API Key + Secret

Todas las solicitudes deben incluir el header Authorization. La API acepta dos formatos equivalentes -- el ApiKey scheme nativo o el HTTP Basic Authentication. Ambos son validados por el mismo plug (ApiKeyAuth) y tienen el mismo comportamiento. Escoja el que sea mas conveniente para su cliente HTTP.

Formato recomendado -- ApiKey scheme

Authorization: ApiKey {client_id}:{client_secret}

Formato alternativo -- HTTP Basic

Authorization: Basic {base64(client_id:client_secret)}

Ejemplo en 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"

Cuando usar Basic vs ApiKey

Use Basic si su cliente HTTP (biblioteca, gateway, proxy) ya monta credenciales via base64 automaticamente (casi todos lo hacen). Use ApiKey si prefiere enviar el secret en texto puro dentro del header -- ambos caen en el mismo parser en el backend.

Campos de la credencial

ComponenteDescripcionPrefijo
client_idIdentificador publico de la API Keycli_
client_secretClave secreta (almacenamos solo el hash)sk_

El secret nunca se almacena en texto puro. Cuando una solicitud llega, el secret enviado es comparado con el hash almacenado. Si no coincide, la solicitud se rechaza antes de llegar a la logica de negocio.

API Key puede expirar

A pesar de que en la practica la mayoria de las API Keys son creadas sin fecha de expiracion, el campo expires_at existe en el schema. Si una clave es configurada con expires_at en el pasado, la autenticacion falla con 401 API key has expired. Tambien es posible revocar (marcar como inactiva): retorna 401 API key is inactive. Esto sustituye la informacion anterior de esta documentacion que afirmaba que API Keys eran permanentes.

Capa 2 -- HMAC-SHA512

Solicitudes transaccionales (POST, PUT, PATCH) exigen firma HMAC-SHA512 del body en el header hmac. La validacion usa comparacion en tiempo constante (constant-time comparison) para impedir ataques de timing.

Vea HMAC-SHA512 para ejemplos de implementacion en 6 lenguajes.

Capa 3 -- IP Whitelist

Toda API Key debe tener al menos un IP en la whitelist -- incluso una API Key recien creada con credenciales validas es rechazada mientras la whitelist este vacia:

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

Despues que la whitelist tiene al menos una entrada, solicitudes provenientes de IPs fuera de la lista reciben:

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

Formatos aceptados

FormatoEjemploComentario
IPv4 individual203.0.113.45Solo un endpoint publico
Notacion CIDR (IPv4)203.0.113.0/24Sub-red /24 entera (256 IPs). Use /32 para un solo host
CIDR agregado172.20.16.0/20Range privado (ejemplo) -- aceptado literalmente

IPv4 vs IPv6

El backend normaliza direcciones ::ffff:A.B.C.D (IPv4 mapeado en IPv6, usado por el load balancer del GKE) a la direccion IPv4 correspondiente antes de comparar con la whitelist. Usted no necesita incluir la forma IPv6-mapeada; basta registrar el IPv4 literal. Para clientes que salen exclusivamente via IPv6, registre la direccion IPv6 completa en notacion estandar (ej: 2001:db8::1).

Formato del string

La whitelist espera strings exactos. Un espacio antes/despues, una mascara incorrecta (/28 cuando el range tiene 256 IPs), o un IP en notacion con ceros a la izquierda (203.000.113.045) rechaza solicitudes silenciosamente sin ningun aviso en el response mas alla del 403 estandar. Siempre valide en el Merchant Portal usando un IP de prueba antes de colocar en produccion.

Configure la whitelist en el Merchant Portal al crear o editar la API Key.

Headers

Headers obligatorios

HeaderValorObligatorio
AuthorizationApiKey {client_id}:{client_secret} o Basic {base64(client_id:client_secret)}Si -- todas las solicitudes
Content-Typeapplication/json (o multipart/form-data en uploads)Si -- POST, PUT, PATCH con body. Enviar application/x-www-form-urlencoded (default de curl -d sin -H) retorna 415 Unsupported Media Type
hmacFirma HMAC-SHA512 del body en hexadecimal lowercaseSi -- solo POST en /api/external/*

Headers opcionales

HeaderValorEfecto
Idempotency-KeyClave unica ≤ 256 chars (UUID v4 recomendado)Deduplica replays en 24h. Solo funciona en POST -- en GET/DELETE el header es silenciosamente ignorado (no retorna error)
X-Key-CasecamelCaseConvierte camelCase de los request params a snake_case (entrada) y snake_case a camelCase de las claves de toda la response JSON (salida). Util para clientes en JavaScript, TypeScript o Kotlin
X-Forwarded-ForIP(s) separados por comaRespetado solo cuando la conexion TCP directa viene de proxy confiable (load balancer del GKE). Ignorado en conexiones directas del cliente

Idempotency-Key -- respuesta del servidor

Cuando envie el header Idempotency-Key, el servidor reproduce el mismo valor en Idempotency-Key en el response y, en caso que la solicitud sea un replay de una ya procesada en las ultimas 24 horas (misma clave + mismo metodo HTTP + mismo path), agrega tambien el header X-Idempotent-Replay: true y retorna el body cacheado tal como fue retornado en la primera ejecucion (mismo status HTTP, mismo body byte-a-byte). El cache es scope por (metodo, path, clave) -- usar la misma clave en endpoints diferentes no causa colision. Claves con mas de 256 caracteres son rechazadas con 400 Idempotency-Key must be at most 256 characters. Solo respuestas 2xx son cacheadas -- respuestas de error (4xx/5xx) permiten retry con la misma clave.

X-Key-Case -- conversion a camelCase

Si su stack trabaja en camelCase (JS/TS, Kotlin, Swift), envie X-Key-Case: camelCase y la API acepta request body en camelCase (ej: externalId, pixKey) y devuelve la response con claves en camelCase. Sin ese header, la API permanece en snake_case (ej: external_id, pix_key). El header puede ser enviado en cualquier endpoint /api/external/* -- no necesita ser siempre el mismo valor por API Key.

HMAC firma el body antes de la conversion interna

Si usted envia X-Key-Case: camelCase junto con un POST que exige HMAC, firme el body exactamente como va a transitar en la red (es decir, en camelCase si esa es la serializacion que usted esta usando). El HMAC es calculado en el servidor sobre el body recibido, no sobre la forma snake_case interna.

Ejemplo Completo

bash
CLIENT_ID="cli_a1b2c3d4e5f6"
CLIENT_SECRET="sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01"

# Consulta de saldo (GET -- sin HMAC)
curl -X GET https://api.owem.com.br/api/external/balance \
  -H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"

# PIX Cash-Out (POST -- con HMAC + Idempotency-Key)
# IMPORTANTE: claves en orden alfabetico (amount < description < pix_key < pix_key_type).
# El servidor reordena alfabeticamente antes de calcular el HMAC esperado — ver /es/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"

Protecciones Adicionales

ProteccionDescripcion
Rate Limiting (backend)90.000 req/min (1.500 req/s) por IP en todo /api/external/*, excepto GET /balance que no tiene rate limit -- permitido polling de alta frecuencia. Clave del limiter: (IP, ventana de 60s). Cuando el limite es excedido, el servidor responde 429 Too Many Requests con header Retry-After: 60. En todas las respuestas 2xx que pasan por el limiter, el header x-ratelimit-remaining es agregado con el numero de requests restantes en la ventana
Rate Limiting (Cloud Armor)Reglas por API Key en el load balancer Google Cloud (tipicamente 3.000/min por clave). Configurado por merchant. Opera antes del backend, por lo tanto un 429 de esta capa no llega a incrementar el contador del backend
Rate Limiting (auth)5 req/min en endpoints de autenticacion de admin/merchant (no aplica a /api/external/*)
DICT quota per-merchant (ClientLimiter)Especifico del POST /pix/cash-out cuando resuelve PIX key via DICT. Default 120 req/min por merchant. Excedido: retorna HTTP 202 con status: "queued" y dispara webhook pix.payout.queued -- retry automatico por hasta 120 min. Ver PIX Cash-Out (por clave)
Cloud Armor (WAF)Firewall de aplicacion protegiendo el cluster con reglas OWASP (XSS, SQLi, LFI, RFI, RCE)
HTTPS + TLS 1.2+Cifrado obligatorio en todas las conexiones
HSTSNavegadores forzados a usar HTTPS

Headers de rate limiting en la respuesta

Siempre que un request atraviesa el plug del rate limiter, la respuesta incluye:

HeaderAparece enValor
x-ratelimit-remainingRespuestas 2xx (despues de pasar por el limiter)Numero entero: requests restantes en la ventana actual de 60s, scope por IP
Retry-AfterSolo en 429 Too Many Requests60 (siempre, en segundos) -- espere antes de intentar de nuevo

Como el limiter cuenta "ventanas"

El limiter usa ventanas fijas de 60 segundos, no sliding window. La clave ETS es (IP, floor(now_ms / 60000)). Esto significa que, en teoria, un cliente puede acumular hasta 180.000 requests en 60 segundos reales si hace 90.000 al final de una ventana y 90.000 al inicio de la siguiente. En la practica, ese burst es imperceptible y el rate estable de 1.500 req/s es lo que importa.

Por que HMAC-SHA512 y no mTLS?

El mTLS (mutual TLS) autentica la conexion, no el contenido. Si la conexion esta autenticada, todas las solicitudes pasan sin validacion individual.

El HMAC valida cada solicitud por separado. Incluso dentro de una conexion valida, cualquier alteracion en el payload hace que la solicitud sea rechazada.

AspectomTLSHMAC-SHA512
ValidaCanal TLSPayload de la solicitud
GestionCertificados X.509 (emision, rotacion, revocacion, CRL/OCSP)Genera par, actualiza, invalida
Riesgo operacionalCertificados expirados -- causa frecuente de incidentesClave es string simple
Integridad del contenidoNoSi

El TLS ya garantiza cifrado del transporte. El HMAC agrega integridad y autenticidad del payload -- algo que el mTLS por si solo no cubre.

Respuestas de Error

La API tiene 4 formatos distintos de error

Dependiendo de cual plug del pipeline rechaza la solicitud, el shape del body JSON es diferente. Siempre inspeccione el shape antes de hacer el parsing del error en el cliente. En orden de aparicion en el pipeline:

  1. {"error": {status, message}} -- Content-Type (415), ApiKeyAuth (401/403), Idempotency (400), RateLimiter (429).
  2. {"error": "forbidden", "message": "..."} -- solo RequirePermission (403).
  3. {"worked": false, "detail": "..."} -- solo HmacValidation (400, 401, 403).
  4. {"errors": {atom: "msg"}} -- FallbackController (cualquier error de negocio 4xx/5xx).

Capa 0 -- Content-Type (plug RequireJsonContentType)

Antes de cualquier autenticacion, POST/PUT/PATCH sin un Content-Type aceptado (application/json o multipart/form-data) es bloqueado.

415 -- Content-Type no soportado

json
{
  "error": {
    "status": 415,
    "message": "Unsupported Media Type. Expected Content-Type: application/json",
    "hint": "Add header: -H 'Content-Type: application/json'"
  }
}

Trampa comun con curl -d

curl -d '{"...":""}' URL (sin -H) envia Content-Type: application/x-www-form-urlencoded por default. El Plug.Parsers trata el JSON como si fuera una unica clave de form, y el controller se queja de "campos obligatorios ausentes". El plug RequireJsonContentType evita ese ruido devolviendo 415 con la pista.

Capa 1 -- API Key + IP Whitelist (plug ApiKeyAuth)

Errores de credenciales ausentes, invalidas, API Key inactiva/expirada o IP fuera de la whitelist vienen en el formato {"error": {status, message}}:

401 -- Credenciales Ausentes

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

401 -- Credenciales Invalidas

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

401 -- API Key Inactiva

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 Vacia

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

403 -- IP no Autorizado

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

403 -- Cuenta Inactiva

json
{
  "error": {
    "status": 403,
    "message": "Account is not active"
  }
}

403 -- Permiso Insuficiente

Este 403 viene de otro plug

Este error es emitido por el plug RequirePermission que corre despues del ApiKeyAuth (ya paso la autenticacion de la API Key, el check de IP whitelist y el rate limiter), ya dentro del controller. Por eso el shape del JSON es diferente de los otros 401/403 de esta capa: usa {"error": "forbidden", "message": "..."} (string en vez de objeto).

json
{
  "error": "forbidden",
  "message": "API key lacks permission: transfer:write"
}

Capa 2 -- Validacion HMAC-SHA512 (plug HMAC)

Errores emitidos por el plug HmacValidation siempre vienen en el formato {"worked": false, "detail": "..."} con diferentes codigos HTTP segun la causa:

401 -- Firma invalida o header ausente

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

400 -- Body ausente o JSON invalido

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 sin HMAC secret configurado

json
{
  "worked": false,
  "detail": "HMAC secret not configured for this API key"
}

Capa 3 -- Errores de Negocio (FallbackController)

Despues de la autenticacion, errores de validacion, parametros faltantes, recursos no encontrados y reglas de negocio vienen en el formato {"errors": {atom: "msg"}}:

400 -- Solicitud Invalida

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

404 -- Recurso No Encontrado

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

401 -- No Autorizado (regla de negocio)

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

422 -- Entidad No Procesable

json
{
  "errors": {
    "unprocessable_entity": "Invalid PIX key format"
  }
}

Capa 4 -- Rate Limiting (plug RateLimiter o Cloud Armor)

429 -- Rate Limit Excedido

Body del 429 viniendo del backend (plug RateLimiter):

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

Headers incluidos en la respuesta 429:

HeaderValor
Retry-After60 (segundos a esperar antes de retry)

Como reaccionar al 429

  • Backoff exponencial: comience con 60s (valor del Retry-After), doble a cada retry siguiente hasta un techo razonable (ej: 5 min).
  • Nunca ignore el header: incluso si su cliente tiene su propia estrategia de retry, el Retry-After es la fuente de verdad canonica para este endpoint.
  • Si 429 es del Cloud Armor (capa arriba del backend), el body puede tener shape diferente -- el status 429 con Retry-After: 60 continua siendo el estandar para cualquier capa.

Capa 5 -- Idempotency (plug Idempotency)

400 -- Idempotency-Key muy larga

json
{
  "error": {
    "status": 400,
    "message": "Idempotency-Key must be at most 256 characters"
  }
}

Permisos (Permissions)

Cada API Key posee una lista de permisos que determinan cuales endpoints pueden ser accesados. Si la API Key no posee el permiso necesario, la solicitud es rechazada con 403 Forbidden.

Como registrar la API Key

  1. Acceda al panel administrativo en core.owem.com.br
  2. Navegue hasta Seguridad → API Keys
  3. Haga clic en Crear API Key
  4. Llene el nombre, seleccione la cuenta y agregue los IPs en la whitelist
  5. Marque los permisos necesarios (vea tabla abajo)
  6. Haga clic en guardar. El client_id y client_secret seran exhibidos una unica vez -- copie y almacene con seguridad

Para editar permisos de una API Key existente, haga clic en el icono de permisos en el listado de API Keys.

Obligatorio para enviar PIX

Para realizar operaciones de PIX Cash-Out (envio de PIX), la API Key debe tener el permiso transfer:write. Sin ese permiso, todas las tentativas de envio retornan 403 Forbidden con el mensaje API key lacks permission: transfer:write.

Permisos minimos recomendados para operacion completa:

  • Cash-In (recibir): pix:write + transfer:read
  • Cash-Out (enviar): transfer:write + transfer:read
  • Consultas: transfer:read + account:read + statement:read
  • Webhooks: account:write + account:read

Permisos Disponibles

PermisoDescripcion
pix:writeGenerar QR Code (Cash-In)
pix:readListar claves PIX
transfer:writeEnviar PIX (Cash-Out)
transfer:readConsultar transacciones (por ID, E2E, Tag, External ID), comprobante, listar transacciones
payment:writeSolicitar devolucion (refund)
payment:readListar y consultar MED
account:writeCrear y remover webhooks
account:readConsultar saldo, listar webhooks, validar CPF
statement:readConsultar extracto

Permisos por Endpoint

EndpointMetodoPermiso
/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

Respuesta de Error -- 403 (Permiso Insuficiente)

Vea el formato en Capa 1 -- 403 Permiso Insuficiente.

Los permisos son configurados en la creacion de la API Key por el Merchant Portal o por la API de administracion.

Seguridad

  • Nunca exponga el client_secret en codigo frontend o repositorios publicos
  • Utilice variables de ambiente en su servidor
  • La API Key puede expirar si el campo expires_at esta configurado; de lo contrario, permanece valida hasta ser revocada manualmente en el Merchant Portal
  • Configure IPs permitidos en la whitelist -- una whitelist vacia bloquea la clave con 403 IP whitelist required

Owem Pay Instituição de Pagamento — ISPB 37839059