# PIX Lifecycle -- Vision Definitiva

Esta es la pagina autoritativa sobre el ciclo de vida de una transaccion PIX en Monetarie. Si tiene dudas sobre lo que significa cada status, vuelva aqui.

## TL;DR -- lo unico que necesita saber

Para saber si una transaccion es **FINAL**, espere uno de estos eventos:

- **PIX IN (recibimiento):** webhook `pix.charge.paid` con `status: "paid"`
- **PIX OUT (envio):** webhook `pix.payout.confirmed` con `status: "settled"` **(exito)** o `pix.payout.failed` con `status: "rejected"` **(falla)**

Cualquier otro estado es intermedio. **No acredite ni debite nada en su sistema antes de esos eventos.**

::: danger Regla de oro
El unico vocabulario que importa es el del webhook. La API GET retorna exactamente los mismos valores - no hay traduccion entre webhook y GET.
:::

---

## Matriz de Status -- fuente unica de verdad

Los mismos valores de status aparecen en tres superficies: el body de la respuesta POST, el body del webhook, y el body de la respuesta GET `/transactions/:id`. No existe traduccion entre ellos.

### PIX IN (Cash-In) -- recibir un PIX

| Superficie | Cuando | `status` retornado |
|------------|--------|--------------------|
| POST `/api/external/pix/cash-in` | Usted genera el QR code | `"active"` |
| Webhook `pix.charge.created` | Monetarie dispara al crear el QR | body `status: "created"` |
| Webhook `pix.charge.paid` | PIX fue liquidado en la cuenta | body `status: "paid"` **← TERMINAL** |
| GET `/api/external/transactions/:id` (despues del pago) | Consulta por `tx_id` del QR o `transaction_id` | `"settled"` ← lo mismo que `paid`, solo cambia la superficie |
| GET antes del pago | Consulta por `tx_id` de QR aun no pagado | `"pending"` / `"expired"` / `"cancelled"` |

::: info Controles opt-in de recepcion - ningun impacto por default
Si la cuenta receptora activo un control **opt-in** ("verificacion de pagador esperado" o "bloquear recepcion de CNPJ"), un PIX que no cumpla el criterio es **rechazado y devuelto al pagador** (devolucion BACEN) en vez de acreditado, en vez de avanzar a `pix.charge.paid`. Estos controles vienen **desactivados por default** - ninguna integracion existente es afectada hasta que el titular de la cuenta los active. Vea [Controles de Cuenta](/es/account-controls).
:::

**Ejemplo de webhook `pix.charge.paid` (payload real de produccion):**

```json
{
  "event_type": "pix.charge.paid",
  "status": "paid",
  "account_id": 10011,
  "amount": 100000,
  "fee_amount": 250,
  "counterparty_name": "Marcia Cristiane Ribeiro Barbosa",
  "end_to_end_id": "E165015552026041016069d8b4c6b2fc",
  "external_id": "T2604101306qtsfffH",
  "paid_at": "2026-04-10T16:07:08.158762Z",
  "payer_bank_name": "STONE IP S.A.",
  "payer_document": "20018216897",
  "payer_ispb": "16501555",
  "qr_code_id": "e9f3df72-031f-49bf-abc3-a9ce1d540726",
  "tx_id": "smyoka2zd5xowvqq2hea"
}
```

### PIX OUT (Cash-Out) -- enviar un PIX

