PIX Lifecycle -- Visão Definitiva
Esta é a página autoritativa sobre o ciclo de vida de uma transação PIX na Owem Pay. Se você tiver dúvidas sobre qual status significa o quê, volte aqui.
TL;DR -- a única coisa que você precisa saber
Para saber se uma transação está FINAL, espere um destes eventos:
- PIX IN (recebimento): webhook
pix.charge.paidcomstatus: "paid" - PIX OUT (envio): webhook
pix.payout.confirmedcomstatus: "settled"(sucesso) oupix.payout.failedcomstatus: "rejected"(falha)
Qualquer outro estado é intermediário. Não credite nem debite nada no seu sistema antes desses eventos.
Regra de ouro
O único vocabulário que importa é o do webhook. A API GET retorna exatamente os mesmos valores — não há tradução entre webhook e GET.
Matriz de Status -- fonte única de verdade
Os mesmos valores de status aparecem em três superfícies: o body da resposta POST, o body do webhook, e o body da resposta GET /transactions/:id. Não existe tradução entre eles.
PIX IN (Cash-In) -- receber um PIX
| Superfície | Quando | status retornado |
|---|---|---|
POST /api/external/pix/cash-in | Você gera o QR code | "active" |
Webhook pix.charge.created | Owem dispara ao criar o QR | body status: "created" |
Webhook pix.charge.paid | PIX foi liquidado na conta | body status: "paid" ← TERMINAL |
GET /api/external/transactions/:id (após pagamento) | Consulta ao tx_id do QR ou ao transaction_id | "settled" ← mesma coisa que paid, só muda a superfície |
| GET antes do pagamento | Consulta por tx_id de QR ainda não pago | "pending" / "expired" / "cancelled" |
Exemplo de webhook pix.charge.paid (payload real de produção):
{
"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 um PIX
| Superfície | Quando | status retornado |
|---|---|---|
POST /api/external/pix/cash-out (async, quase sempre) | Request aceito, enviado ao SPI | HTTP 202 "accepted" |
POST /api/external/pix/cash-out (fast-track, raro) | BACEN liquidou antes da resposta voltar | HTTP 200 "settled" |
Webhook pix.payout.processing (opcional, pode pular) | Enquanto espera BACEN | body status: "processing" |
Webhook pix.payout.confirmed | BACEN confirmou a liquidação | body status: "settled" ← TERMINAL SUCESSO |
Webhook pix.payout.failed | SPI rejeitou a transação | body status: "rejected" ← TERMINAL FALHA |
Webhook pix.payout.returned | PIX enviado e devolvido posteriormente | body status: "returned" |
GET /api/external/transactions/:id (transação em voo) | Enquanto a transação não foi liquidada | "processing" |
GET /api/external/transactions/:id (liquidada) | Depois de pix.payout.confirmed | "settled" |
GET /api/external/transactions/:id (falhou) | Depois de pix.payout.failed | "failed" |
Exemplo de webhook pix.payout.confirmed (payload real de produção):
{
"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"
}Exemplo de webhook pix.payout.failed (payload real de produção):
{
"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 estruturado (BACEN UPPERCASE vs provider snake_case)
Os campos reason_code e reason_description têm duas convenções coexistindo — não é inconsistência, é reflexo da fonte do erro:
| Origem do erro | reason_code | Exemplo | Quando ocorre |
|---|---|---|---|
| BACEN/SPI (via PACS.002 RJCT) | UPPERCASE 4 chars | AC03, ED05, AM02, BE01, DUPL | Rejeição do BACEN após nossa PACS.008 ser enviada |
| Provider (pré-BACEN) | snake_case lowercase | dict_key_not_found, dict_bucket_exhausted, dict_client_rate_limited, provider_schema_error | Falha no OnZ ou bucket antes de chegar no BACEN |
| Outros | CamelCase_SNAKE quando misto | DICT_CLIENT_RATE_LIMITED, DICT_BUCKET_EXHAUSTED, DICT_RATE_LIMITED | Casos específicos de retry queue (pix.payout.queued) |
reason_description vem em inglês por padrão (ex: "Invalid creditor account number" para AC03). Para classificar retentativas: match exato em código + direction=outbound + tabela de retry determinística. Não fazer case-insensitive match entre BACEN e provider — as duas convenções são distintas por design.
O campo legacy reason (string) só aparece quando o backend não consegue extrair um código BACEN estruturado; quando reason_code vem preenchido, reason é omitido (exclusão mútua, session 141+163).
Refund (devoluções)
Existem dois cenários distintos de devolução. Preste atenção na direção.
Cenário A -- você RECEBEU uma devolução (inbound refund)
Outra instituição devolveu um PIX que você havia recebido (ex.: pagador enviou PIX a mais, solicitou devolução parcial/total, e você, como recebedor original, recebe essa devolução de volta como um crédito).
| Superfície | Quando | status retornado |
|---|---|---|
Webhook pix.return.received | Você recebeu um PIX de volta (crédito na sua conta) | body status: "settled" |
Cenário B -- você ENVIOU uma devolução (outbound refund)
Você iniciou uma devolução via POST /api/external/pix/refund (geralmente MED) OU recebeu um PIX e devolveu via PACS.004 manualmente.
| Superfície | Quando | status retornado |
|---|---|---|
POST /api/external/pix/refund (async) | Request aceito | HTTP 202 "accepted" |
POST /api/external/pix/refund (fast-track) | Liquidado sincronamente | HTTP 200 "settled" |
Webhook pix.refund.requested | Bloqueio cautelar MED criado (disputa iniciada) | body status: "requested" |
Webhook pix.refund.completed | Devolução efetivada (MED fluxo completo) | body status: "completed" |
Webhook pix.payout.returned | PIX OUT enviado foi devolvido via PACS.004 (D-prefix) | body status: "returned" |
Eventos de refund disparados automaticamente
Os eventos pix.refund.requested e pix.refund.completed SÃO disparados automaticamente pelo backend a partir de abril/2026. pix.refund.requested dispara ao criar bloqueio cautelar; pix.refund.completed dispara ao efetivar a devolução. Polling em GET /med/:id continua funcionando como alternativa.
Fluxo completo de infração → bloqueio → devolução em Infrações (fluxo). Consulta de bloqueios cautelares em Listar MED.
Refund voluntário vs MED
- POST /api/external/pix/refund (este fluxo Cenário B): devolução voluntária iniciada por você. E2E da devolução tem prefixo
D. - MED (Mecanismo Especial de Devolução): devolução regulatória executada pelo sistema quando uma infração BACEN é aceita (
analysis_result=AGREED). Não chame/pix/refundpara MED — o backend executa automaticamente. Ver Infrações.
Fluxogramas
PIX IN -- recebimento
POST /api/external/pix/cash-in
│
│ Resposta: status "active", transaction_id (tx_id do QR)
▼
[Webhook] pix.charge.created ← status "created"
│
│ Tempo indeterminado (até o pagador pagar o QR)
▼
[Pagador paga o 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
│
├─ Caminho A (99% dos casos): HTTP 202 + status "accepted"
│ │
│ │ Provider OnZ enviou PACS.008 ao SPI/BACEN
│ ▼
│ [Webhook] pix.payout.processing (opcional, pode pular)
│ │ ← body status "processing"
│ │ BACEN responde (1.6-2s típico)
│ │
│ ├─ Sucesso → [Webhook] pix.payout.confirmed
│ │ ← body status "settled" (TERMINAL SUCESSO)
│ │ ← GET retorna "settled"
│ │
│ └─ Falha → [Webhook] pix.payout.failed
│ ← body status "rejected" (TERMINAL FALHA)
│ ← GET retorna "failed"
│
└─ Caminho B (raro, fast-track): HTTP 200 + status "settled" (já terminal)Quarentena (operações sem resposta BACEN)
Se uma PIX OUT ficar >30min em estado processing sem confirmação/rejeição do BACEN, o sistema move a operação para quarentena (stage=5) em vez de forçar void automático. O saldo do cliente permanece bloqueado até decisão manual do piloto da reserva Owem (que verifica MGMT OnZ + cabine Planner + conta de liquidação).
| Aspecto | Comportamento |
|---|---|
| Duração | Indefinida — pode ser minutos, horas ou D+1 |
| Escalação | Emails automáticos em 6h/24h/48h sem decisão para compliance@owem.com.br |
| Resolução automática | Se o BACEN responder tardiamente (PACS.002 via long-polling), a operação é resolvida sem intervenção manual — o piloto é notificado da resolução retroativa |
| Visibilidade para o cliente | O webhook correspondente (pix.payout.confirmed ou pix.payout.failed) é disparado apenas após a decisão final — não há webhook intermediário para quarentena |
| Status intermediário | Permanece "processing" em GET /transactions/:id e GET /transactions/ref/:external_id (shape 2) durante a quarentena |
| Saldo | Permanece em pending (descontado de available, preservado em balance). Ver Saldo. |
Isso substitui o comportamento anterior de "force_void após 30min" que causava risco de perda financeira (saldo restituído no sistema Owem enquanto o BACEN efetivava a transferência na ponta do destinatário).
Quarentena vs incidente
Se você vê um PIX OUT em processing por mais de 30min, não é erro do sistema — é quarentena aguardando validação manual. O webhook final será disparado quando resolvido. Contate o suporte apenas se passar 48h sem resolução ou se você identificou que o pagamento caiu no BACEN mas não recebeu webhook.
Como detectar quarentena via API
Nos endpoints de consulta (GET /transactions/:id, GET /transactions/ref/:external_id shape 2, GET /statement com status=processing), não há campo específico que diferencie "quarentena" de "processamento normal" — ambos aparecem com status="processing" e payment_status="processing". Para identificar quarentena:
- O
started_até de mais de 30 min atrás - Nenhum webhook
pix.payout.confirmedoupix.payout.failedfoi recebido - A transação permanece nesse estado por >1h
Se esses 3 sinais se confirmam, é muito provável quarentena — aguarde ou contate o suporte. Nunca reenvie a mesma requisição (gera duplicidade quando a original liquidar retroativamente).
Retentativa de PIX OUT em quarentena
NUNCA reenvie um PIX OUT que está em quarentena. A transação original pode liquidar a qualquer momento. Reenviar gera duplicidade. Aguarde a resolução manual ou automática — você será notificado via webhook quando resolvido.
FAQ
Por que o POST retorna accepted mas o webhook retorna settled?
São fases distintas. POST = "recebemos seu pedido e colocamos na fila". Webhook confirmed = "BACEN confirmou a liquidação". No Brasil, a liquidação é rápida (~1.6 segundos), mas ainda é assíncrona — o cliente HTTP que chamou o POST já recebeu a resposta antes do BACEN responder.
accepted significa que o dinheiro foi debitado definitivamente?
Não. Significa que o dinheiro está em hold — reservado, mas ainda pode ser liberado se o SPI rejeitar. Dinheiro só sai definitivamente quando você recebe pix.payout.confirmed (ou GET retorna settled).
Qual é a diferença entre failed e rejected?
Nenhuma. São o mesmo estado visto de superfícies diferentes:
- Webhook body:
"rejected" - GET
/transactions/:idbody:"failed"
Ambos significam: a transação foi rejeitada pelo SPI, o hold foi liberado, e o saldo foi restaurado. Não credite o pagamento.
O documento antigo dizia completed. Ainda existe?
Na prática, não. A palavra completed aparecia em exemplos antigos e era documentação fictícia — corrigida em 2026-04-12. Para todas as transações em produção, o campo retorna "settled".
Existe ainda um fallback teórico no código (helpers.ex:127): se uma row de transactions tem status=1 (approved) E payment_status IS NULL, o backend devolve "completed". Na prática, o pipeline TbFirst sempre preenche payment_status em PIX IN/OUT bem-sucedidos — portanto esse fallback nunca é acionado em tráfego real. Se mesmo assim sua integração observar status="completed", trate-o como equivalente a settled (rollback seguro) e reporte ao suporte para investigar a row divergente.
O que fazer quando recebo pix.payout.processing?
Nada. É apenas um aviso. Saldo está em hold. Espere o próximo evento: pix.payout.confirmed (sucesso) ou pix.payout.failed (falha).
E se eu precisar de aprovação antes de enviar?
O endpoint /pix/cash-out/approve não existe hoje. O fluxo de envio é uma única chamada POST /pix/cash-out que imediatamente vai para o SPI. Se você quer aprovação manual, implemente isso no seu lado antes de chamar a API.
Como garantir idempotência?
- Para POST cash-out/cash-in/refund: use o header
Idempotency-Keycom um valor único do seu sistema. TTL de 24h. - Para webhooks recebidos: deduplique pelo header
X-Owem-Event-Id. O mesmo evento pode ser reentregue até 8 vezes em caso de falha HTTP no seu endpoint.
E o external_id?
Campo opcional (max 128 chars) que você define em cada POST. É retornado em todas as respostas e webhooks, permitindo consulta reversa via GET /api/external/transactions/ref/:external_id. Use para correlacionar pedidos do seu sistema com transações da Owem.
Um PIX que recebi ficou "contestado" — o que é?
Uma infração PIX foi aberta pela instituição do pagador (via BACEN DICT). Dependendo do valor e do E2E, pode gerar um bloqueio cautelar do saldo até a resolução. Ver fluxo completo em Infrações (fluxo).
Posso receber uma devolução sem ter enviado? (inbound refund)
Sim. Se você recebeu um PIX, a contraparte pode iniciar uma devolução unilateral (PACS.004 D-prefix) — você recebe o evento pix.return.received com status="settled" e um crédito de volta. É diferente de uma infração (que é uma contestação formal BACEN, não uma devolução direta).
Integrações rápidas
- Postman Collection: Download -- importe direto no Postman
- Bruno Collection:
backend/bruno/external/no repositório - Exemplos de payload: Webhook Payloads
- Autenticação: API Key + HMAC
Resumo em uma frase
Para cash-out, só trate como final quando receber
pix.payout.confirmed(settled) oupix.payout.failed(rejected). Para cash-in, só trate como final quando receberpix.charge.paid(paid). Nada além disso é terminal.