Skip to content

PIX Cash-Out por Clave

Realiza una transferencia PIX utilizando la clave PIX del destinatario.

Endpoint

POST /api/external/pix/cash-out

Headers

HeaderTipoObligatorioDescripcion
AuthorizationStringSiApiKey {client_id}:{client_secret}
Content-TypeStringSiapplication/json
hmacStringSiFirma HMAC-SHA512 del body (hex)
Idempotency-KeyStringNoClave 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: true y reproduce el Idempotency-Key enviado.
  • 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_id determinista aun garantiza idempotencia en la capa BACEN/SPI, pero puede generar rechazo con failure_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

CampoTipoObligatorioDescripcion
amountIntegerSiValor en centavos. R$ 30,00 = 3000
pix_keyStringSiClave PIX del destinatario
pix_key_typeStringNoTipo de clave: cpf, cnpj, email, phone, evp. Si se omite, se detecta automaticamente a partir de la clave.
descriptionStringNoDescripcion de la transferencia (max 140 caracteres)
external_idStringNoIdentificador 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_ispbStringNoISPB 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_idStringNoEnd-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.
purposeStringNoFinalidad 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

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"
  }'

Respuesta de Exito -- 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: Transaccion ya liquidada (final: true, status: "settled").
  • HTTP 202: Transaccion aceptada para procesamiento (final: false). Acompane el status via polling o webhook. status puede 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).
CampoTipoDescripcion
workedBooleantrue indica que la solicitud fue aceptada
finalBooleantrue cuando la transaccion alcanzo estado terminal (liquidada o rechazada). false cuando aun en procesamiento
transaction_idStringIdentificador unico de la transaccion
end_to_end_idStringIdentificador End-to-End en el SPI/BACEN (formato E{ISPB}...)
external_idStringSu identificador, retornado tal como fue enviado. null si no informado
amountIntegerValor de la transferencia en unidades base (÷ 10.000 para reales). 300000 = R$ 30,00
fee_amountIntegerTarifa cobrada en unidades base (÷ 10.000 para reales)
net_amountIntegerValor 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=350net_amount=300350 (R$ 30,035 debitados de su cuenta, R$ 30,00 acreditados al destinatario)
statusStringUno 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
detailStringMensaje 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)

HTTPFormatoCampo con codigoSignificado
400Berrors.bad_request: "invalid or missing amount"amount ausente, cero, negativo o no-entero
422Aerrors[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
400Berrors.bad_request: "invalid pix_key"Clave no paso las reglas de formato (CPF checksum invalido, email malformado, etc.)
422Aerrors[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: []}]}
422Aerrors[0].code: "insufficient_balance"Saldo disponible menor que amount + fee_amount. Considera hold activo (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

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)SignificadoAccion recomendada
dict_key_not_foundClave PIX no localizada en DICT/BACENVerifique con el pagador; la clave puede haber sido removida o nunca registrada
dict_key_blockedClave bloqueada, por ejemplo por sospecha de fraudeContacto con el titular de la clave
dict_lookup_failedFalla al consultar DICTRetry en 5-30s
dict_rate_limitedLimite temporal de consulta DICTAguarde el reprocesamiento automatico o aplique backoff antes de nueva solicitud
dict_bucket_exhaustedLimite DICT temporalmente indisponibleAguarde el reprocesamiento automatico; evite rafagas
provider_rejectedProveedor rechazo con error 4xx generico no clasificadoVea errors[0].params para contexto y reabra caso con soporte Minha Konta
provider_schema_errorPayload de integracion incompatibleReporte inmediatamente y no intente rehacer sin orientacion de Minha Konta
provider_unknown_errorStatus fuera de 400..499 que entro en este caminoLog 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:

Origenreason_code (webhook pix.payout.queued)Causa
Cuota por clienteDICT_CLIENT_RATE_LIMITEDVolumen de consultas DICT del cliente por encima de la politica de proteccion de Minha Konta.
Limite DICT del proveedorDICT_BUCKET_EXHAUSTEDCapacidad operacional de consultas DICT temporalmente indisponible.

Respuesta HTTP cuando encolado:

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
}

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.failed con reason_code: "DICT_QUEUE_TIMEOUT".
  • Webhook inmediato: al entrar en la fila, Minha Konta dispara pix.payout.queued con reason_code (DICT_CLIENT_RATE_LIMITED o DICT_BUCKET_EXHAUSTED) y reason_description. El proximo evento sera pix.payout.confirmed o pix.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)

HTTPdetailSignificado
401Invalid HMAC signatureFirma HMAC no coincide. Confirme el orden alfabetico de los campos en el body serializado - vea HMAC-SHA512
401Invalid API Keyclient_id:client_secret incorrecto
403permission 'transfer:write' requiredAPI Key sin permiso para PIX
403IP not whitelistedIP 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:

NamespaceConvencionOrigenEjemplos
BACEN SPIUPPERCASERechazos asincronicos via PACS.002 RJCT, visibles en consulta de status y webhook pix.payout.rejectedAC03, AB03, ED05, DUPL, AM02, FF08, BE01
Proveedor / DICTlowercase snake_caseRechazos sincronicos antes de la confirmacion BACENdict_key_not_found, dict_rate_limited, same_institution_transfer, provider_schema_error
Reprocesamiento automaticoUPPERCASE (prefijo DICT_)Webhook pix.payout.queued / pix.payout.failed cuando hay reprocesamiento automaticoDICT_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) dispara pix.payout.queued inmediatamente con reason_code + reason_description.
  • Rechazos asincronicos (PACS.002 RJCT despues de aceptacion 202) disparan pix.payout.rejected con reason_code BACEN (AC03, AB03, ED05, DUPL etc.) y reason_description en ingles.
  • Voids de orfanas (>30min sin PACS.002) disparan pix.payout.failed con reason_code: "orphan_force_voided".
  • Expiracion de la fila de retry (120min) dispara pix.payout.failed con reason_code: "DICT_QUEUE_TIMEOUT".

Tipos de Clave PIX

TipoFormatoEjemplo
cpf11 digitos (sin puntuacion)12345678901
cnpj14 digitos (sin puntuacion)12345678000199
emailDireccion de e-mailnome@empresa.com.br
phoneDDD + numero (11 digitos)11999998888
evpUUID v4a1b2c3d4-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:

  1. Use el endpoint Validacion CPF (POST /api/external/cpf/validate) para verificar si los 11 digitos forman un CPF valido
  2. Si valid: true → envie pix_key_type: "cpf" en el cash-out
  3. Si valid: false → es un telefono, envie pix_key_type: "phone" (la API agrega automaticamente el prefijo +55)
javascript
// 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:

O reciba la confirmacion automaticamente via Webhook.

Minha Konta Instituição de Pagamento - ISPB 39929224