# PIX Cash-Out por Chave

Realiza uma transferência PIX utilizando a chave PIX do destinatário.

## Endpoint

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

## Headers

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

::: tip Autenticação
Veja [Autenticação](/auth-token). A assinatura HMAC deve ser gerada conforme descrito em [HMAC-SHA512](/hmac).
:::

::: tip 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: true` e ecoa o `Idempotency-Key` enviado.
- 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_id` determinístico ainda garante idempotência na camada BACEN/SPI, mas pode gerar rejeição com `failure_reason: "DUPL"` se a primeira tentativa já tiver sido liquidada).
:::

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

## 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 Monetarie (`46026562`)** - 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). |
| `recipient_document` | String | Não* | CPF (11 dígitos) ou CNPJ (14 dígitos) do destinatário; entrada formatada (pontos/traços/barra) é aceita e normalizada para dígitos. **Opcional por padrão**; torna-se **condicionalmente obrigatório** quando o controle "bloquear PIX-OUT para CNPJ" está ativado na conta pagadora (veja abaixo). Quando a `pix_key` já é um CPF/CNPJ, ela é autoritativa e este campo é dispensável. |

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

### Controle opt-in: bloquear PIX-OUT para CNPJ

::: info Controle opt-in - nenhum impacto por padrão
Este é um controle **opcional, por conta, desativado por padrão**. Enquanto estiver desativado, `recipient_document` é ignorado e nada muda - integrações existentes continuam funcionando sem nenhum campo novo obrigatório. O controle só passa a valer quando o time da sua conta o ativa na conta pagadora.
:::

Quando o controle **está ativado** na conta pagadora:

- O pagamento precisa ter um documento de destinatário **determinável**. Se a `pix_key` já é um CPF/CNPJ, essa chave é autoritativa. Caso contrário (chave aleatória/EVP, telefone ou e-mail), você **deve** enviar `recipient_document`.
- Se nenhum documento válido de destinatário puder ser determinado → a requisição é rejeitada com `cnpj_out_document_required` (HTTP 400/422): "recipient document (CPF/CNPJ) is required when CNPJ-out block is enabled".
- Se o destinatário for um CNPJ que **não** está na lista de CNPJs autorizados da conta → rejeitada com `cnpj_out_blocked`.
- Um destinatário CPF, ou um CNPJ presente na lista autorizada, é permitido.

A lista de CNPJs autorizados é gerida pelo time da sua conta. Veja [Controles de Conta](/account-controls).

## Exemplo

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

## Resposta de Sucesso -- 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**: Transação já liquidada (`final: true`, `status: "settled"`).
- **HTTP 202**: Transação aceita para processamento (`final: false`). Acompanhe o status via polling ou webhook. `status` pode 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](/pix-cashout-status#valores-do-campo-status) |
| `detail` | String | Mensagem descritiva |

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

::: tip 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](/cpf-validation) 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 Monetarie (`46026562`). 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: "funds_under_med_hold"` | Saldo disponível está retido por um mecanismo de devolução regulatório (MED). Os fundos não podem ser usados nesta transferência até a resolução do bloqueio cautelar. Distinto de `balance_corrupted` (que sinaliza divergência de ledger) |
| 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 |
| 400/422 | A | `errors[0].code: "cnpj_out_document_required"` | Controle **opt-in** "bloquear PIX-OUT para CNPJ" ativado e nenhum documento do destinatário pôde ser determinado (chave aleatória/EVP/telefone/e-mail sem `recipient_document`). `errors[0].message`: "recipient document (CPF/CNPJ) is required when CNPJ-out block is enabled". Sem o controle ativado, este erro nunca ocorre |
| 400/422 | A | `errors[0].code: "cnpj_out_blocked"` | Controle **opt-in** "bloquear PIX-OUT para CNPJ" ativado e o destinatário é um CNPJ que não está na lista de CNPJs autorizados da conta. Sem o controle ativado, este erro nunca ocorre |

::: danger 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_key_invalid` | Formato da chave PIX inválido (rejeitado pelo DICT) | Corrija a chave antes de reenviar; retry com a mesma chave nunca resolve |
| `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 Monetarie |
| `provider_schema_error` | Payload de integração incompatível | Reporte imediatamente e não tente refazer sem orientação da Monetarie |
| `provider_unknown_error` | Status fora de 400..499 que entrou neste caminho | Log completo disponível no suporte |

::: warning 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 Monetarie 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 Monetarie. |
| Limite DICT do provedor | `DICT_BUCKET_EXHAUSTED` | Limite operacional de consultas DICT temporariamente indisponível. |

**Resposta HTTP quando enfileirado**:

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

**Mecânica do retry**:

- A Monetarie 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.failed` com `reason_code: "DICT_QUEUE_TIMEOUT"`.
- **Webhook imediato**: ao entrar na fila, a Monetarie dispara `pix.payout.queued` com `reason_code` (`DICT_CLIENT_RATE_LIMITED` ou `DICT_BUCKET_EXHAUSTED`) e `reason_description`. O próximo evento será `pix.payout.confirmed` ou `pix.payout.failed`.

::: tip 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](/hmac) |
| 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`) dispara `pix.payout.queued` **imediatamente** com `reason_code` + `reason_description`.
- Rejeições assíncronas (PACS.002 RJCT após aceite 202) disparam `pix.payout.rejected` com `reason_code` BACEN (AC03, AB03, ED05, DUPL etc.) e `reason_description` em inglês.
- Voids de orfãs (>30min sem PACS.002) disparam `pix.payout.failed` com `reason_code: "orphan_force_voided"`.
- Expiração da fila de retry (120min) dispara `pix.payout.failed` com `reason_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` |

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

1. Use o endpoint [Validação CPF](/cpf-validation) (`POST /api/external/cpf/validate`) para verificar se os 11 dígitos formam um CPF válido
2. Se `valid: true` → envie `pix_key_type: "cpf"` no cash-out
3. Se `valid: false` → é um telefone, envie `pix_key_type: "phone"` (a API adiciona automaticamente o prefixo `+55`)

```javascript
// 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](/pix-cashout-status)
- [Consultar por E2E ID](/pix-cashout-e2e)
- [Consultar por Tag](/pix-cashout-tag)
- [Consultar por External ID](/guide/concepts#external-id) -- `GET /api/external/transactions/ref/{external_id}`

Ou receba a confirmação automaticamente via [Webhook](/webhooks).