| Superficie | Cuando | `status` retornado |
|------------|--------|--------------------|
| POST `/api/external/pix/cash-out` (async, casi siempre) | Request aceptado, enviado al SPI | HTTP 202 `"accepted"` |
| POST `/api/external/pix/cash-out` (fast-track, raro) | BACEN liquido antes de la respuesta volver | HTTP 200 `"settled"` |
| Webhook `pix.payout.processing` (opcional, puede saltar) | Mientras aguarda BACEN | body `status: "processing"` |
| Webhook `pix.payout.confirmed` | BACEN confirmo la liquidacion | body `status: "settled"` **← TERMINAL EXITO** |
| Webhook `pix.payout.failed` | SPI rechazo la transaccion | body `status: "rejected"` **← TERMINAL FALLA** |
| Webhook `pix.payout.returned` | PIX enviado devuelto posteriormente | body `status: "returned"` |
| GET `/api/external/transactions/:id` (transaccion en vuelo) | Mientras la transaccion no fue liquidada | `"processing"` |
| GET `/api/external/transactions/:id` (liquidada) | Despues de `pix.payout.confirmed` | `"settled"` |
| GET `/api/external/transactions/:id` (fallo) | Despues de `pix.payout.failed` | `"failed"` |

**Ejemplo de webhook `pix.payout.confirmed` (payload real de produccion):**

```json
{
  "event_type": "pix.payout.confirmed",
  "status": "settled",
  "account_id": 10011,
  "amount": 500000,
  "fee_amount": 250,
  "description": "PIX Cash-Out",
  "end_to_end_id": "E4602656220260411101530220db1672",
  "external_id": "T2604110715qx55o7E",
  "pix_key": "08389612747",
  "initiated_at": "2026-04-11T10:15:31.141953Z",
  "recipient": {
    "name": "Claudio Portugal Wanderley",
    "document": "08389612747",
    "account": "67469312",
    "agency": "1",
    "ispb": "18236120",
    "institution_name": "NU PAGAMENTOS - IP"
  },
  "transaction_id": "PIXOUT8813809cc536884c83056900088b"
}
```

**Ejemplo de webhook `pix.payout.failed` (payload real de produccion):**

```json
{
  "event_type": "pix.payout.failed",
  "status": "rejected",
  "account_id": 10016,
  "amount": 80000000,
  "fee_amount": 350,
  "reason": "rejected",
  "reason_code": "AC03",
  "reason_description": "Invalid creditor account number",
  "description": "tx-MONETARIE-1775664887942",
  "end_to_end_id": "E4602656220260408161448ad70215f0",
  "pix_key": "23bb00c0-9b4a-48f5-b62a-03546beb858f",
  "recipient": {
    "name": null,
    "document": null,
    "ispb": null,
    "institution_name": null
  },
  "initiated_at": "2026-04-08T16:14:48.213978Z",
  "transaction_id": "PIXOUT00350c7c85c0b54e83056900e009"
}
```

::: tip reason_code estructurado (BACEN UPPERCASE vs provider snake_case)
Los campos `reason_code` y `reason_description` tienen **dos convenciones** coexistiendo - no es inconsistencia, es reflejo de la fuente del error:

| Origen del error | `reason_code` | Ejemplo | Cuando ocurre |
|---|---|---|---|
| **BACEN/SPI** (via PACS.002 RJCT) | UPPERCASE 4 chars | `AC03`, `ED05`, `AM02`, `BE01`, `DUPL` | Rechazo del BACEN despues que nuestra PACS.008 fue enviada |
| **Provider** (pre-BACEN) | snake_case lowercase | `dict_key_not_found`, `dict_bucket_exhausted`, `dict_client_rate_limited`, `provider_schema_error` | Falla del provider o limite DICT antes de llegar al BACEN |
| **Otros** | CamelCase_SNAKE cuando mixto | `DICT_CLIENT_RATE_LIMITED`, `DICT_BUCKET_EXHAUSTED`, `DICT_RATE_LIMITED` | Casos especificos de retry queue (`pix.payout.queued`) |

`reason_description` viene en **ingles** por defecto (ej: `"Invalid creditor account number"` para `AC03`). Para clasificar reintentos: match exacto en codigo + `direction=outbound` + tabla de retry deterministica. No hacer case-insensitive match entre BACEN y provider - las dos convenciones son distintas por diseno.

