Skip to content

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.paid com status: "paid"
  • PIX OUT (envio): webhook pix.payout.confirmed com status: "settled" (sucesso) ou pix.payout.failed com status: "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ícieQuandostatus retornado
POST /api/external/pix/cash-inVocê gera o QR code"active"
Webhook pix.charge.createdOwem dispara ao criar o QRbody status: "created"
Webhook pix.charge.paidPIX foi liquidado na contabody 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 pagamentoConsulta por tx_id de QR ainda não pago"pending" / "expired" / "cancelled"

Exemplo de webhook pix.charge.paid (payload real de produção):

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 um PIX

SuperfícieQuandostatus retornado
POST /api/external/pix/cash-out (async, quase sempre)Request aceito, enviado ao SPIHTTP 202 "accepted"
POST /api/external/pix/cash-out (fast-track, raro)BACEN liquidou antes da resposta voltarHTTP 200 "settled"
Webhook pix.payout.processing (opcional, pode pular)Enquanto espera BACENbody status: "processing"
Webhook pix.payout.confirmedBACEN confirmou a liquidaçãobody status: "settled" ← TERMINAL SUCESSO
Webhook pix.payout.failedSPI rejeitou a transaçãobody status: "rejected" ← TERMINAL FALHA
Webhook pix.payout.returnedPIX enviado e devolvido posteriormentebody 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):

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

Exemplo de webhook pix.payout.failed (payload real de produção):

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 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 erroreason_codeExemploQuando ocorre
BACEN/SPI (via PACS.002 RJCT)UPPERCASE 4 charsAC03, ED05, AM02, BE01, DUPLRejeição do BACEN após nossa PACS.008 ser enviada
Provider (pré-BACEN)snake_case lowercasedict_key_not_found, dict_bucket_exhausted, dict_client_rate_limited, provider_schema_errorFalha no OnZ ou bucket antes de chegar no BACEN
OutrosCamelCase_SNAKE quando mistoDICT_CLIENT_RATE_LIMITED, DICT_BUCKET_EXHAUSTED, DICT_RATE_LIMITEDCasos 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ícieQuandostatus retornado
Webhook pix.return.receivedVocê 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ícieQuandostatus retornado
POST /api/external/pix/refund (async)Request aceitoHTTP 202 "accepted"
POST /api/external/pix/refund (fast-track)Liquidado sincronamenteHTTP 200 "settled"
Webhook pix.refund.requestedBloqueio cautelar MED criado (disputa iniciada)body status: "requested"
Webhook pix.refund.completedDevolução efetivada (MED fluxo completo)body status: "completed"
Webhook pix.payout.returnedPIX 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/refund para 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).

AspectoComportamento
DuraçãoIndefinida — pode ser minutos, horas ou D+1
EscalaçãoEmails automáticos em 6h/24h/48h sem decisão para compliance@owem.com.br
Resolução automáticaSe 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 clienteO 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árioPermanece "processing" em GET /transactions/:id e GET /transactions/ref/:external_id (shape 2) durante a quarentena
SaldoPermanece 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:

  1. O started_at é de mais de 30 min atrás
  2. Nenhum webhook pix.payout.confirmed ou pix.payout.failed foi recebido
  3. 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/:id body: "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-Key com 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


Resumo em uma frase

Para cash-out, só trate como final quando receber pix.payout.confirmed (settled) ou pix.payout.failed (rejected). Para cash-in, só trate como final quando receber pix.charge.paid (paid). Nada além disso é terminal.

Owem Pay Instituição de Pagamento — ISPB 37839059