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étierPipeline par méthode HTTP
GET /balanceutilise uniquementContent-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/DELETEutilisentContent-Type → KeyCase → API Key + Secret → IP Whitelist → Rate Limiter → RequirePermission-- sans HMAC ni idempotence. POSTutilise 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 :
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
| Composant | Description | Préfixe |
|---|---|---|
client_id | Identifiant public de l'API Key | cli_ |
client_secret | Clé 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 :
{
"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 :
{
"error": {
"status": 403,
"message": "Request IP not in API key whitelist"
}
}Formats acceptés
| Format | Exemple | Commentaire |
|---|---|---|
| IPv4 individuel | 203.0.113.45 | Un seul endpoint public |
| Notation CIDR (IPv4) | 203.0.113.0/24 | Sous-réseau /24 entier (256 IPs). Utilisez /32 pour un hôte unique |
| CIDR agrégé | 172.20.16.0/20 | Plage 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
| Header | Valeur | Obligatoire |
|---|---|---|
Authorization | ApiKey {client_id}:{client_secret} ou Basic {base64(client_id:client_secret)} | Oui -- toutes les requêtes |
Content-Type | application/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 |
hmac | Signature HMAC-SHA512 du body en hexadécimal lowercase | Oui -- uniquement POST sur /api/external/* |
Headers optionnels
| Header | Valeur | Effet |
|---|---|---|
Idempotency-Key | Clé 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-Case | camelCase | Convertit 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-For | IP(s) séparés par virgule | Respecté 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
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
| Protection | Description |
|---|---|
| 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 |
| HSTS | Navigateurs 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 :
| Header | Apparaît dans | Valeur |
|---|---|---|
x-ratelimit-remaining | Réponses 2xx (après passage par le limiter) | Entier : requêtes restantes dans la fenêtre actuelle de 60s, limité par IP |
Retry-After | Uniquement dans 429 Too Many Requests | 60 (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.
| Aspect | mTLS | HMAC-SHA512 |
|---|---|---|
| Valide | Canal TLS | Payload de la requête |
| Gestion | Certificats X.509 (émission, rotation, révocation, CRL/OCSP) | Génère la paire, met à jour, invalide |
| Risque opérationnel | Certificats expirés -- cause fréquente d'incidents | La clé est une simple chaîne |
| Intégrité du contenu | Non | Oui |
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 :
{"error": {status, message}}--Content-Type(415),ApiKeyAuth(401/403),Idempotency(400),RateLimiter(429).{"error": "forbidden", "message": "..."}-- uniquementRequirePermission(403).{"worked": false, "detail": "..."}-- uniquementHmacValidation(400, 401, 403).{"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é
{
"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
{
"error": {
"status": 401,
"message": "Missing API key credentials. Use Authorization: ApiKey <client_id>:<client_secret>"
}
}401 -- Credentials invalides
{
"error": {
"status": 401,
"message": "Invalid API key credentials"
}
}401 -- API Key inactive
{
"error": {
"status": 401,
"message": "API key is inactive"
}
}401 -- API Key expirée
{
"error": {
"status": 401,
"message": "API key has expired"
}
}403 -- Whitelist d'IP vide
{
"error": {
"status": 403,
"message": "IP whitelist required. Configure at least one allowed IP to use this API key."
}
}403 -- IP non autorisé
{
"error": {
"status": 403,
"message": "Request IP not in API key whitelist"
}
}403 -- Compte inactif
{
"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).
{
"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
{
"worked": false,
"detail": "Invalid HMAC signature"
}{
"worked": false,
"detail": "Missing HMAC header"
}400 -- Body absent ou JSON invalide
{
"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 sans HMAC secret configuré
{
"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
{
"errors": {
"bad_request": {
"amount": ["is required"]
}
}
}404 -- Ressource introuvable
{
"errors": {
"not_found": "Transaction not found"
}
}401 -- Non autorisé (règle métier)
{
"errors": {
"unauthorized": "invalid credentials"
}
}422 -- Entité non traitable
{
"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) :
{
"error": {
"status": 429,
"message": "Too many requests. Please try again later."
}
}Headers inclus dans la response 429 :
| Header | Valeur |
|---|---|
Retry-After | 60 (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-Afterest 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: 60reste le pattern pour toute couche.
Couche 5 -- Idempotency (plug Idempotency)
400 -- Idempotency-Key trop longue
{
"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
- Accédez au panneau d'administration sur core.owem.com.br
- Naviguez vers Sécurité → API Keys
- Cliquez sur Créer API Key
- Remplissez le nom, sélectionnez le compte et ajoutez les IPs à la whitelist
- Cochez les permissions nécessaires (voir tableau ci-dessous)
- Cliquez sur enregistrer. Le
client_idet leclient_secretseront 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
| Permission | Description |
|---|---|
pix:write | Générer QR Code (Cash-In) |
pix:read | Lister les clés PIX |
transfer:write | Envoyer PIX (Cash-Out) |
transfer:read | Consulter transactions (par ID, E2E, Tag, External ID), reçu, lister transactions |
payment:write | Demander remboursement (refund) |
payment:read | Lister et consulter MED |
account:write | Créer et supprimer webhooks |
account:read | Consulter solde, lister webhooks, valider CPF |
statement:read | Consulter relevé |
Permissions par endpoint
| Endpoint | Méthode | Permission |
|---|---|---|
/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 |
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_secretdans 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_atest 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