PIX Cash-Out por Chave
Realiza uma transferência PIX utilizando a chave PIX do destinatário.
Endpoint
POST /api/external/pix/cash-outHeaders
| Header | Tipo | Obrigatório | Descrição |
|---|---|---|---|
Authorization | String | Sim | ApiKey {client_id}:{client_secret} |
Content-Type | String | Sim | application/json |
hmac | String | Sim | Assinatura HMAC-SHA512 do body (hex) |
Idempotency-Key | String | Não | Chave única para evitar processamento duplicado (max 256 chars) |
Autenticação
Veja Autenticação. A assinatura HMAC deve ser gerada conforme descrito em HMAC-SHA512.
Idempotency-Key - comportamento de replay
Quando presente, a API armazena a resposta (apenas em 2xx) por 24 horas e retorna a resposta em cache para qualquer novo POST com a mesma combinação (método, path, Idempotency-Key). O cache é escopado por endpoint (mesma chave em /cash-out e /refund não colide).
- Na resposta replay, a API inclui o header
X-Idempotent-Replay: truee ecoa oIdempotency-Keyenviado. - Chaves acima de 256 caracteres retornam
400 Bad Request. - A chave é opcional. Caso não envie, a API processa cada POST como uma nova transação (o
end_to_end_iddeterminístico ainda garante idempotência na camada BACEN/SPI, mas pode gerar rejeição comfailure_reason: "DUPL"se a primeira tentativa já tiver sido liquidada).
Permissão obrigatória
A API Key deve ter a permissão transfer:write para enviar PIX. Sem ela, a requisição retorna 403 Forbidden. Veja como configurar permissões.
Request Body
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
amount | Integer | Sim | Valor em centavos. R$ 30,00 = 3000 |
pix_key | String | Sim | Chave PIX do destinatário |
pix_key_type | String | Não | Tipo da chave: cpf, cnpj, email, phone, evp. Se omitido, detectado automaticamente a partir da chave. |
description | String | Não | Descrição da transferência (max 140 caracteres) |
external_id | String | Não | Identificador do seu sistema para rastreamento. Max 128 chars após trim. Apenas caracteres a-zA-Z0-9._:-. Retornado em respostas e webhooks. Valores inválidos (chars não permitidos, > 128 chars, vazio após trim) são silenciosamente descartados - a transação prossegue com external_id: null. Valide no seu lado antes de enviar se precisar garantir a persistência. |
recipient_ispb | String | Não | ISPB da instituição do destinatário para roteamento manual (8 dígitos). Quando informado, direciona o pagamento ao PSP especificado. Não envie o ISPB da Minha Konta (04838403) - requests intra-institucionais retornam erro same_institution (PIX interno não suportado). |
end_to_end_id | String | Não | End-to-End ID no formato BACEN (E{ISPB}{YYYYMMDDHHmm}{entropy}). Recomendado omitir - o backend gera um E2E determinístico a cada tentativa (mesmo amount + pix_key + merchant_id → mesmo E2E). Esse determinismo garante idempotência no SPI/BACEN mesmo sem Idempotency-Key. Só envie manualmente em cenários de reprocessamento coordenado. |
purpose | String | Não | Finalidade da transferência (campo livre para uso interno e compliance). |
Valores monetários
Valores de entrada são em centavos (R$ 1,00 = 100). Valores de resposta são em unidades base (R$ 1,00 = 10000). Para converter a resposta para reais, divida por 10.000. Nunca use ponto flutuante.
Exemplo
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"
}'Resposta de Sucesso -- 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: Transação já liquidada (
final: true,status: "settled"). - HTTP 202: Transação aceita para processamento (
final: false). Acompanhe o status via polling ou webhook.statuspode ser"accepted"(fluxo normal),"queued"(limite DICT aplicado, com reprocessamento automático) ou"pending_approval"(aguardando aprovação via workflow de dupla-alçada, quando habilitado).
| Campo | Tipo | Descrição |
|---|---|---|
worked | Boolean | true indica que a requisição foi aceita |
final | Boolean | true quando a transação atingiu estado terminal (liquidada ou rejeitada). false quando ainda em processamento |
transaction_id | String | Identificador único da transação |
end_to_end_id | String | Identificador End-to-End no SPI/BACEN (formato E{ISPB}...) |
external_id | String | Seu identificador, retornado tal como enviado. null se não informado |
amount | Integer | Valor da transferência em unidades base (÷ 10.000 para reais). 300000 = R$ 30,00 |
fee_amount | Integer | Tarifa cobrada em unidades base (÷ 10.000 para reais) |
net_amount | Integer | Valor bruto debitado na conta pagadora, em unidades base. Calculado como amount + fee_amount (o debito total inclui a tarifa). Não é o valor que o destinatário recebe - ele recebe apenas amount. Exemplo: amount=300000 + fee_amount=350 → net_amount=300350 (R$ 30,035 debitados da sua conta, R$ 30,00 creditados no destinatário) |
status | String | Um de: accepted (HTTP 202, processamento síncrono normal), settled (HTTP 200, liquidação imediata - rara em fast-track), queued (HTTP 202, aguardando reprocessamento automático por limite DICT), pending_approval (HTTP 202, aguardando aprovação). Veja os status terminais em Consultar Cash-Out por ID -- Valores do campo status |
detail | String | Mensagem descritiva |
Sentido de net_amount em cash-out difere de cash-in
Em cash-out, net_amount = amount + fee_amount (débito bruto na conta pagadora). Em cash-in (QR Code pago), o backend trata net_amount como valor líquido creditado após a tarifa ser descontada. Essa assimetria é histórica - trate net_amount sempre como "movimentação efetiva na sua conta naquela direção". Para conciliação contábil, prefira operar com os campos amount e fee_amount separadamente.
Códigos de Rejeição
A API pode rejeitar um cash-out por validação de entrada (antes de enviar ao SPI), por erro de integração com o provedor / DICT (durante o envio síncrono), ou por rate-limit com retry automático em fila. Rejeições BACEN via PACS.002 RJCT chegam de forma assíncrona e aparecem apenas via consulta de status ou webhook pix.payout.rejected.
Formato da resposta de erro
As rejeições síncronas do cash-out retornam em dois formatos distintos - escolha o parser correto conforme a origem do erro:
Formato A -- Validação ou integração: HTTP 400 ou 422, body {"status": "failed", "errors": [{"code": "<código>", "message": "<texto>", "params": {...}}]}. O campo message (texto em inglês legível por humanos) foi adicionado em 2026-06-10; code e params permanecem inalterados. Códigos comuns: same_institution_transfer, insufficient_balance, dict_key_not_found, dict_rate_limited, dict_bucket_exhausted.
Formato B -- Erro de validação de payload: HTTP 400, body {"errors": {"bad_request": "mensagem"}}. Exemplos: invalid or missing amount, ambiguous key.
Roteie via data.status === "failed" (Formato A) vs data.errors.bad_request (Formato B).
Erros de validação (HTTP 400 / 422)
| HTTP | Formato | Campo com código | Significado |
|---|---|---|---|
| 400 | B | errors.bad_request: "invalid or missing amount" | amount ausente, zero, negativo ou não-inteiro |
| 422 | A | errors[0].code: "pix_key_ambiguous" | Chave de 11 dígitos sem pix_key_type - pode ser CPF ou telefone. Resolva via Validação CPF e informe pix_key_type explicitamente. O campo errors[0].message traz a orientação |
| 400 | B | errors.bad_request: "invalid pix_key" | Chave não passou nas regras de formato (CPF checksum inválido, email malformado, etc.) |
| 422 | A | errors[0].code: "same_institution_transfer" | recipient_ispb é o próprio ISPB da Minha Konta (04838403). PIX intra-institucional não é suportado - use TEF interno. Nota: esta validação retorna HTTP 422 (não 400) com a estrutura {status: "failed", errors: [{code: "same_institution_transfer", params: []}]} |
| 422 | A | errors[0].code: "insufficient_balance" | Saldo disponível menor que amount + fee_amount. Considera hold ativo (gotcha min(TB, PG)) |
| 422 | A | errors[0].code: "ceiling_exceeded" | amount excede o limite por transação configurado para a instituição. errors[0].message = "PIX out limit reached: amount R$ X exceeds the R$ Y per-transaction limit"; errors[0].params.ceiling traz o teto em subcentavos |
Mudança de shape para same_institution
Versões anteriores destes docs afirmavam HTTP 400 com detail: "same_institution". O comportamento real é HTTP 422 com o shape do Formato A (errors como array de {code, params}). Clientes que fazem if (status === 400 && body.detail === "same_institution") não disparam na prática - utilize if (status === 422 && body.errors?.[0]?.code === "same_institution_transfer").
Erros de integração com o provedor / DICT (HTTP 400)
Quando o provedor retorna erro síncrono antes da confirmação BACEN, a API retorna Formato A com HTTP 400:
Código (errors[0].code) | Significado | Ação recomendada |
|---|---|---|
dict_key_not_found | Chave PIX não localizada no DICT/BACEN | Verifique com o pagador; a chave pode ter sido removida ou nunca registrada |
dict_key_blocked | Chave bloqueada, por exemplo por suspeita de fraude | Contato com o titular da chave |
dict_lookup_failed | Falha ao consultar DICT | Retry em 5-30s |
dict_rate_limited | Limite temporário de consulta DICT | Aguarde o reprocessamento automático ou aplique backoff antes de nova solicitação |
dict_bucket_exhausted | Limite DICT temporariamente indisponível | Aguarde o reprocessamento automático; evite rajadas |
provider_rejected | Provedor rejeitou com erro 4xx genérico não classificado | Veja errors[0].params para contexto e reabra caso com suporte Minha Konta |
provider_schema_error | Payload de integração incompatível | Reporte imediatamente e não tente refazer sem orientação da Minha Konta |
provider_unknown_error | Status fora de 400..499 que entrou neste caminho | Log completo disponível no suporte |
HTTP é 400 (não 429)
Versões anteriores destes docs mostravam HTTP 429 para dict_rate_limited e dict_bucket_exhausted no caminho síncrono. O contrato atual é: erro síncrono de integração retorna HTTP 400; limite DICT com reprocessamento automático retorna HTTP 202 queued.
Rate-limit com retry automático (HTTP 202 queued)
Quando a Minha Konta detecta limite de consulta DICT antes de enviar a transação ao provedor, o PIX OUT permanece em processamento e entra em reprocessamento automático. Esse caminho evita nova chamada DICT enquanto não houver capacidade disponível.
Dois cenários disparam a fila:
| Origem | reason_code (webhook pix.payout.queued) | Causa |
|---|---|---|
| Quota por cliente | DICT_CLIENT_RATE_LIMITED | Volume de consultas DICT do cliente acima da política de proteção da Minha Konta. |
| Limite DICT do provedor | DICT_BUCKET_EXHAUSTED | Limite operacional de consultas DICT temporariamente indisponível. |
Resposta HTTP quando enfileirado:
{
"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
}Mecânica do retry:
- A Minha Konta tenta reenviar automaticamente enquanto a janela de reprocessamento estiver ativa.
- TTL total: 7200 segundos (120 minutos). Após expirar, o cliente recebe webhook
pix.payout.failedcomreason_code: "DICT_QUEUE_TIMEOUT". - Webhook imediato: ao entrar na fila, a Minha Konta dispara
pix.payout.queuedcomreason_code(DICT_CLIENT_RATE_LIMITEDouDICT_BUCKET_EXHAUSTED) ereason_description. O próximo evento serápix.payout.confirmedoupix.payout.failed.
Status não terminal
Sempre trate queued e accepted como estados não terminais. Acompanhe o resultado por webhook ou pelo endpoint de consulta da transação.
Permissão e autenticação (HTTP 401 / 403)
| HTTP | detail | Significado |
|---|---|---|
| 401 | Invalid HMAC signature | Assinatura HMAC não bate. Confira a ordem alfabética dos campos no body serializado - veja HMAC-SHA512 |
| 401 | Invalid API Key | client_id:client_secret incorreto |
| 403 | permission 'transfer:write' required | API Key sem permissão para PIX |
| 403 | IP not whitelisted | IP de origem fora da allowlist da API Key |
Vocabulário de códigos - UPPERCASE × lowercase
Os códigos estruturados do cash-out vêm de vocabulários distintos:
| Namespace | Convenção | Origem | Exemplos |
|---|---|---|---|
| BACEN SPI | UPPERCASE | Rejeições assíncronas via PACS.002 RJCT, visíveis em consulta de status e webhook pix.payout.rejected | AC03, AB03, ED05, DUPL, AM02, FF08, BE01 |
| Provedor / DICT | lowercase snake_case | Rejeições síncronas antes da confirmação BACEN | dict_key_not_found, dict_rate_limited, same_institution_transfer, provider_schema_error |
| Reprocessamento automático | UPPERCASE (prefixo DICT_) | Webhook pix.payout.queued / pix.payout.failed quando há reprocessamento automático | DICT_CLIENT_RATE_LIMITED, DICT_BUCKET_EXHAUSTED, DICT_QUEUE_TIMEOUT |
Ao fazer switch programático de erros, normalize para uppercase ou lowercase no seu lado para evitar branches duplicados. Não espere AM02 em respostas síncronas - BACEN codes só aparecem em consultas GET pós-aceite.
Webhooks correspondentes
- Rejeições síncronas (Formato A/B acima) não disparam webhook - o cliente já recebeu o erro na resposta HTTP.
- Enfileiramento por rate-limit (HTTP 202
queued) disparapix.payout.queuedimediatamente comreason_code+reason_description. - Rejeições assíncronas (PACS.002 RJCT após aceite 202) disparam
pix.payout.rejectedcomreason_codeBACEN (AC03, AB03, ED05, DUPL etc.) ereason_descriptionem inglês. - Voids de orfãs (>30min sem PACS.002) disparam
pix.payout.failedcomreason_code: "orphan_force_voided". - Expiração da fila de retry (120min) dispara
pix.payout.failedcomreason_code: "DICT_QUEUE_TIMEOUT".
Tipos de Chave PIX
| Tipo | Formato | Exemplo |
|---|---|---|
cpf | 11 dígitos (sem pontuação) | 12345678901 |
cnpj | 14 dígitos (sem pontuação) | 12345678000199 |
email | Endereço de e-mail | nome@empresa.com.br |
phone | DDD + número (11 dígitos) | 11999998888 |
evp | UUID v4 | a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d |
Chaves de 11 dígitos - Ambiguidade CPF vs Telefone
Chaves com exatamente 11 dígitos podem ser tanto um CPF quanto um telefone celular (DDD + 9xxxx-xxxx). Quando a chave é ambígua, a API rejeita com HTTP 422 e errors[0].code: "pix_key_ambiguous" (Formato A).
Solução recomendada:
- Use o endpoint Validação CPF (
POST /api/external/cpf/validate) para verificar se os 11 dígitos formam um CPF válido - Se
valid: true→ enviepix_key_type: "cpf"no cash-out - Se
valid: false→ é um telefone, enviepix_key_type: "phone"(a API adiciona automaticamente o prefixo+55)
// Exemplo de fluxo automatizado
async function resolveKeyType(key) {
if (key.length !== 11 || /\D/.test(key)) return null; // sem ambiguidade
const { data } = await api.post('/api/external/cpf/validate', { cpf: key });
return data.valid ? 'cpf' : 'phone';
}Dica: envie telefones como 11 dígitos puros (DDD + número). A API adiciona o prefixo +55 automaticamente. Evite enviar o +55 manualmente - pode causar falha na validação HMAC em alguns clientes.
Próximos Passos
Após criar a transferência, acompanhe o status via:
- Consultar por ID
- Consultar por E2E ID
- Consultar por Tag
- Consultar por External ID --
GET /api/external/transactions/ref/{external_id}
Ou receba a confirmação automaticamente via Webhook.