El campo legacy `reason` (string) solo aparece cuando el backend **no logra** extraer un codigo BACEN estructurado; cuando `reason_code` viene llenado, `reason` es omitido (exclusion mutua, sesion 141+163).
:::

### Refund (devoluciones)

Existen dos escenarios distintos de devolucion. Preste atencion a la direccion.

**Escenario A -- usted RECIBIO una devolucion (inbound refund)**

Otra institucion devolvio un PIX que usted **habia recibido** (ej.: pagador envio PIX de mas, solicito devolucion parcial/total, y usted, como receptor original, recibe esa devolucion de vuelta como un credito).

| Superficie | Cuando | `status` retornado |
|------------|--------|--------------------|
| Webhook `pix.return.received` | Usted recibio un PIX de vuelta (credito en su cuenta) | body `status: "settled"` |

**Escenario B -- usted ENVIO una devolucion (outbound refund)**

Usted inicio una devolucion via `POST /api/external/pix/refund` (generalmente MED) O recibio un PIX y devolvio via PACS.004 manualmente.

| Superficie | Cuando | `status` retornado |
|------------|--------|--------------------|
| POST `/api/external/pix/refund` (async) | Request aceptado | HTTP 202 `"accepted"` |
| POST `/api/external/pix/refund` (fast-track) | Liquidado sincronicamente | HTTP 200 `"settled"` |
| Webhook `pix.refund.requested` | Bloqueo cautelar MED creado (disputa iniciada) | body `status: "pending"` |
| Webhook `pix.refund.completed` | Devolucion efectivada (MED flujo completo) | body `status: "settled"` |
| Webhook `pix.payout.returned` | PIX OUT enviado fue devuelto via PACS.004 (D-prefix) | body `status: "returned"` |

::: tip Eventos de refund disparados automaticamente
Los eventos `pix.refund.requested` y `pix.refund.completed` **SON disparados automaticamente** por el backend a partir de abril/2026. `pix.refund.requested` dispara al crear bloqueo cautelar; `pix.refund.completed` dispara al efectivar la devolucion. Polling en `GET /med/:id` continua funcionando como alternativa.

Flujo completo de infraccion → bloqueo → devolucion en [Infracciones (flujo)](/es/infractions). Consulta de bloqueos cautelares en [Listar MED](/es/med-list).
:::

::: warning Refund voluntario vs MED
- **POST /api/external/pix/refund** (este flujo Escenario B): devolucion **voluntaria** iniciada por usted. E2E de la devolucion tiene prefijo `D`.
- **MED (Mecanismo Especial de Devolucion)**: devolucion **regulatoria** ejecutada por el sistema cuando una infraccion BACEN es aceptada (`analysis_result=AGREED`). **No llame `/pix/refund` para MED** - el backend ejecuta automaticamente. Ver [Infracciones](/es/infractions).
:::

---

## Flujogramas

### PIX IN -- recibimiento

```
POST /api/external/pix/cash-in
       │
       │ Respuesta: status "active", transaction_id (tx_id del QR)
       ▼
[Webhook] pix.charge.created       ← status "created"
       │
       │ Tiempo indeterminado (hasta que el pagador pague el QR)
       ▼
[Pagador paga el QR externamente]
       │
       │ BACEN liquida (<2s)
       ▼
[Webhook] pix.charge.paid          ← status "paid" (TERMINAL)
       │
       ▼
GET /api/external/transactions/:id retorna status "settled"
```

### PIX OUT -- envio

```
POST /api/external/pix/cash-out
       │
       ├─ Camino A (99% de los casos): HTTP 202 + status "accepted"
       │        │
       │        │ Proveedor envio PACS.008 al SPI/BACEN
       │        ▼
       │   [Webhook] pix.payout.processing (opcional, puede saltar)
       │        │          ← body status "processing"
       │        │ BACEN responde (1.6-2s tipico)
       │        │
       │        ├─ Exito  → [Webhook] pix.payout.confirmed
       │        │                    ← body status "settled" (TERMINAL EXITO)
       │        │                    ← GET retorna "settled"
       │        │
       │        └─ Falla  → [Webhook] pix.payout.failed
       │                             ← body status "rejected" (TERMINAL FALLA)
       │                             ← GET retorna "failed"
       │
       └─ Camino B (raro, fast-track): HTTP 200 + status "settled" (ya terminal)
```

