Skip to content

Authentification

L'API externe d'Owem Pay utilise un modèle de sécurité à trois couches principales d'authentification -- API Key + Secret, signature HMAC-SHA512 par requête (uniquement en POST) et whitelist d'IP obligatoire -- exécutées à l'intérieur d'un pipeline plus large de plugs qui valide aussi le Content-Type, applique le rate limiting et implémente l'idempotence.

Vue d'ensemble du pipeline

La requête HTTP traverse les plugs ci-dessous, dans cet ordre. Chaque plug peut interrompre le pipeline avec une erreur terminale (halt) :

POST /api/external/...

  ├─ 1. Content-Type ──────── Pas application/json ni multipart/form-data ? → 415
  ├─ 2. X-Key-Case (KeyCase) ─ Convertit params/response snake_case ↔ camelCase (optionnel)
  ├─ 3. API Key + Secret ───── Credentials absents/invalides ? → 401 | API Key inactive ? → 401 | API Key expirée ? → 401
  ├─ 4. IP Whitelist ───────── Whitelist vide ? → 403 "ip whitelist required" | IP hors liste ? → 403 "ip not allowed" | Compte inactif ? → 403
  ├─ 5. HMAC-SHA512 (POST) ─── Header `hmac` absent ? → 401 | Signature invalide ? → 401 | Body invalide ? → 400 | API Key sans secret ? → 403
  ├─ 6. Rate Limiter (ETS) ─── Plus de 90 000 req/min par IP ? → 429 Retry-After: 60
  ├─ 7. Idempotency (POST) ─── Clé > 256 chars ? → 400 | Replay dans 24h ? → renvoie body en cache + X-Idempotent-Replay: true
  └─ 8. RequirePermission ───── API Key sans la permission exigée par la route ? → 403

       └─ Requête acceptée → Controller / logique métier

Pipeline par méthode HTTP

  • GET /balance utilise uniquement Content-Type → KeyCase → API Key + Secret → IP Whitelist → RequirePermission -- pas de rate limiter, pas de HMAC, pas d'idempotence (polling haute fréquence autorisé).
  • Les autres GET / DELETE utilisent Content-Type → KeyCase → API Key + Secret → IP Whitelist → Rate Limiter → RequirePermission -- sans HMAC ni idempotence.
  • POST utilise le pipeline complet ci-dessus (les 8 plugs).

Couche 1 -- API Key + Secret

Toutes les requêtes doivent inclure l'en-tête Authorization. L'API accepte deux formats équivalents -- le schéma natif ApiKey ou HTTP Basic Authentication. Les deux sont validés par le même plug (ApiKeyAuth) et ont le même comportement. Choisissez celui qui convient le mieux à votre client HTTP.

Format recommandé -- schéma ApiKey

Authorization: ApiKey {client_id}:{client_secret}

Format alternatif -- HTTP Basic

Authorization: Basic {base64(client_id:client_secret)}

Exemple en Bash :

bash
CLIENT_ID="cli_a1b2c3d4e5f6"
CLIENT_SECRET="sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01"

# Équivalent à 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"

Quand utiliser Basic vs ApiKey

Utilisez Basic si votre client HTTP (bibliothèque, gateway, proxy) construit automatiquement les credentials via base64 (la quasi-totalité le fait). Utilisez ApiKey si vous préférez envoyer le secret en clair dans l'en-tête -- les deux passent par le même parser côté backend.

Champs du credential

ComposantDescriptionPréfixe
client_idIdentifiant public de l'API Keycli_
client_secretClé secrète (nous stockons uniquement le hash)sk_

Le secret n'est jamais stocké en clair. Lorsqu'une requête arrive, le secret envoyé est comparé au hash stocké. En cas de non-correspondance, la requête est rejetée avant d'atteindre la logique métier.

L'API Key peut expirer

