Skip to content

PIX Cash-Out par clé

Effectue un virement PIX en utilisant la clé PIX du destinataire.

Endpoint

POST /api/external/pix/cash-out

Headers

HeaderTypeObligatoireDescription
AuthorizationStringOuiApiKey {client_id}:{client_secret}
Content-TypeStringOuiapplication/json
hmacStringOuiSignature HMAC-SHA512 du body (hex)
Idempotency-KeyStringNonClé 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: true et renvoie le Idempotency-Key envoyé.
  • 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_id déterministe garantit encore l'idempotence au niveau BACEN/SPI, mais peut générer un rejet avec failure_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

ChampTypeObligatoireDescription
amountIntegerOuiValeur en centavos. R$ 30,00 = 3000
pix_keyStringOuiClé PIX du destinataire
pix_key_typeStringNonType de la clé : cpf, cnpj, email, phone, evp. Si omis, détecté automatiquement à partir de la clé.
descriptionStringNonDescription du virement (max 140 caractères)
external_idStringNonIdentifiant 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_ispbStringNonISPB 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_idStringNonEnd-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é.
purposeStringNonFinalité 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

bash
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

json
{
  "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. status peut ê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é).
ChampTypeDescription
workedBooleantrue indique que la requête a été acceptée
finalBooleantrue quand la transaction a atteint un état terminal (liquidée ou rejetée). false quand encore en traitement
transaction_idStringIdentifiant unique de la transaction
end_to_end_idStringIdentifiant End-to-End dans le SPI/BACEN (format E{ISPB}...)
external_idStringVotre identifiant, retourné tel qu'envoyé. null s'il n'a pas été renseigné
amountIntegerValeur du virement en unités de base (÷ 10 000 pour reais). 300000 = R$ 30,00
fee_amountIntegerFrais prélevés en unités de base (÷ 10 000 pour reais)
net_amountIntegerValeur 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=350net_amount=300350 (R$ 30,035 débités de votre compte, R$ 30,00 crédités au destinataire)
statusStringUn 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
detailStringMessage 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)

HTTPFormatChamp avec codeSignification
400Berrors.bad_request: "invalid or missing amount"amount absent, zéro, négatif ou non-entier
422Aerrors[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
400Berrors.bad_request: "invalid pix_key"La clé n'a pas passé les règles de format (checksum CPF invalide, email mal formé, etc.)
422Aerrors[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: []}]}
422Aerrors[0].code: "insufficient_balance"Solde disponible inférieur à amount + fee_amount. Tient compte du hold actif (gotcha min(TB, PG))
422Aerrors[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)SignificationAction recommandée
dict_key_not_foundClé PIX non localisée dans DICT/BACENVérifiez avec le payeur ; la clé peut avoir été supprimée ou jamais enregistrée
dict_key_blockedClé bloquée, par exemple pour suspicion de fraudeContact avec le titulaire de la clé
dict_lookup_failedÉchec de consultation du DICTRetry dans 5-30s
dict_rate_limitedLimite temporaire de consultation DICTAttendez le retraitement automatique ou appliquez un backoff avant une nouvelle demande
dict_bucket_exhaustedLimite DICT temporairement indisponibleAttendez le retraitement automatique ; évitez les rafales
provider_rejectedLe fournisseur a rejeté avec une erreur 4xx générique non classéeVoir errors[0].params pour contexte ; réouvrez un cas avec le support Minha Konta
provider_schema_errorPayload d'intégration incompatibleSignalez immédiatement et ne relancez pas sans orientation Minha Konta
provider_unknown_errorStatus hors de 400..499 qui est entré dans ce cheminLog 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 :

Originereason_code (webhook pix.payout.queued)Cause
Quota par clientDICT_CLIENT_RATE_LIMITEDVolume de consultations DICT du client au-dessus de la politique de protection Minha Konta.
Limite DICT du fournisseurDICT_BUCKET_EXHAUSTEDCapacité opérationnelle de consultation DICT temporairement indisponible.

Réponse HTTP quand mise en file :

json
{
  "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.failed avec reason_code: "DICT_QUEUE_TIMEOUT".
  • Webhook immédiat : à l'entrée dans la file, Minha Konta déclenche pix.payout.queued avec reason_code (DICT_CLIENT_RATE_LIMITED ou DICT_BUCKET_EXHAUSTED) et reason_description. L'événement suivant sera pix.payout.confirmed ou pix.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)

HTTPdetailSignification
401Invalid HMAC signatureLa signature HMAC ne correspond pas. Vérifiez l'ordre alphabétique des champs dans le body sérialisé - voir HMAC-SHA512
401Invalid API Keyclient_id:client_secret incorrect
403permission 'transfer:write' requiredAPI Key sans permission pour PIX
403IP not whitelistedIP 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 :

NamespaceConventionOrigineExemples
BACEN SPIUPPERCASERejets asynchrones via PACS.002 RJCT, visibles en consultation de status et webhook pix.payout.rejectedAC03, AB03, ED05, DUPL, AM02, FF08, BE01
Fournisseur / DICTlowercase snake_caseRejets synchrones avant confirmation BACENdict_key_not_found, dict_rate_limited, same_institution_transfer, provider_schema_error
Retraitement automatiqueUPPERCASE (préfixe DICT_)Webhook pix.payout.queued / pix.payout.failed quand il y a retraitement automatiqueDICT_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éclenche pix.payout.queued immédiatement avec reason_code + reason_description.
  • Les rejets asynchrones (PACS.002 RJCT après acceptation 202) déclenchent pix.payout.rejected avec reason_code BACEN (AC03, AB03, ED05, DUPL etc.) et reason_description en anglais.
  • Les voids d'orphelines (>30min sans PACS.002) déclenchent pix.payout.failed avec reason_code: "orphan_force_voided".
  • L'expiration de la file de retry (120min) déclenche pix.payout.failed avec reason_code: "DICT_QUEUE_TIMEOUT".

Types de clé PIX

TypeFormatExemple
cpf11 chiffres (sans ponctuation)12345678901
cnpj14 chiffres (sans ponctuation)12345678000199
emailAdresse emailnom@entreprise.com.br
phoneDDD + numéro (11 chiffres)11999998888
evpUUID v4a1b2c3d4-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 :

  1. Utilisez l'endpoint Validation CPF (POST /api/external/cpf/validate) pour vérifier si les 11 chiffres forment un CPF valide
  2. Si valid: true → envoyez pix_key_type: "cpf" dans le cash-out
  3. Si valid: false → c'est un téléphone, envoyez pix_key_type: "phone" (l'API ajoute automatiquement le préfixe +55)
javascript
// 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 :

Ou recevez la confirmation automatiquement via Webhook.

Minha Konta Instituição de Pagamento - ISPB 39929224