---

## Cuarentena (operaciones sin respuesta BACEN)

Si un PIX OUT queda >30min en estado `processing` sin confirmacion/rechazo del BACEN, el sistema mueve la operacion a **cuarentena** (stage=5) en vez de forzar void automatico. El saldo del cliente **permanece bloqueado** hasta decision manual del piloto de la reserva Monetarie (que verifica mesa operacional Monetarie y cuenta de liquidacion).

| Aspecto | Comportamiento |
|---|---|
| **Duracion** | Indefinida - puede ser minutos, horas o D+1 |
| **Escalacion** | Emails automaticos en 6h/24h/48h sin decision a compliance@monetarie.com |
| **Resolucion automatica** | Si el BACEN responde tardiamente (PACS.002 via long-polling), la operacion es resuelta sin intervencion manual - el piloto es notificado de la resolucion retroactiva |
| **Visibilidad para el cliente** | El webhook correspondiente (`pix.payout.confirmed` o `pix.payout.failed`) es disparado **solo despues de la decision final** - no hay webhook intermedio para cuarentena |
| **Status intermedio** | Permanece `"processing"` en `GET /transactions/:id` y `GET /transactions/ref/:external_id` (shape 2) durante la cuarentena |
| **Saldo** | Permanece en `pending` (descontado de `available`, preservado en `balance`). Ver [Saldo](/es/bank-balance). |

Esto sustituye el comportamiento anterior de "force_void despues de 30min" que causaba riesgo de perdida financiera (saldo restituido en el sistema Monetarie mientras BACEN efectivaba la transferencia en el destinatario).

::: info Cuarentena vs incidente
Si usted ve un PIX OUT en `processing` por mas de 30min, **no es error del sistema** - es cuarentena aguardando validacion manual. El webhook final sera disparado cuando resuelto. Contacte al soporte solo si pasan 48h sin resolucion o si usted identifico que el pago cayo en el BACEN pero no recibio webhook.
:::

::: warning Como detectar cuarentena via API
En los endpoints de consulta (`GET /transactions/:id`, `GET /transactions/ref/:external_id` shape 2, `GET /statement` con `status=processing`), **no hay campo especifico** que diferencie "cuarentena" de "procesamiento normal" - ambos aparecen con `status="processing"` y `payment_status="processing"`. Para identificar cuarentena:

1. El `started_at` es de hace mas de 30 min
2. Ningun webhook `pix.payout.confirmed` o `pix.payout.failed` fue recibido
3. La transaccion permanece en ese estado por >1h

Si esos 3 senales se confirman, es muy probable cuarentena - aguarde o contacte al soporte. **Nunca reenvie** la misma solicitud (genera duplicidad cuando la original liquide retroactivamente).
:::

::: warning Reintento de PIX OUT en cuarentena
**NUNCA reenvie un PIX OUT que esta en cuarentena.** La transaccion original puede liquidar en cualquier momento. Reenviar genera duplicidad. Aguarde la resolucion manual o automatica - usted sera notificado via webhook cuando resuelto.
:::

---

## FAQ

### Por que el POST retorna `accepted` pero el webhook retorna `settled`?

Son fases distintas. POST = "recibimos su solicitud y la colocamos en fila". Webhook `confirmed` = "BACEN confirmo la liquidacion". En Brasil, la liquidacion es rapida (~1.6 segundos), pero aun es asincronica - el cliente HTTP que llamo el POST ya recibio la respuesta antes del BACEN responder.

### `accepted` significa que el dinero fue debitado definitivamente?

