# PIX Cash-Out por Clave

Realiza una transferencia PIX utilizando la clave PIX del destinatario.

## Endpoint

```
POST /api/external/pix/cash-out
```

## Headers

| 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) |

::: tip Autenticacion
Vea [Autenticacion](/es/auth-token). La firma HMAC debe generarse conforme lo descrito en [HMAC-SHA512](/es/hmac).
:::

::: tip 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).
:::

::: danger 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](/es/auth-token#permisos-permissions).
:::

## 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 Monetarie (`46026562`)** - 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). |
| `recipient_document` | String | No* | CPF (11 digitos) o CNPJ (14 digitos) del destinatario; entrada formateada (puntos/guiones/barra) es aceptada y normalizada a digitos. **Opcional por default**; se vuelve **condicionalmente obligatorio** cuando el control "bloquear PIX-OUT a CNPJ" esta activado en la cuenta pagadora (vea abajo). Cuando la `pix_key` ya es un CPF/CNPJ, ella es autoritativa y este campo es prescindible. |

::: danger 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.**
:::

### Control opt-in: bloquear PIX-OUT a CNPJ

::: info Control opt-in - ningun impacto por default
Este es un control **opcional, por cuenta, desactivado por default**. Mientras este desactivado, `recipient_document` es ignorado y nada cambia - las integraciones existentes siguen funcionando sin ningun campo nuevo obligatorio. El control solo pasa a valer cuando el equipo de su cuenta lo activa en la cuenta pagadora.
:::

Cuando el control **esta activado** en la cuenta pagadora:

- El pago necesita tener un documento de destinatario **determinable**. Si la `pix_key` ya es un CPF/CNPJ, esa clave es autoritativa. En caso contrario (clave aleatoria/EVP, telefono o e-mail), usted **debe** enviar `recipient_document`.
- Si ningun documento valido de destinatario puede ser determinado → la solicitud es rechazada con `cnpj_out_document_required` (HTTP 400/422): "recipient document (CPF/CNPJ) is required when CNPJ-out block is enabled".
- Si el destinatario es un CNPJ que **no** esta en la lista de CNPJs autorizados de la cuenta → rechazada con `cnpj_out_blocked`.
- Un destinatario CPF, o un CNPJ presente en la lista autorizada, es permitido.

La lista de CNPJs autorizados es gestionada por el equipo de su cuenta. Vea [Controles de Cuenta](/es/account-controls).

## Ejemplo

```bash
curl -X POST https://api.monetarie.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": "E46026562202603091530abcdef01",
  "external_id": "order-9876",
  "amount": 300000,
  "fee_amount": 350,
  "net_amount": 300350,
  "status": "accepted",
  "detail": "PIX enviado para processamento"
}
```

::: tip 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).
:::

| 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](/es/pix-cashout-status#valores-del-campo-status) |
| `detail` | String | Mensaje descriptivo |

::: warning 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](/es/pix-cashout-status) o webhook `pix.payout.rejected`.

::: tip 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](/es/cpf-validation) 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 Monetarie (`46026562`). 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: "funds_under_med_hold"` | El saldo disponible está retenido por un mecanismo de devolución regulatorio (MED). Los fondos no pueden usarse en esta transferencia hasta que se resuelva el bloqueo cautelar. Distinto de `balance_corrupted` (que indica una divergencia de ledger) |
| 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 |
| 400/422 | A | `errors[0].code: "cnpj_out_document_required"` | Control **opt-in** "bloquear PIX-OUT a CNPJ" activado y ningun documento del destinatario pudo ser determinado (clave aleatoria/EVP/telefono/e-mail sin `recipient_document`). `errors[0].message`: "recipient document (CPF/CNPJ) is required when CNPJ-out block is enabled". Sin el control activado, este error nunca ocurre |
| 400/422 | A | `errors[0].code: "cnpj_out_blocked"` | Control **opt-in** "bloquear PIX-OUT a CNPJ" activado y el destinatario es un CNPJ que no esta en la lista de CNPJs autorizados de la cuenta. Sin el control activado, este error nunca ocurre |

::: danger 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_key_invalid` | Formato de clave PIX inválido (rechazado por el DICT) | Corrija la clave antes de reenviar; reintentar con la misma clave nunca funciona |
| `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 Monetarie |
| `provider_schema_error` | Payload de integracion incompatible | Reporte inmediatamente y no intente rehacer sin orientacion de Monetarie |
| `provider_unknown_error` | Status fuera de 400..499 que entro en este camino | Log completo disponible en soporte |

::: warning 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 Monetarie 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 Monetarie. |
| Limite DICT del proveedor | `DICT_BUCKET_EXHAUSTED` | Capacidad operacional de consultas DICT temporalmente indisponible. |

**Respuesta HTTP cuando encolado**:

```json
{
  "status": "queued",
  "type": "pix",
  "transaction_id": "PIXOUT20260309a1b2c3d4e5f6",
  "end_to_end_id": "E46026562202603091530abcdef01",
  "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**:

- Monetarie 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, Monetarie 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`.

::: tip 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](/es/hmac) |
| 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`) 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

| 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` |

::: danger 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](/es/cpf-validation) (`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:

- [Consultar por ID](/es/pix-cashout-status)
- [Consultar por E2E ID](/es/pix-cashout-e2e)
- [Consultar por Tag](/es/pix-cashout-tag)
- [Consultar por External ID](/es/pix-query-external-id) -- `GET /api/external/transactions/ref/{external_id}`

O reciba la confirmacion automaticamente via [Webhook](/es/webhooks).
