PIX Lifecycle -- Vision Definitiva
Esta es la pagina autoritativa sobre el ciclo de vida de una transaccion PIX en Owem Pay. 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.paidconstatus: "paid" - PIX OUT (envio): webhook
pix.payout.confirmedconstatus: "settled"(exito) opix.payout.failedconstatus: "rejected"(falla)
Cualquier otro estado es intermedio. No acredite ni debite nada en su sistema antes de esos eventos.
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 | Owem 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" |
Ejemplo de webhook pix.charge.paid (payload real de produccion):
{
"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):
{
"event_type": "pix.payout.confirmed",
"status": "settled",
"account_id": 10011,
"amount": 500000,
"fee_amount": 250,
"description": "PIX Cash-Out",
"end_to_end_id": "E3783905920260411101530220db1672",
"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):
{
"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-OWEMPAY-1775664887942",
"end_to_end_id": "E3783905920260408161448ad70215f0",
"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"
}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 en OnZ o bucket 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: "requested" |
Webhook pix.refund.completed | Devolucion efectivada (MED flujo completo) | body status: "completed" |
Webhook pix.payout.returned | PIX OUT enviado fue devuelto via PACS.004 (D-prefix) | body status: "returned" |
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). Consulta de bloqueos cautelares en Listar MED.
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/refundpara MED — el backend ejecuta automaticamente. Ver Infracciones.
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"
│ │
│ │ Provider OnZ 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 Owem (que verifica MGMT OnZ + cabina Planner + 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@owem.com.br |
| 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. |
Esto sustituye el comportamiento anterior de "force_void despues de 30min" que causaba riesgo de perdida financiera (saldo restituido en el sistema Owem mientras BACEN efectivaba la transferencia en el destinatario).
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.
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:
- El
started_ates de hace mas de 30 min - Ningun webhook
pix.payout.confirmedopix.payout.failedfue recibido - 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).
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/:idbody:"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-Keycon un valor unico de su sistema. TTL de 24h. - Para webhooks recibidos: deduplique por el header
X-Owem-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 Owem.
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).
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 -- importe directamente en Postman
- Bruno Collection:
backend/bruno/external/en el repositorio - Ejemplos de payload: Webhook Payloads
- Autenticacion: API Key + HMAC
Resumen en una frase
Para cash-out, solo trate como final cuando reciba
pix.payout.confirmed(settled) opix.payout.failed(rejected). Para cash-in, solo trate como final cuando recibapix.charge.paid(paid). Nada mas alla de eso es terminal.