Authentification
L'API externe Minha Konta utilise un modele de securite avec trois couches principales d'authentification -- API Key + Secret, signature HMAC-SHA512 par requete (uniquement en POST) et whitelist IP obligatoire -- completees par la validation du contenu, le rate limiting et l'idempotence.
Vue generale des validations
La requete HTTP passe par les validations ci-dessous, dans cet ordre. Tout rejet renvoie une erreur avant l'execution de la logique metier :
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 Limiting ─── 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. Permission API Key ───── API Key sans la permission exigée par la route ? → 403
│
└─ Requête acceptée → Logique metierValidations par methode HTTP
GET /balanceutilise uniquementContent-Type → KeyCase → API Key + Secret → IP Whitelist → Permission API Key-- 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 → Permission API Key-- sans HMAC ni idempotence. POSTutilise le flux complet de validations ci-dessus (toutes les validations).
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 la même couche d'authentification 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.minhakonta.com/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é API.
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
L'API normalise les adresses ::ffff:A.B.C.D (IPv4 mappé en IPv6, utilisé par des load balancers de confiance) 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. 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.minhakonta.com/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.minhakonta.com/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 (API) | 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 (WAF de bord) | Regles par API Key/IP dans le WAF de bord lorsqu'elles sont configurees. Opere avant le API, donc un 429 de cette couche n'incremente pas le compteur du API |
| Rate Limiting (auth) | 5 req/min sur les endpoints d'authentification admin/merchant (ne s'applique pas à /api/external/*) |
| Protection DICT sur Cash-Out | Spécifique au POST /pix/cash-out quand il résout une clé PIX via DICT. Quand elle s'applique, retourne HTTP 202 avec status: "queued" et déclenche le webhook pix.payout.queued pendant que la transaction attend un retraitement automatique. Voir PIX Cash Out (par clé) |
| AWS WAF | Firewall d'application protegeant l'API avec des regles 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 passe par le rate limiter, la réponse 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 rate limiting utilise des fenetres fixes de 60 secondes, pas de sliding window. En pratique, la limite stable de 1 500 req/s par IP est le comportement que le client doit prendre en 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 la couche de validation qui rejette la requete, le format du body JSON est different. Inspectez toujours le format avant de parser l erreur cote client :
{"error": {status, message}}--Content-Type(415),API Key + IP Whitelist(401/403),Idempotency(400), rate limiting (429).{"error": "forbidden", "message": "..."}-- uniquementPermission API Key(403).{"worked": false, "detail": "..."}-- uniquementHMAC validation(400, 401, 403).{"errors": {atom: "msg"}}-- erreurs métier (toute erreur métier 4xx/5xx).
Couche 0 -- Content-Type
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 defaut. L API peut l interpreter comme form-urlencoded et non comme JSON. Ajoutez -H 'Content-Type: application/json' pour obtenir la validation attendue.
Couche 1 -- API Key + IP Whitelist
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 de la validation des permissions
Cette erreur apparait quand l API Key est valide, l IP est autorisee, mais la credential ne possede pas la permission exigee par l endpoint. C est pourquoi le JSON differe des erreurs de credential/IP : il utilise {"error": "forbidden", "message": "..."}.
{
"error": "forbidden",
"message": "API key lacks permission: transfer:write"
}Couche 2 -- Validation HMAC-SHA512
Les erreurs emises par la validation HMAC utilisent toujours le format {"worked": false, "detail": "..."} avec des codes HTTP differents 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
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 (API ou WAF de bord)
429 -- Rate Limit dépassé
Body du 429 emis par la couche de rate limiting :
{
"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 du WAF de bord (couche au-dessus du API), le body peut avoir un shape différent -- le status 429 avec
Retry-After: 60reste le pattern pour toute couche.
Couche 5 -- 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 obtenir l'API Key
- Utilisez la base de production api.minhakonta.com
- Demandez l'emission des identifiants pour le compte cible
- Indiquez les IPs qui doivent etre ajoutees a la whitelist
- Indiquez les permissions necessaires (voir tableau ci-dessous)
- Apres emission, stockez le
client_idet leclient_secreten securite
Pour editer les permissions d'une API Key existante, demandez la mise a jour des permissions de l'identifiant.
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) et envoyer une defense MED |
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 |
/med/:id/defense | 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.
Utilisez payment:read pour lister et consulter les MEDs. Utilisez payment:write pour envoyer une defense via POST /api/external/med/:id/defense. L'endpoint de defense recoit du JSON signe par HMAC; les uploads binaires restent disponibles dans les portails Minha Konta.
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
