Skip to content

PIX Cash-Out by Key

Performs a PIX transfer using the recipient's PIX key.

Endpoint

POST /api/external/pix/cash-out

Headers

HeaderTypeRequiredDescription
AuthorizationStringYesApiKey {client_id}:{client_secret}
Content-TypeStringYesapplication/json
hmacStringYesHMAC-SHA512 signature of the body (hex)
Idempotency-KeyStringNoUnique key to prevent duplicate processing (max 256 chars)

Authentication

See Authentication. The HMAC signature must be generated as described in HMAC-SHA512.

Idempotency-Key - replay behavior

When present, the API stores the response (only on 2xx) for 24 hours and returns the cached response for any new POST with the same (method, path, Idempotency-Key) combination. The cache is scoped by endpoint (the same key on /cash-out and /refund does not collide).

  • On the replay response, the API includes the header X-Idempotent-Replay: true and echoes the Idempotency-Key sent.
  • Keys longer than 256 characters return 400 Bad Request.
  • The key is optional. If you don't send it, the API processes each POST as a new transaction (the deterministic end_to_end_id still guarantees idempotency at the BACEN/SPI layer, but may generate rejection with failure_reason: "DUPL" if the first attempt has already been settled).

Required permission

The API Key must have the transfer:write permission to send PIX. Without it, the request returns 403 Forbidden. See how to configure permissions.

Request Body

FieldTypeRequiredDescription
amountIntegerYesAmount in centavos. R$ 30.00 = 3000
pix_keyStringYesRecipient's PIX key
pix_key_typeStringNoKey type: cpf, cnpj, email, phone, evp. If omitted, auto-detected from the key.
descriptionStringNoTransfer description (max 140 characters)
external_idStringNoYour system identifier for tracking. Max 128 chars after trim. Only a-zA-Z0-9._:- characters. Returned in responses and webhooks. Invalid values (disallowed chars, > 128 chars, empty after trim) are silently discarded - the transaction proceeds with external_id: null. Validate on your side before sending if you need to guarantee persistence.
recipient_ispbStringNoISPB of the recipient institution for manual routing (8 digits). When provided, directs the payment to the specified PSP. Do not send Minha Konta's ISPB (04838403) - intra-institutional requests return same_institution error (internal PIX not supported).
end_to_end_idStringNoEnd-to-End ID in BACEN format (E{ISPB}{YYYYMMDDHHmm}{entropy}). Recommended to omit - the backend generates a deterministic E2E on each attempt (same amount + pix_key + merchant_id → same E2E). This determinism guarantees idempotency at SPI/BACEN even without Idempotency-Key. Only send manually in coordinated reprocessing scenarios.
purposeStringNoTransfer purpose (free-form field for internal use and compliance).

Monetary values

Request values are in centavos (R$ 1.00 = 100). Response values are in base units (R$ 1.00 = 10000). To convert the response to BRL, divide by 10,000. Never use floating point.

Example

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": "Pagamento fornecedor",
    "external_id": "order-9876"
  }'

Success Response -- 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 already settled (final: true, status: "settled").
  • HTTP 202: Transaction accepted for processing (final: false). Track the status via polling or webhook. status can be "accepted" (normal flow), "queued" (rate-limit applied - automatic retry every 3s for up to 120min) or "pending_approval" (awaiting approval via dual-control workflow, when enabled).
FieldTypeDescription
workedBooleantrue indicates the request was accepted
finalBooleantrue when the transaction reached a terminal state (settled or rejected). false when still processing
transaction_idStringUnique transaction identifier
end_to_end_idStringEnd-to-End identifier in SPI/BACEN (format E{ISPB}...)
external_idStringYour identifier, returned as sent. null if not provided
amountIntegerTransfer amount in base units (÷ 10,000 for BRL). 300000 = R$ 30.00
fee_amountIntegerFee charged in base units (÷ 10,000 for BRL)
net_amountIntegerGross amount debited from the paying account, in base units. Computed as amount + fee_amount (total debit includes the fee). Not what the recipient receives - they receive only amount. Example: amount=300000 + fee_amount=350net_amount=300350 (R$ 30.035 debited from your account, R$ 30.00 credited to the recipient)
statusStringOne of: accepted (HTTP 202, normal synchronous processing), settled (HTTP 200, immediate settlement - rare in fast-track), queued (HTTP 202, awaiting automatic reprocessing due to a DICT limit), pending_approval (HTTP 202, awaiting approval). See terminal statuses in Query Cash-Out by ID -- Status field values
detailStringDescriptive message