Bien qu'en pratique la plupart des API Keys soient créées sans date d'expiration, le champ expires_at existe dans le schéma. Si une clé est configurée avec expires_at dans le passé, l'authentification échoue avec 401 API key has expired. Il est également possible de la révoquer (marquer comme inactive) : retourne 401 API key is inactive. Cela remplace l'information précédente de cette documentation qui affirmait que les API Keys étaient permanentes.

Couche 2 -- HMAC-SHA512

Les requêtes transactionnelles (POST, PUT, PATCH) exigent une signature HMAC-SHA512 du body dans l'en-tête hmac. La validation utilise une comparaison en temps constant (constant-time comparison) pour empêcher les attaques par timing.

Consultez HMAC-SHA512 pour des exemples d'implémentation dans 6 langages.

Couche 3 -- IP Whitelist

Toute API Key doit avoir au moins un IP dans la whitelist -- même une API Key fraîchement créée avec des credentials valides est rejetée tant que la whitelist est vide :

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

Une fois la whitelist dotée d'au moins une entrée, les requêtes provenant d'IPs hors liste reçoivent :

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

Formats acceptés

FormatExempleCommentaire
IPv4 individuel203.0.113.45Un seul endpoint public
Notation CIDR (IPv4)203.0.113.0/24Sous-réseau /24 entier (256 IPs). Utilisez /32 pour un hôte unique
CIDR agrégé172.20.16.0/20Plage privée (exemple) -- accepté littéralement

IPv4 vs IPv6

Le backend normalise les adresses ::ffff:A.B.C.D (IPv4 mappé en IPv6, utilisé par le load balancer du GKE) vers l'adresse IPv4 correspondante avant de comparer avec la whitelist. Vous n'avez pas besoin d'inclure la forme IPv6-mappée ; il suffit d'enregistrer l'IPv4 littéral. Pour des clients qui sortent exclusivement via IPv6, enregistrez l'adresse IPv6 complète en notation standard (ex. : 2001:db8::1).

Format de la chaîne

La whitelist attend des chaînes exactes. Un espace avant/après, un masque erroné (/28 quand la plage a 256 IPs), ou un IP en notation avec zéros en tête (203.000.113.045) rejette silencieusement les requêtes sans autre avertissement que le 403 standard. Validez toujours dans le Merchant Portal avec un IP de test avant la mise en production.

Configurez la whitelist dans le Merchant Portal à la création ou à l'édition de l'API Key.

Headers

Headers obligatoires

