PIX Cash-Out par clé
Effectue un virement PIX en utilisant la clé PIX du destinataire.
Endpoint
POST /api/external/pix/cash-outHeaders
| Header | Type | Obligatoire | Description |
|---|---|---|---|
Authorization | String | Oui | ApiKey {client_id}:{client_secret} |
Content-Type | String | Oui | application/json |
hmac | String | Oui | Signature HMAC-SHA512 du body (hex) |
Idempotency-Key | String | Non | Clé unique pour éviter le traitement en double (max 256 caractères) |
Authentification
Voir Authentification. La signature HMAC doit être générée comme décrit dans HMAC-SHA512.
Idempotency-Key - comportement de replay
Quand présent, l'API stocke la réponse (seulement en 2xx) pendant 24 heures et retourne la réponse en cache pour tout nouveau POST avec la même combinaison (méthode, path, Idempotency-Key). Le cache est limité par endpoint (même clé sur /cash-out et /refund n'entrent pas en collision).
- Dans la réponse replay, l'API inclut l'en-tête
X-Idempotent-Replay: trueet renvoie leIdempotency-Keyenvoyé. - Les clés de plus de 256 caractères retournent
400 Bad Request. - La clé est optionnelle. Si vous ne l'envoyez pas, l'API traite chaque POST comme une nouvelle transaction (le
end_to_end_iddéterministe garantit encore l'idempotence au niveau BACEN/SPI, mais peut générer un rejet avecfailure_reason: "DUPL"si la première tentative a déjà été liquidée).
Permission obligatoire
L'API Key doit avoir la permission transfer:write pour envoyer du PIX. Sans elle, la requête retourne 403 Forbidden. Voir comment configurer les permissions.
Request Body
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
amount | Integer | Oui | Valeur en centavos. R$ 30,00 = 3000 |
pix_key | String | Oui | Clé PIX du destinataire |
pix_key_type | String | Non | Type de la clé : cpf, cnpj, email, phone, evp. Si omis, détecté automatiquement à partir de la clé. |
description | String | Non | Description du virement (max 140 caractères) |
external_id | String | Non | Identifiant de votre système pour le suivi. Max 128 caractères après trim. Uniquement caractères a-zA-Z0-9._:-. Retourné dans les réponses et webhooks. Les valeurs invalides (caractères non autorisés, > 128 caractères, vide après trim) sont silencieusement écartées - la transaction continue avec external_id: null. Validez de votre côté avant d'envoyer si vous avez besoin de garantir la persistance. |
recipient_ispb | String | Non | ISPB de l'institution du destinataire pour le routage manuel (8 chiffres). Quand fourni, dirige le paiement vers le PSP spécifié. N'envoyez pas l'ISPB d'Minha Konta (04838403) - les requêtes intra-institutionnelles retournent l'erreur same_institution (PIX interne non supporté). |
end_to_end_id | String | Non | End-to-End ID au format BACEN (E{ISPB}{YYYYMMDDHHmm}{entropy}). Recommandé à omettre - le backend génère un E2E déterministe à chaque tentative (mêmes amount + pix_key + merchant_id → même E2E). Ce déterminisme garantit l'idempotence dans le SPI/BACEN même sans Idempotency-Key. N'envoyez manuellement que dans des scénarios de reprocessement coordonné. |
purpose | String | Non | Finalité du virement (champ libre pour usage interne et compliance). |
Valeurs monétaires
Les valeurs d'entrée sont en centavos (R$ 1,00 = 100). Les valeurs de réponse sont en unités de base (R$ 1,00 = 10000). Pour convertir la réponse en reais, divisez par 10 000. N'utilisez jamais de virgule flottante.
Exemple
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" \
-d '{
"amount": 3000,
"pix_key": "12345678901",
"pix_key_type": "cpf",
"description": "Paiement fournisseur",
"external_id": "order-9876"
}'Réponse de succès -- 200 / 202
{
"worked": true,
"final": false,
"transaction_id": "PIXOUT20260309a1b2c3d4e5f6",
"end_to_end_id": "E04838403202603091530abcdef01",
"external_id": "order-9876",
"amount": 300000,
"fee_amount": 350,
"net_amount": 300350,
"status": "accepted",
"detail": "PIX enviado para processamento"
}HTTP 200 vs 202
- HTTP 200 : Transaction déjà liquidée (
final: true,status: "settled"). - HTTP 202 : Transaction acceptée pour traitement (
final: false). Suivez le status via polling ou webhook.statuspeut être"accepted"(flux normal),"queued"(rate-limit appliqué - retry automatique toutes les 3s pendant jusqu'à 120 min) ou"pending_approval"(en attente d'approbation via workflow de double autorisation, quand activé).
| Champ | Type | Description |
|---|---|---|
worked | Boolean | true indique que la requête a été acceptée |
final | Boolean | true quand la transaction a atteint un état terminal (liquidée ou rejetée). false quand encore en traitement |
transaction_id | String | Identifiant unique de la transaction |
end_to_end_id | String | Identifiant End-to-End dans le SPI/BACEN (format E{ISPB}...) |
external_id | String | Votre identifiant, retourné tel qu'envoyé. null s'il n'a pas été renseigné |
amount | Integer | Valeur du virement en unités de base (÷ 10 000 pour reais). 300000 = R$ 30,00 |
fee_amount | Integer | Frais prélevés en unités de base (÷ 10 000 pour reais) |
net_amount | Integer | Valeur brute débitée sur le compte payeur, en unités de base. Calculée comme amount + fee_amount (le débit total inclut les frais). Ce n'est pas la valeur que le destinataire reçoit - il ne reçoit que amount. Exemple : amount=300000 + fee_amount=350 → net_amount=300350 (R$ 30,035 débités de votre compte, R$ 30,00 crédités au destinataire) |
status | String | Un de : accepted (HTTP 202, traitement synchrone normal), settled (HTTP 200, liquidation immédiate - rare en fast-track), queued (HTTP 202, en attente de retraitement automatique pour limite DICT), pending_approval (HTTP 202, en attente d'approbation). Voir les status terminaux dans Consulter Cash-Out par ID -- Valeurs du champ status |
detail | String | Message descriptif |
Sens de net_amount en cash-out diffère de cash-in
En cash-out, net_amount = amount + fee_amount (débit brut sur le compte payeur). En cash-in (QR Code payé), le backend traite net_amount comme valeur nette créditée après déduction des frais. Cette asymétrie est historique - traitez net_amount toujours comme « mouvement effectif sur votre compte dans cette direction ». Pour la conciliation comptable, préférez opérer avec les champs amount et fee_amount séparément.
Codes de rejet
L'API peut rejeter un cash-out par validation d'entrée (avant envoi au SPI), par erreur d'intégration avec le provider / DICT (pendant l'envoi synchrone), ou par rate-limit avec retry automatique en file d'attente. Les rejets BACEN via PACS.002 RJCT arrivent de façon asynchrone et n'apparaissent que via consultation de status ou webhook pix.payout.rejected.
Format de la réponse d'erreur
Les rejets synchrones du cash-out retournent en deux formats distincts - choisissez le parser correct selon l'origine de l'erreur :
Format A -- Validation ou intégration : HTTP 400 ou 422, body {"status": "failed", "errors": [{"code": "<code>", "message": "<text>", "params": {...}}]}. Codes courants : same_institution_transfer, insufficient_balance, dict_key_not_found, dict_rate_limited, dict_bucket_exhausted.
Format B -- Erreur de validation du payload : HTTP 400, body {"errors": {"bad_request": "message"}}. Exemples : invalid or missing amount, ambiguous key.
Routez via data.status === "failed" (Format A) vs data.errors.bad_request (Format B).
Erreurs de validation (HTTP 400 / 422)
| HTTP | Format | Champ avec code | Signification |
|---|---|---|---|
| 400 | B | errors.bad_request: "invalid or missing amount" | amount absent, zéro, négatif ou non-entier |
| 422 | A | errors[0].code: "pix_key_ambiguous" | Clé de 11 chiffres sans pix_key_type - peut être CPF ou téléphone. Résolvez via Validation CPF et fournissez pix_key_type explicitement |
| 400 | B | errors.bad_request: "invalid pix_key" | La clé n'a pas passé les règles de format (checksum CPF invalide, email mal formé, etc.) |
| 422 | A | errors[0].code: "same_institution_transfer" | recipient_ispb est l'ISPB d'Minha Konta (04838403). PIX intra-institutionnel non supporté - utilisez TEF interne. Note : cette validation retourne HTTP 422 (pas 400) avec la structure {status: "failed", errors: [{code: "same_institution_transfer", params: []}]} |
| 422 | A | errors[0].code: "insufficient_balance" | Solde disponible inférieur à amount + fee_amount. Tient compte du hold actif (gotcha min(TB, PG)) |
| 422 | A | errors[0].code: "ceiling_exceeded" | amount exceeds the configured per-transaction limit. errors[0].message = "PIX out limit reached: amount R$ X exceeds the R$ Y per-transaction limit"; errors[0].params.ceiling is the limit in subcents |
Changement de shape pour same_institution
Les versions précédentes de ces docs affirmaient HTTP 400 avec detail: "same_institution". Le comportement réel est HTTP 422 avec le shape du Format A (errors comme tableau de {code, params}). Les clients qui font if (status === 400 && body.detail === "same_institution") ne se déclenchent pas en pratique - utilisez if (status === 422 && body.errors?.[0]?.code === "same_institution_transfer").
Erreurs d'intégration avec le provider / DICT (HTTP 400)
Quand le fournisseur retourne une erreur synchrone avant la confirmation BACEN, l'API retourne le Format A avec HTTP 400 :
Code (errors[0].code) | Signification | Action recommandée |
|---|---|---|
dict_key_not_found | Clé PIX non localisée dans DICT/BACEN | Vérifiez avec le payeur ; la clé peut avoir été supprimée ou jamais enregistrée |
dict_key_blocked | Clé bloquée, par exemple pour suspicion de fraude | Contact avec le titulaire de la clé |
dict_lookup_failed | Échec de consultation du DICT | Retry dans 5-30s |
dict_rate_limited | Limite temporaire de consultation DICT | Attendez le retraitement automatique ou appliquez un backoff avant une nouvelle demande |
dict_bucket_exhausted | Limite DICT temporairement indisponible | Attendez le retraitement automatique ; évitez les rafales |
provider_rejected | Le fournisseur a rejeté avec une erreur 4xx générique non classée | Voir errors[0].params pour contexte ; réouvrez un cas avec le support Minha Konta |
provider_schema_error | Payload d'intégration incompatible | Signalez immédiatement et ne relancez pas sans orientation Minha Konta |
provider_unknown_error | Status hors de 400..499 qui est entré dans ce chemin | Log complet disponible auprès du support |
HTTP est 400 (pas 429)
Les versions précédentes de ces docs montraient HTTP 429 pour dict_rate_limited et dict_bucket_exhausted dans le chemin synchrone. Le contrat actuel est : erreur synchrone d'intégration retourne HTTP 400 ; limite DICT avec retraitement automatique retourne HTTP 202 queued.
Rate-limit avec retry automatique (HTTP 202 queued)
Quand Minha Konta détecte une limite de consultation DICT avant d'envoyer la transaction au fournisseur, le PIX OUT reste en traitement et entre en retry automatique. Ce chemin évite une nouvelle consultation DICT tant que la capacité n'est pas disponible.
Deux scénarios déclenchent la file :
| Origine | reason_code (webhook pix.payout.queued) | Cause |
|---|---|---|
| Quota par client | DICT_CLIENT_RATE_LIMITED | Volume de consultations DICT du client au-dessus de la politique de protection Minha Konta. |
| Limite DICT du fournisseur | DICT_BUCKET_EXHAUSTED | Capacité opérationnelle de consultation DICT temporairement indisponible. |
Réponse HTTP quand mise en file :
{
"status": "queued",
"type": "pix",
"transaction_id": "PIXOUT20260309a1b2c3d4e5f6",
"end_to_end_id": "E04838403202603091530abcdef01",
"outbound_request_id": "0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D",
"amount": 300000,
"message": "Payment rate-limited, enqueued for automatic retry (TTL 120 min)",
"estimated_retry_seconds": 3,
"queue_ttl_seconds": 7200
}Mécanique du retry :
- Minha Konta réessaie automatiquement tant que la fenêtre de retraitement est active.
- TTL total : 7200 secondes (120 minutes). Après expiration, le client reçoit
pix.payout.failedavecreason_code: "DICT_QUEUE_TIMEOUT". - Webhook immédiat : à l'entrée dans la file, Minha Konta déclenche
pix.payout.queuedavecreason_code(DICT_CLIENT_RATE_LIMITEDouDICT_BUCKET_EXHAUSTED) etreason_description. L'événement suivant serapix.payout.confirmedoupix.payout.failed.
Statut non terminal
Traitez toujours queued et accepted comme des états non terminaux. Suivez le résultat par webhook ou par consultation de la transaction.
Permission et authentification (HTTP 401 / 403)
| HTTP | detail | Signification |
|---|---|---|
| 401 | Invalid HMAC signature | La signature HMAC ne correspond pas. Vérifiez l'ordre alphabétique des champs dans le body sérialisé - voir HMAC-SHA512 |
| 401 | Invalid API Key | client_id:client_secret incorrect |
| 403 | permission 'transfer:write' required | API Key sans permission pour PIX |
| 403 | IP not whitelisted | IP d'origine hors de l'allowlist de l'API Key |
Vocabulaire de codes - UPPERCASE × lowercase
Les codes structurés du cash-out viennent de vocabulaires distincts :
| Namespace | Convention | Origine | Exemples |
|---|---|---|---|
| BACEN SPI | UPPERCASE | Rejets asynchrones via PACS.002 RJCT, visibles en consultation de status et webhook pix.payout.rejected | AC03, AB03, ED05, DUPL, AM02, FF08, BE01 |
| Fournisseur / DICT | lowercase snake_case | Rejets synchrones avant confirmation BACEN | dict_key_not_found, dict_rate_limited, same_institution_transfer, provider_schema_error |
| Retraitement automatique | UPPERCASE (préfixe DICT_) | Webhook pix.payout.queued / pix.payout.failed quand il y a retraitement automatique | DICT_CLIENT_RATE_LIMITED, DICT_BUCKET_EXHAUSTED, DICT_QUEUE_TIMEOUT |
En faisant un switch programmatique d'erreurs, normalisez en uppercase ou lowercase de votre côté pour éviter les branches dupliquées. N'attendez pas AM02 dans les réponses synchrones - les codes BACEN n'apparaissent que dans les consultations GET post-acceptation.
Webhooks correspondants
- Les rejets synchrones (Format A/B ci-dessus) ne déclenchent pas de webhook - le client a déjà reçu l'erreur dans la réponse HTTP.
- La mise en file par rate-limit (HTTP 202
queued) déclenchepix.payout.queuedimmédiatement avecreason_code+reason_description. - Les rejets asynchrones (PACS.002 RJCT après acceptation 202) déclenchent
pix.payout.rejectedavecreason_codeBACEN (AC03, AB03, ED05, DUPL etc.) etreason_descriptionen anglais. - Les voids d'orphelines (>30min sans PACS.002) déclenchent
pix.payout.failedavecreason_code: "orphan_force_voided". - L'expiration de la file de retry (120min) déclenche
pix.payout.failedavecreason_code: "DICT_QUEUE_TIMEOUT".
Types de clé PIX
| Type | Format | Exemple |
|---|---|---|
cpf | 11 chiffres (sans ponctuation) | 12345678901 |
cnpj | 14 chiffres (sans ponctuation) | 12345678000199 |
email | Adresse email | nom@entreprise.com.br |
phone | DDD + numéro (11 chiffres) | 11999998888 |
evp | UUID v4 | a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d |
Clés de 11 chiffres - Ambiguïté CPF vs Téléphone
Les clés de exactement 11 chiffres peuvent être soit un CPF soit un téléphone mobile (DDD + 9xxxx-xxxx). Quand la clé est ambiguë, l'API rejette avec HTTP 400 et failure_reason: "ambiguous key".
Solution recommandée :
- Utilisez l'endpoint Validation CPF (
POST /api/external/cpf/validate) pour vérifier si les 11 chiffres forment un CPF valide - Si
valid: true→ envoyezpix_key_type: "cpf"dans le cash-out - Si
valid: false→ c'est un téléphone, envoyezpix_key_type: "phone"(l'API ajoute automatiquement le préfixe+55)
// Exemple de flux automatisé
async function resolveKeyType(key) {
if (key.length !== 11 || /\D/.test(key)) return null; // sans ambiguïté
const { data } = await api.post('/api/external/cpf/validate', { cpf: key });
return data.valid ? 'cpf' : 'phone';
}Astuce : envoyez les téléphones sous forme de 11 chiffres purs (DDD + numéro). L'API ajoute le préfixe +55 automatiquement. Évitez d'envoyer le +55 manuellement - cela peut causer un échec de la validation HMAC dans certains clients.
Étapes suivantes
Après avoir créé le virement, suivez le status via :
- Consulter par ID
- Consulter par E2E ID
- Consulter par Tag
- Consulter par External ID --
GET /api/external/transactions/ref/{external_id}
Ou recevez la confirmation automatiquement via Webhook.