Meaning of net_amount in cash-out differs from cash-in

In cash-out, net_amount = amount + fee_amount (gross debit on the paying account). In cash-in (paid QR Code), the backend treats net_amount as the net value credited after the fee is deducted. This asymmetry is historical - always treat net_amount as "actual movement in your account in that direction". For accounting reconciliation, prefer working with the amount and fee_amount fields separately.

Rejection Codes

The API may reject a cash-out by input validation (before sending to the SPI), by integration error with provider / DICT (during synchronous sending), or by rate-limit with automatic retry in queue. BACEN rejections via PACS.002 RJCT arrive asynchronously and appear only via status query or the pix.payout.rejected webhook.

Error response format

Synchronous cash-out rejections return in two distinct formats - pick the correct parser based on the error origin:

Format A -- Validation or integration: HTTP 400 or 422, body {"status": "failed", "errors": [{"code": "<code>", "message": "<text>", "params": {...}}]}. Common codes: same_institution_transfer, insufficient_balance, dict_key_not_found, dict_rate_limited, dict_bucket_exhausted.

Format B -- Payload validation error: HTTP 400, body {"errors": {"bad_request": "message"}}. Examples: invalid or missing amount, ambiguous key.

Route via data.status === "failed" (Format A) vs data.errors.bad_request (Format B).

Validation errors (HTTP 400 / 422)

HTTPFormatField with codeMeaning
400Berrors.bad_request: "invalid or missing amount"amount missing, zero, negative, or non-integer
422Aerrors[0].code: "pix_key_ambiguous"11-digit key without pix_key_type - could be CPF or phone. Resolve via CPF Validation and pass pix_key_type explicitly
400Berrors.bad_request: "invalid pix_key"Key failed format rules (invalid CPF checksum, malformed email, etc.)
422Aerrors[0].code: "same_institution_transfer"recipient_ispb is Minha Konta's own ISPB (04838403). Intra-institutional PIX is not supported - use internal TEF. Note: this validation returns HTTP 422 (not 400) with the structure {status: "failed", errors: [{code: "same_institution_transfer", params: []}]}
422Aerrors[0].code: "insufficient_balance"Available balance less than amount + fee_amount. Considers active holds (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

Shape change for same_institution

Earlier versions of these docs stated HTTP 400 with detail: "same_institution". The actual behavior is HTTP 422 with the Format A shape (errors as array of {code, params}). Clients doing if (status === 400 && body.detail === "same_institution") do not match in practice - use if (status === 422 && body.errors?.[0]?.code === "same_institution_transfer").

Integration errors with provider / DICT (HTTP 400)

When the provider returns a synchronous error before BACEN confirmation, the API returns Format A with HTTP 400:

Code (errors[0].code)MeaningRecommended action
dict_key_not_foundPIX key not located in DICT/BACENCheck with the payer; the key may have been removed or never registered
dict_key_blockedKey blocked, for example due to fraud suspicionContact the key owner
dict_lookup_failedFailure querying DICTRetry in 5-30s
dict_rate_limitedTemporary DICT lookup limitWait for automatic reprocessing or apply backoff before a new request
dict_bucket_exhaustedDICT limit temporarily unavailableWait for automatic reprocessing; avoid bursts
provider_rejectedProvider rejected with an unclassified generic errorCheck errors[0].params for context and reopen the case with Minha Konta support
provider_schema_errorIntegration payload incompatibleReport immediately and do not retry without Minha Konta guidance
provider_unknown_errorStatus outside 400..499 that entered this pathFull log available via support

HTTP is 400 (not 429)

Earlier versions of these docs showed HTTP 429 for dict_rate_limited and dict_bucket_exhausted in the synchronous path. The current contract is: synchronous integration errors return HTTP 400; DICT limits with automatic reprocessing return HTTP 202 queued.

Rate-limit with automatic retry (HTTP 202 queued)

When Minha Konta detects a DICT lookup limit before sending the transaction to the provider, the PIX OUT remains in processing and enters automatic retry. This path avoids another DICT call while capacity is unavailable.

Two scenarios trigger the queue:

Originreason_code (webhook pix.payout.queued)Cause
Per-client quotaDICT_CLIENT_RATE_LIMITEDClient DICT lookup volume exceeded Minha Konta protection policy.
Provider DICT limitDICT_BUCKET_EXHAUSTEDOperational DICT lookup capacity is temporarily unavailable.

HTTP response when queued:

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
}