HeaderValeurObligatoire
AuthorizationApiKey {client_id}:{client_secret} ou Basic {base64(client_id:client_secret)}Oui -- toutes les requêtes
Content-Typeapplication/json (ou multipart/form-data pour les uploads)Oui -- POST, PUT, PATCH avec body. Envoyer application/x-www-form-urlencoded (default de curl -d sans -H) retourne 415 Unsupported Media Type
hmacSignature HMAC-SHA512 du body en hexadécimal lowercaseOui -- uniquement POST sur /api/external/*

Headers optionnels

HeaderValeurEffet
Idempotency-KeyClé unique ≤ 256 caractères (UUID v4 recommandé)Déduplique les replays pendant 24h. Ne fonctionne qu'en POST -- en GET/DELETE l'en-tête est silencieusement ignoré (pas d'erreur)
X-Key-CasecamelCaseConvertit le camelCase des request params en snake_case (entrée) et le snake_case en camelCase pour les clés de toute la response JSON (sortie). Utile pour les clients en JavaScript, TypeScript ou Kotlin
X-Forwarded-ForIP(s) séparés par virguleRespecté uniquement lorsque la connexion TCP directe vient d'un proxy de confiance (load balancer du GKE). Ignoré dans les connexions directes du client

Idempotency-Key -- réponse du serveur

Quand vous envoyez l'en-tête Idempotency-Key, le serveur renvoie la même valeur dans Idempotency-Key dans la response et, si la requête est un replay d'une déjà traitée dans les dernières 24 heures (même clé + même méthode HTTP + même path), il ajoute aussi l'en-tête X-Idempotent-Replay: true et retourne le body en cache tel qu'il a été renvoyé à la première exécution (même code HTTP, même body byte-à-byte). Le cache est limité à (méthode, path, clé) -- utiliser la même clé sur des endpoints différents ne provoque pas de collision. Les clés de plus de 256 caractères sont rejetées avec 400 Idempotency-Key must be at most 256 characters. Seules les réponses 2xx sont mises en cache -- les réponses d'erreur (4xx/5xx) permettent un retry avec la même clé.

X-Key-Case -- conversion en camelCase

Si votre stack travaille en camelCase (JS/TS, Kotlin, Swift), envoyez X-Key-Case: camelCase et l'API acceptera le request body en camelCase (ex. : externalId, pixKey) et renverra la response avec des clés en camelCase. Sans cet en-tête, l'API reste en snake_case (ex. : external_id, pix_key). L'en-tête peut être envoyé sur n'importe quel endpoint /api/external/* -- pas besoin d'être toujours la même valeur par API Key.

HMAC signe le body avant la conversion interne

Si vous envoyez X-Key-Case: camelCase avec un POST qui exige HMAC, signez le body exactement tel qu'il va transiter sur le réseau (i.e., en camelCase si c'est cette sérialisation que vous utilisez). Le HMAC est calculé côté serveur sur le body reçu, pas sur la forme snake_case interne.

Exemple complet

bash
CLIENT_ID="cli_a1b2c3d4e5f6"
CLIENT_SECRET="sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01"

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

# PIX Cash-Out (POST -- avec HMAC + Idempotency-Key)
# IMPORTANT : clés en ordre alphabétique (amount < description < pix_key < pix_key_type).
# Le serveur réordonne alphabétiquement avant de calculer le HMAC attendu — voir /fr/hmac.
BODY='{"amount":3000,"description":"Paiement","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"

Protections additionnelles

ProtectionDescription
Rate Limiting (backend)90 000 req/min (1 500 req/s) par IP sur tout /api/external/*, sauf GET /balance qui n'a pas de rate limit -- polling haute fréquence autorisé. Clé du limiter : (IP, fenêtre de 60s). Quand la limite est dépassée, le serveur répond 429 Too Many Requests avec l'en-tête Retry-After: 60. Dans toutes les réponses 2xx qui traversent le limiter, l'en-tête x-ratelimit-remaining est ajouté avec le nombre de requêtes restantes dans la fenêtre
Rate Limiting (Cloud Armor)Règles par API Key dans le load balancer Google Cloud (typiquement 3 000/min par clé). Configuré par merchant. Opère avant le backend, donc un 429 de cette couche n'incrémente pas le compteur du backend
Rate Limiting (auth)5 req/min sur les endpoints d'authentification admin/merchant (ne s'applique pas à /api/external/*)
Quota DICT per-merchant (ClientLimiter)Spécifique au POST /pix/cash-out quand il résout une clé PIX via DICT. Default 120 req/min par merchant. Dépassé : retourne HTTP 202 avec status: "queued" et déclenche le webhook pix.payout.queued -- retry automatique pendant jusqu'à 120 min. Voir PIX Cash Out (par clé)
Cloud Armor (WAF)Firewall d'application protégeant le cluster avec des règles OWASP (XSS, SQLi, LFI, RFI, RCE)
HTTPS + TLS 1.2+Chiffrement obligatoire sur toutes les connexions
HSTSNavigateurs forcés à utiliser HTTPS

Headers de rate limiting dans la response

Chaque fois qu'une requête traverse le plug du rate limiter, la response inclut :

HeaderApparaît dansValeur
x-ratelimit-remainingRéponses 2xx (après passage par le limiter)Entier : requêtes restantes dans la fenêtre actuelle de 60s, limité par IP
Retry-AfterUniquement dans 429 Too Many Requests60 (toujours, en secondes) -- attendez avant de réessayer

Comment le limiter compte les « fenêtres »

Le limiter utilise des fenêtres fixes de 60 secondes, pas de sliding window. La clé ETS est (IP, floor(now_ms / 60000)). Cela signifie que, en théorie, un client peut accumuler jusqu'à 180 000 requêtes en 60 secondes réelles s'il en fait 90 000 à la fin d'une fenêtre et 90 000 au début de la suivante. En pratique, ce burst est imperceptible et le rate stable de 1 500 req/s est ce qui compte.

Pourquoi HMAC-SHA512 et pas mTLS ?

Le mTLS (mutual TLS) authentifie la connexion, pas le contenu. Si la connexion est authentifiée, toutes les requêtes passent sans validation individuelle.

Le HMAC valide chaque requête séparément. Même à l'intérieur d'une connexion valide, toute altération du payload fait rejeter la requête.

AspectmTLSHMAC-SHA512
ValideCanal TLSPayload de la requête
GestionCertificats X.509 (émission, rotation, révocation, CRL/OCSP)Génère la paire, met à jour, invalide
Risque opérationnelCertificats expirés -- cause fréquente d'incidentsLa clé est une simple chaîne
Intégrité du contenuNonOui

Le TLS garantit déjà le chiffrement du transport. Le HMAC ajoute l'intégrité et l'authenticité du payload -- ce que le mTLS seul ne couvre pas.

Réponses d'erreur

L'API a 4 formats distincts d'erreur

Selon quel plug du pipeline rejette la requête, le shape du body JSON est différent. Inspectez toujours le shape avant de faire le parsing de l'erreur côté client. Dans l'ordre d'apparition dans le pipeline :

  1. {"error": {status, message}} -- Content-Type (415), ApiKeyAuth (401/403), Idempotency (400), RateLimiter (429).
  2. {"error": "forbidden", "message": "..."} -- uniquement RequirePermission (403).
  3. {"worked": false, "detail": "..."} -- uniquement HmacValidation (400, 401, 403).
  4. {"errors": {atom: "msg"}} -- FallbackController (toute erreur métier 4xx/5xx).

Couche 0 -- Content-Type (plug RequireJsonContentType)

Avant toute authentification, POST/PUT/PATCH sans un Content-Type accepté (application/json ou multipart/form-data) est bloqué.

415 -- Content-Type non supporté

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

Piège courant avec curl -d

curl -d '{"...":""}' URL (sans -H) envoie Content-Type: application/x-www-form-urlencoded par défaut. Le Plug.Parsers traite alors le JSON comme une clé unique de formulaire, et le controller se plaint de « champs obligatoires absents ». Le plug RequireJsonContentType évite ce bruit en renvoyant 415 avec l'indication.

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

Les erreurs de credentials absents, invalides, API Key inactive/expirée ou IP hors whitelist viennent au format {"error": {status, message}} :

401 -- Credentials absents

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

401 -- Credentials invalides

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

401 -- API Key inactive

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

401 -- API Key expirée

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

403 -- Whitelist d'IP vide

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

403 -- IP non autorisé

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

403 -- Compte inactif

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

403 -- Permission insuffisante

Ce 403 vient d'un autre plug

Cette erreur est émise par le plug RequirePermission qui s'exécute après ApiKeyAuth (l'authentification de l'API Key, le check d'IP whitelist et le rate limiter sont déjà passés), déjà à l'intérieur du controller. C'est pour cela que le shape du JSON est différent des autres 401/403 de cette couche : il utilise {"error": "forbidden", "message": "..."} (chaîne au lieu d'objet).

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

Couche 2 -- Validation HMAC-SHA512 (plug HMAC)

Les erreurs émises par le plug HmacValidation viennent toujours au format {"worked": false, "detail": "..."} avec différents codes HTTP selon la cause :

401 -- Signature invalide ou header absent

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

400 -- Body absent ou JSON invalide

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 sans HMAC secret configuré

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

Couche 3 -- Erreurs métier (FallbackController)

Après l'authentification, les erreurs de validation, paramètres manquants, ressources introuvables et règles métier viennent au format {"errors": {atom: "msg"}} :

400 -- Requête invalide

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

404 -- Ressource introuvable

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

401 -- Non autorisé (règle métier)

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

422 -- Entité non traitable

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

Couche 4 -- Rate Limiting (plug RateLimiter ou Cloud Armor)

429 -- Rate Limit dépassé

Body du 429 venant du backend (plug RateLimiter) :

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

Headers inclus dans la response 429 :

HeaderValeur
Retry-After60 (secondes à attendre avant retry)

Comment réagir au 429

  • Backoff exponentiel : commencez à 60s (valeur du Retry-After), doublez à chaque retry suivant jusqu'à un plafond raisonnable (ex. : 5 min).
  • N'ignorez jamais l'en-tête : même si votre client a sa propre stratégie de retry, le Retry-After est la source de vérité canonique pour cet endpoint.
  • Si le 429 vient de Cloud Armor (couche au-dessus du backend), le body peut avoir un shape différent -- le status 429 avec Retry-After: 60 reste le pattern pour toute couche.

Couche 5 -- Idempotency (plug Idempotency)

400 -- Idempotency-Key trop longue

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

Permissions

Chaque API Key possède une liste de permissions qui déterminent quels endpoints sont accessibles. Si l'API Key n'a pas la permission nécessaire, la requête est rejetée avec 403 Forbidden.

Comment créer l'API Key

  1. Accédez au panneau d'administration sur core.owem.com.br
  2. Naviguez vers Sécurité → API Keys
  3. Cliquez sur Créer API Key
  4. Remplissez le nom, sélectionnez le compte et ajoutez les IPs à la whitelist
  5. Cochez les permissions nécessaires (voir tableau ci-dessous)
  6. Cliquez sur enregistrer. Le client_id et le client_secret seront affichés une seule fois -- copiez-les et stockez-les en sécurité

Pour éditer les permissions d'une API Key existante, cliquez sur l'icône de permissions dans la liste des API Keys.

Obligatoire pour envoyer PIX

Pour réaliser des opérations de PIX Cash-Out (envoi de PIX), l'API Key doit avoir la permission transfer:write. Sans cette permission, toutes les tentatives d'envoi retournent 403 Forbidden avec le message API key lacks permission: transfer:write.

Permissions minimales recommandées pour une opération complète :

  • Cash-In (recevoir) : pix:write + transfer:read
  • Cash-Out (envoyer) : transfer:write + transfer:read
  • Consultations : transfer:read + account:read + statement:read
  • Webhooks : account:write + account:read

Permissions disponibles

PermissionDescription
pix:writeGénérer QR Code (Cash-In)
pix:readLister les clés PIX
transfer:writeEnvoyer PIX (Cash-Out)
transfer:readConsulter transactions (par ID, E2E, Tag, External ID), reçu, lister transactions
payment:writeDemander remboursement (refund)
payment:readLister et consulter MED
account:writeCréer et supprimer webhooks
account:readConsulter solde, lister webhooks, valider CPF
statement:readConsulter relevé

Permissions par endpoint

EndpointMéthodePermission
/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

Réponse d'erreur -- 403 (Permission insuffisante)

Voir le format dans Couche 1 -- 403 Permission insuffisante.

Les permissions sont configurées à la création de l'API Key par le Merchant Portal ou par l'API d'administration.

Sécurité

  • N'exposez jamais le client_secret dans du code frontend ou des dépôts publics
  • Utilisez des variables d'environnement sur votre serveur
  • L'API Key peut expirer si le champ expires_at est configuré ; sinon, elle reste valide jusqu'à révocation manuelle dans le Merchant Portal
  • Configurez les IPs autorisés dans la whitelist -- une whitelist vide bloque la clé avec 403 IP whitelist required

Owem Pay Instituição de Pagamento — ISPB 37839059