Skip to content

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

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

SuperficieCuandostatus retornado
POST /api/external/pix/cash-inUsted genera el QR code"active"
Webhook pix.charge.createdOwem dispara al crear el QRbody status: "created"
Webhook pix.charge.paidPIX fue liquidado en la cuentabody 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 pagoConsulta por tx_id de QR aun no pagado"pending" / "expired" / "cancelled"

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

SuperficieCuandostatus retornado
POST /api/external/pix/cash-out (async, casi siempre)Request aceptado, enviado al SPIHTTP 202 "accepted"
POST /api/external/pix/cash-out (fast-track, raro)BACEN liquido antes de la respuesta volverHTTP 200 "settled"
Webhook pix.payout.processing (opcional, puede saltar)Mientras aguarda BACENbody status: "processing"
Webhook pix.payout.confirmedBACEN confirmo la liquidacionbody status: "settled" ← TERMINAL EXITO
Webhook pix.payout.failedSPI rechazo la transaccionbody status: "rejected" ← TERMINAL FALLA
Webhook pix.payout.returnedPIX enviado devuelto posteriormentebody 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": "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):

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-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 errorreason_codeEjemploCuando ocurre
BACEN/SPI (via PACS.002 RJCT)UPPERCASE 4 charsAC03, ED05, AM02, BE01, DUPLRechazo del BACEN despues que nuestra PACS.008 fue enviada
Provider (pre-BACEN)snake_case lowercasedict_key_not_found, dict_bucket_exhausted, dict_client_rate_limited, provider_schema_errorFalla en OnZ o bucket antes de llegar al BACEN
OtrosCamelCase_SNAKE cuando mixtoDICT_CLIENT_RATE_LIMITED, DICT_BUCKET_EXHAUSTED, DICT_RATE_LIMITEDCasos 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).

SuperficieCuandostatus retornado
Webhook pix.return.receivedUsted 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.

SuperficieCuandostatus retornado
POST /api/external/pix/refund (async)Request aceptadoHTTP 202 "accepted"
POST /api/external/pix/refund (fast-track)Liquidado sincronicamenteHTTP 200 "settled"
Webhook pix.refund.requestedBloqueo cautelar MED creado (disputa iniciada)body status: "requested"
Webhook pix.refund.completedDevolucion efectivada (MED flujo completo)body status: "completed"
Webhook pix.payout.returnedPIX 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/refund para 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).

AspectoComportamiento
DuracionIndefinida — puede ser minutos, horas o D+1
EscalacionEmails automaticos en 6h/24h/48h sin decision a compliance@owem.com.br
Resolucion automaticaSi 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 clienteEl webhook correspondiente (pix.payout.confirmed o pix.payout.failed) es disparado solo despues de la decision final — no hay webhook intermedio para cuarentena
Status intermedioPermanece "processing" en GET /transactions/:id y GET /transactions/ref/:external_id (shape 2) durante la cuarentena
SaldoPermanece 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:

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

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

Owem Pay Instituição de Pagamento — ISPB 37839059