Retry mechanics:

  • Minha Konta retries automatically while the retry window is active.
  • Total TTL: 7200 seconds (120 minutes). After expiration, the client receives pix.payout.failed with reason_code: "DICT_QUEUE_TIMEOUT".
  • Immediate webhook: when entering the queue, Minha Konta dispatches pix.payout.queued with reason_code (DICT_CLIENT_RATE_LIMITED or DICT_BUCKET_EXHAUSTED) and reason_description. The next event will be pix.payout.confirmed or pix.payout.failed.

Non-terminal status

Always treat both queued and accepted as non-terminal states. Track the final result by webhook or by querying the transaction.

Permission and authentication (HTTP 401 / 403)

HTTPdetailMeaning
401Invalid HMAC signatureHMAC signature does not match. Check the alphabetical order of fields in the serialized body - see HMAC-SHA512
401Invalid API KeyIncorrect client_id:client_secret
403permission 'transfer:write' requiredAPI Key lacks PIX permission
403IP not whitelistedSource IP outside the API Key allowlist

Code vocabulary - UPPERCASE × lowercase

The structured cash-out codes come from distinct vocabularies:

NamespaceConventionOriginExamples
BACEN SPIUPPERCASEAsynchronous rejections via PACS.002 RJCT, visible in status queries and webhook pix.payout.rejectedAC03, AB03, ED05, DUPL, AM02, FF08, BE01
Provider / DICTlowercase snake_caseSynchronous rejections before BACEN confirmationdict_key_not_found, dict_rate_limited, same_institution_transfer, provider_schema_error
Automatic reprocessingUPPERCASE (prefix DICT_)Webhook pix.payout.queued / pix.payout.failed when there is automatic reprocessingDICT_CLIENT_RATE_LIMITED, DICT_BUCKET_EXHAUSTED, DICT_QUEUE_TIMEOUT

When switching on errors programmatically, normalize to uppercase or lowercase on your side to avoid duplicate branches. Do not expect AM02 in synchronous responses - BACEN codes only appear in GET queries after acceptance.

Corresponding webhooks

  • Synchronous rejections (Formats A/B above) do not fire a webhook - the client has already received the error in the HTTP response.
  • Queueing due to rate-limit (HTTP 202 queued) dispatches pix.payout.queued immediately with reason_code + reason_description.
  • Asynchronous rejections (PACS.002 RJCT after 202 acceptance) dispatch pix.payout.rejected with BACEN reason_code (AC03, AB03, ED05, DUPL, etc.) and reason_description in English.
  • Orphan voids (>30min without PACS.002) dispatch pix.payout.failed with reason_code: "orphan_force_voided".
  • Automatic reprocessing expiration dispatches pix.payout.failed with reason_code: "DICT_QUEUE_TIMEOUT".

PIX Key Types

TypeFormatExample
cpf11 digits (no punctuation)12345678901
cnpj14 digits (no punctuation)12345678000199
emailEmail addressnome@empresa.com.br
phonearea code + number (11 digits)11999998888
evpUUID v4a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d

11-digit keys - CPF vs Phone ambiguity

Keys with exactly 11 digits may be either a CPF or a mobile phone (area code + 9xxxx-xxxx). When the key is ambiguous, the API rejects with HTTP 400 and failure_reason: "ambiguous key".

Recommended solution:

  1. Use the CPF Validation endpoint (POST /api/external/cpf/validate) to check if the 11 digits form a valid CPF
  2. If valid: true → send pix_key_type: "cpf" in cash-out
  3. If valid: false → it's a phone, send pix_key_type: "phone" (the API automatically adds the +55 prefix)
javascript
// Example automated flow
async function resolveKeyType(key) {
  if (key.length !== 11 || /\D/.test(key)) return null; // no ambiguity
  
  const { data } = await api.post('/api/external/cpf/validate', { cpf: key });
  return data.valid ? 'cpf' : 'phone';
}

Tip: send phones as 11 raw digits (area code + number). The API adds the +55 prefix automatically. Avoid sending +55 manually - it may cause HMAC validation failures in some clients.

Next Steps

After creating the transfer, track the status via:

Or receive confirmation automatically via Webhook.

Minha Konta Instituição de Pagamento - ISPB 39929224