PIX Cash-Out por Clave
Realiza una transferencia PIX utilizando la clave PIX del destinatario.
Endpoint
POST /api/external/pix/cash-outHeaders
| Header | Tipo | Obligatorio | Descripcion |
|---|---|---|---|
Authorization | String | Si | ApiKey {client_id}:{client_secret} |
Content-Type | String | Si | application/json |
hmac | String | Si | Firma HMAC-SHA512 del body (hex) |
Idempotency-Key | String | No | Clave unica para evitar procesamiento duplicado (max 256 chars) |
Autenticacion
Vea Autenticacion. La firma HMAC debe generarse conforme lo descrito en HMAC-SHA512.
Idempotency-Key - comportamiento de replay
Cuando presente, la API almacena la respuesta (solo en 2xx) por 24 horas y retorna la respuesta en cache para cualquier nuevo POST con la misma combinacion (metodo, path, Idempotency-Key). El cache es scope por endpoint (misma clave en /cash-out y /refund no colisiona).
- En la respuesta replay, la API incluye el header
X-Idempotent-Replay: truey reproduce elIdempotency-Keyenviado. - Claves arriba de 256 caracteres retornan
400 Bad Request. - La clave es opcional. En caso de no enviarla, la API procesa cada POST como una nueva transaccion (el
end_to_end_iddeterminista aun garantiza idempotencia en la capa BACEN/SPI, pero puede generar rechazo confailure_reason: "DUPL"si el primer intento ya fue liquidado).
Permiso obligatorio
La API Key debe tener el permiso transfer:write para enviar PIX. Sin el, la solicitud retorna 403 Forbidden. Vea como configurar permisos.
Request Body
| Campo | Tipo | Obligatorio | Descripcion |
|---|---|---|---|
amount | Integer | Si | Valor en centavos. R$ 30,00 = 3000 |
pix_key | String | Si | Clave PIX del destinatario |
pix_key_type | String | No | Tipo de clave: cpf, cnpj, email, phone, evp. Si se omite, se detecta automaticamente a partir de la clave. |
description | String | No | Descripcion de la transferencia (max 140 caracteres) |
external_id | String | No | Identificador de su sistema para rastreo. Max 128 chars despues de trim. Solo caracteres a-zA-Z0-9._:-. Retornado en respuestas y webhooks. Valores invalidos (chars no permitidos, > 128 chars, vacio despues de trim) son silenciosamente descartados - la transaccion prosigue con external_id: null. Valide en su lado antes de enviar si necesita garantizar la persistencia. |
recipient_ispb | String | No | ISPB de la institucion del destinatario para enrutamiento manual (8 digitos). Cuando informado, dirige el pago al PSP especificado. No envie el ISPB de Minha Konta (04838403) - requests intra-institucionales retornan error same_institution (PIX interno no soportado). |
end_to_end_id | String | No | End-to-End ID en formato BACEN (E{ISPB}{YYYYMMDDHHmm}{entropy}). Recomendado omitir - el backend genera un E2E determinista en cada intento (mismo amount + pix_key + merchant_id → mismo E2E). Ese determinismo garantiza idempotencia en el SPI/BACEN incluso sin Idempotency-Key. Solo envie manualmente en escenarios de reprocesamiento coordinado. |
purpose | String | No | Finalidad de la transferencia (campo libre para uso interno y compliance). |
Valores monetarios
Los valores de entrada son en centavos (R$ 1,00 = 100). Los valores de respuesta son en unidades base (R$ 1,00 = 10000). Para convertir la respuesta a reales, divida por 10.000. Nunca use punto flotante.
Ejemplo
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"
}'Respuesta de Exito -- 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: Transaccion ya liquidada (
final: true,status: "settled"). - HTTP 202: Transaccion aceptada para procesamiento (
final: false). Acompane el status via polling o webhook.statuspuede ser"accepted"(flujo normal),"queued"(rate-limit aplicado - retry automatico cada 3s por hasta 120min) o"pending_approval"(aguardando aprobacion via workflow de doble validacion, cuando habilitado).
| Campo | Tipo | Descripcion |
|---|---|---|
worked | Boolean | true indica que la solicitud fue aceptada |
final | Boolean | true cuando la transaccion alcanzo estado terminal (liquidada o rechazada). false cuando aun en procesamiento |
transaction_id | String | Identificador unico de la transaccion |
end_to_end_id | String | Identificador End-to-End en el SPI/BACEN (formato E{ISPB}...) |
external_id | String | Su identificador, retornado tal como fue enviado. null si no informado |
amount | Integer | Valor de la transferencia en unidades base (÷ 10.000 para reales). 300000 = R$ 30,00 |
fee_amount | Integer | Tarifa cobrada en unidades base (÷ 10.000 para reales) |
net_amount | Integer | Valor bruto debitado en la cuenta pagadora, en unidades base. Calculado como amount + fee_amount (el debito total incluye la tarifa). No es el valor que el destinatario recibe - el recibe solo amount. Ejemplo: amount=300000 + fee_amount=350 → net_amount=300350 (R$ 30,035 debitados de su cuenta, R$ 30,00 acreditados al destinatario) |
status | String | Uno de: accepted (HTTP 202, procesamiento sincronico normal), settled (HTTP 200, liquidacion inmediata - raro en fast-track), queued (HTTP 202, aguardando reprocesamiento automatico por limite DICT), pending_approval (HTTP 202, aguardando aprobacion). Vea los status terminales en Consultar Cash-Out por ID -- Valores del campo status |
detail | String | Mensaje descriptivo |
Sentido de net_amount en cash-out difiere de cash-in
En cash-out, net_amount = amount + fee_amount (debito bruto en la cuenta pagadora). En cash-in (QR Code pagado), el backend trata net_amount como valor liquido acreditado despues de la tarifa ser descontada. Esa asimetria es historica - trate net_amount siempre como "movimiento efectivo en su cuenta en esa direccion". Para conciliacion contable, prefiera operar con los campos amount y fee_amount por separado.
Codigos de Rechazo
La API puede rechazar un cash-out por validacion de entrada (antes de enviar al SPI), por error de integracion con el proveedor / DICT (durante el envio sincronico), o por rate-limit con retry automatico en fila. Rechazos BACEN via PACS.002 RJCT llegan de forma asincronica y aparecen solo via consulta de status o webhook pix.payout.rejected.
Formato de la respuesta de error
Los rechazos sincronicos del cash-out retornan en dos formatos distintos - escoja el parser correcto segun el origen del error:
Formato A -- Validacion o integracion: HTTP 400 o 422, body {"status": "failed", "errors": [{"code": "<codigo>", "params": [...]}]}. Codigos comunes: same_institution_transfer, insufficient_balance, dict_key_not_found, dict_rate_limited, dict_bucket_exhausted.
Formato B -- Error de validacion de payload: HTTP 400, body {"errors": {"bad_request": "mensaje"}}. Ejemplos: invalid or missing amount, ambiguous key.
Enrute via data.status === "failed" (Formato A) vs data.errors.bad_request (Formato B).
Errores de validacion (HTTP 400 / 422)
| HTTP | Formato | Campo con codigo | Significado |
|---|---|---|---|
| 400 | B | errors.bad_request: "invalid or missing amount" | amount ausente, cero, negativo o no-entero |
| 422 | A | errors[0].code: "pix_key_ambiguous" | Clave de 11 digitos sin pix_key_type - puede ser CPF o telefono. Resuelva via Validacion CPF e informe pix_key_type explicitamente |
| 400 | B | errors.bad_request: "invalid pix_key" | Clave no paso las reglas de formato (CPF checksum invalido, email malformado, etc.) |
| 422 | A | errors[0].code: "same_institution_transfer" | recipient_ispb es el propio ISPB de Minha Konta (04838403). PIX intra-institucional no es soportado - use TEF interno. Nota: esta validacion retorna HTTP 422 (no 400) con la estructura {status: "failed", errors: [{code: "same_institution_transfer", params: []}]} |
| 422 | A | errors[0].code: "insufficient_balance" | Saldo disponible menor que amount + fee_amount. Considera hold activo (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 |
Cambio de shape para same_institution
Versiones anteriores de estos docs afirmaban HTTP 400 con detail: "same_institution". El comportamiento real es HTTP 422 con el shape del Formato A (errors como array de {code, params}). Clientes que hacen if (status === 400 && body.detail === "same_institution") no disparan en la practica - utilice if (status === 422 && body.errors?.[0]?.code === "same_institution_transfer").
Errores de integracion con el proveedor / DICT (HTTP 400)
Cuando el proveedor retorna error sincronico antes de la confirmacion BACEN, la API retorna Formato A con HTTP 400:
Codigo (errors[0].code) | Significado | Accion recomendada |
|---|---|---|
dict_key_not_found | Clave PIX no localizada en DICT/BACEN | Verifique con el pagador; la clave puede haber sido removida o nunca registrada |
dict_key_blocked | Clave bloqueada, por ejemplo por sospecha de fraude | Contacto con el titular de la clave |
dict_lookup_failed | Falla al consultar DICT | Retry en 5-30s |
dict_rate_limited | Limite temporal de consulta DICT | Aguarde el reprocesamiento automatico o aplique backoff antes de nueva solicitud |
dict_bucket_exhausted | Limite DICT temporalmente indisponible | Aguarde el reprocesamiento automatico; evite rafagas |
provider_rejected | Proveedor rechazo con error 4xx generico no clasificado | Vea errors[0].params para contexto y reabra caso con soporte Minha Konta |
provider_schema_error | Payload de integracion incompatible | Reporte inmediatamente y no intente rehacer sin orientacion de Minha Konta |
provider_unknown_error | Status fuera de 400..499 que entro en este camino | Log completo disponible en soporte |
HTTP es 400 (no 429)
Versiones anteriores de estos docs mostraban HTTP 429 para dict_rate_limited y dict_bucket_exhausted en el camino sincronico. El contrato actual es: error sincronico de integracion retorna HTTP 400; limite DICT con reprocesamiento automatico retorna HTTP 202 queued.
Rate-limit con retry automatico (HTTP 202 queued)
Cuando Minha Konta detecta un limite de consulta DICT antes de enviar la transaccion al proveedor, el PIX OUT permanece en procesamiento y entra en retry automatico. Este camino evita una nueva llamada DICT mientras no haya capacidad disponible.
Dos escenarios disparan la fila:
| Origen | reason_code (webhook pix.payout.queued) | Causa |
|---|---|---|
| Cuota por cliente | DICT_CLIENT_RATE_LIMITED | Volumen de consultas DICT del cliente por encima de la politica de proteccion de Minha Konta. |
| Limite DICT del proveedor | DICT_BUCKET_EXHAUSTED | Capacidad operacional de consultas DICT temporalmente indisponible. |
Respuesta HTTP cuando encolado:
{
"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
}Mecanica del retry:
- Minha Konta reintenta automaticamente mientras la ventana de reprocesamiento este activa.
- TTL total: 7200 segundos (120 minutos). Al expirar, el cliente recibe
pix.payout.failedconreason_code: "DICT_QUEUE_TIMEOUT". - Webhook inmediato: al entrar en la fila, Minha Konta dispara
pix.payout.queuedconreason_code(DICT_CLIENT_RATE_LIMITEDoDICT_BUCKET_EXHAUSTED) yreason_description. El proximo evento serapix.payout.confirmedopix.payout.failed.
Status no terminal
Siempre trate queued y accepted como estados no terminales. Acompanhe el resultado por webhook o por consulta de la transaccion.
Permiso y autenticacion (HTTP 401 / 403)
| HTTP | detail | Significado |
|---|---|---|
| 401 | Invalid HMAC signature | Firma HMAC no coincide. Confirme el orden alfabetico de los campos en el body serializado - vea HMAC-SHA512 |
| 401 | Invalid API Key | client_id:client_secret incorrecto |
| 403 | permission 'transfer:write' required | API Key sin permiso para PIX |
| 403 | IP not whitelisted | IP de origen fuera de la allowlist de la API Key |
Vocabulario de codigos - UPPERCASE × lowercase
Los codigos estructurados del cash-out vienen de vocabularios distintos:
| Namespace | Convencion | Origen | Ejemplos |
|---|---|---|---|
| BACEN SPI | UPPERCASE | Rechazos asincronicos via PACS.002 RJCT, visibles en consulta de status y webhook pix.payout.rejected | AC03, AB03, ED05, DUPL, AM02, FF08, BE01 |
| Proveedor / DICT | lowercase snake_case | Rechazos sincronicos antes de la confirmacion BACEN | dict_key_not_found, dict_rate_limited, same_institution_transfer, provider_schema_error |
| Reprocesamiento automatico | UPPERCASE (prefijo DICT_) | Webhook pix.payout.queued / pix.payout.failed cuando hay reprocesamiento automatico | DICT_CLIENT_RATE_LIMITED, DICT_BUCKET_EXHAUSTED, DICT_QUEUE_TIMEOUT |
Al hacer switch programatico de errores, normalice a uppercase o lowercase en su lado para evitar branches duplicados. No espere AM02 en respuestas sincronicas - BACEN codes solo aparecen en consultas GET pos-aceptacion.
Webhooks correspondientes
- Rechazos sincronicos (Formato A/B arriba) no disparan webhook - el cliente ya recibio el error en la respuesta HTTP.
- Encolamiento por rate-limit (HTTP 202
queued) disparapix.payout.queuedinmediatamente conreason_code+reason_description. - Rechazos asincronicos (PACS.002 RJCT despues de aceptacion 202) disparan
pix.payout.rejectedconreason_codeBACEN (AC03, AB03, ED05, DUPL etc.) yreason_descriptionen ingles. - Voids de orfanas (>30min sin PACS.002) disparan
pix.payout.failedconreason_code: "orphan_force_voided". - Expiracion de la fila de retry (120min) dispara
pix.payout.failedconreason_code: "DICT_QUEUE_TIMEOUT".
Tipos de Clave PIX
| Tipo | Formato | Ejemplo |
|---|---|---|
cpf | 11 digitos (sin puntuacion) | 12345678901 |
cnpj | 14 digitos (sin puntuacion) | 12345678000199 |
email | Direccion de e-mail | nome@empresa.com.br |
phone | DDD + numero (11 digitos) | 11999998888 |
evp | UUID v4 | a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d |
Claves de 11 digitos - Ambiguedad CPF vs Telefono
Claves con exactamente 11 digitos pueden ser tanto un CPF como un telefono celular (DDD + 9xxxx-xxxx). Cuando la clave es ambigua, la API rechaza con HTTP 400 y failure_reason: "ambiguous key".
Solucion recomendada:
- Use el endpoint Validacion CPF (
POST /api/external/cpf/validate) para verificar si los 11 digitos forman un CPF valido - Si
valid: true→ enviepix_key_type: "cpf"en el cash-out - Si
valid: false→ es un telefono, enviepix_key_type: "phone"(la API agrega automaticamente el prefijo+55)
// Ejemplo de flujo automatizado
async function resolveKeyType(key) {
if (key.length !== 11 || /\D/.test(key)) return null; // sin ambiguedad
const { data } = await api.post('/api/external/cpf/validate', { cpf: key });
return data.valid ? 'cpf' : 'phone';
}Tip: envie telefonos como 11 digitos puros (DDD + numero). La API agrega el prefijo +55 automaticamente. Evite enviar el +55 manualmente - puede causar falla en la validacion HMAC en algunos clientes.
Proximos Pasos
Despues de crear la transferencia, acompane el status via:
- Consultar por ID
- Consultar por E2E ID
- Consultar por Tag
- Consultar por External ID --
GET /api/external/transactions/ref/{external_id}
O reciba la confirmacion automaticamente via Webhook.