**No.** Significa que el dinero esta **en hold** - reservado, pero aun puede ser liberado si el SPI rechaza. Dinero solo sale definitivamente cuando usted recibe `pix.payout.confirmed` (o GET retorna `settled`).

### Cual es la diferencia entre `failed` y `rejected`?

Ninguna. Son el mismo estado visto desde superficies diferentes:

- Webhook body: `"rejected"`
- GET `/transactions/:id` body: `"failed"`

Ambos significan: la transaccion fue rechazada por el SPI, el hold fue liberado, y el saldo fue restaurado. **No acredite el pago.**

### El documento antiguo decia `completed`. Aun existe?

En la practica, no. La palabra `completed` aparecia en ejemplos antiguos y era documentacion ficticia - corregida en 2026-04-12. Para todas las transacciones en produccion, el campo retorna `"settled"`.

Existe aun un fallback teorico en el codigo (`helpers.ex:127`): si una row de `transactions` tiene `status=1` (approved) **Y** `payment_status IS NULL`, el backend devuelve `"completed"`. En la practica, el pipeline TbFirst **siempre** llena `payment_status` en PIX IN/OUT exitosos - por lo tanto ese fallback nunca es activado en trafico real. Si aun asi su integracion observa `status="completed"`, tratelo como equivalente a `settled` (rollback seguro) y reporte al soporte para investigar la row divergente.

### Que hacer cuando recibo `pix.payout.processing`?

**Nada.** Es solo un aviso. Saldo esta en hold. Espere el proximo evento: `pix.payout.confirmed` (exito) o `pix.payout.failed` (falla).

### Y si necesito aprobacion antes de enviar?

El endpoint `/pix/cash-out/approve` no existe hoy. El flujo de envio es una unica llamada POST `/pix/cash-out` que inmediatamente va al SPI. Si usted quiere aprobacion manual, implemente eso en su lado antes de llamar la API.

### Como garantizar idempotencia?

- **Para POST cash-out/cash-in/refund:** use el header `Idempotency-Key` con un valor unico de su sistema. TTL de 24h.
- **Para webhooks recibidos:** deduplique por el header `X-Monetarie-Event-Id`. El mismo evento puede ser re-entregado hasta 8 veces en caso de falla HTTP en su endpoint.

### Y el `external_id`?

Campo opcional (max 128 chars) que usted define en cada POST. Es retornado en todas las respuestas y webhooks, permitiendo consulta inversa via `GET /api/external/transactions/ref/:external_id`. Use para correlacionar pedidos de su sistema con transacciones de Monetarie.

### Un PIX que recibi quedo "impugnado" - que es?

Una **infraccion PIX** fue abierta por la institucion del pagador (via BACEN DICT). Dependiendo del valor y del E2E, puede generar un bloqueo cautelar del saldo hasta la resolucion. Ver flujo completo en [Infracciones (flujo)](/es/infractions).

### Puedo recibir una devolucion sin haber enviado? (inbound refund)

Si. Si usted recibio un PIX, la contraparte puede iniciar una devolucion unilateral (PACS.004 D-prefix) - usted recibe el evento `pix.return.received` con `status="settled"` y un credito de vuelta. Es diferente de una infraccion (que es una **impugnacion** formal BACEN, no una devolucion directa).

---

## Integraciones rapidas

- **Postman Collection v2.1:** [Download](/monetarie-postman-collection.json) -- importe directamente en Postman
- **Ejemplos de payload:** [Webhook Payloads](/es/webhooks-payloads)
- **Autenticacion:** [API Key + HMAC](/es/auth-token)

---

## Resumen en una frase

> Para cash-out, solo trate como final cuando reciba **`pix.payout.confirmed`** (`settled`) o **`pix.payout.failed`** (`rejected`). Para cash-in, solo trate como final cuando reciba **`pix.charge.paid`** (`paid`). Nada mas alla de eso es terminal.
