Skip to content

PIX Lifecycle -- Visão Definitiva

Esta é a página autoritativa sobre o ciclo de vida de uma transação PIX na Monetarie. 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.

Unidade monetária dos exemplos

Os corpos de webhook desta página estão em subcentavos (1 BRL = 10.000), a mesma unidade da API GET, e com as chaves em camelCase, exatamente como a entrega. Até 08/08/2026 o callback entregava centavos, 100 vezes menos; a correção e o que fazer se sua integração se calibrou ao comportamento antigo estão em Payloads de Webhook, que é a página autoritativa de campo a campo.


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.createdMonetarie 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 (forma e unidade do corpo entregue):

json
{
  "eventType": "pix.charge.paid",
  "status": "paid",
  "accountId": 10011,
  "amount": 10000000,
  "feeAmount": 25000,
  "counterpartyName": "Marcia Cristiane Ribeiro Barbosa",
  "endToEndId": "E165015552026041016069d8b4c6b2fc",
  "externalId": "T2604101306qtsfffH",
  "paidAt": "2026-04-10T16:07:08.158762Z",
  "payerBankName": "STONE IP S.A.",
  "payerDocument": "200***97",
  "payerIspb": "16501555",
  "qrCodeId": "e9f3df72-031f-49bf-abc3-a9ce1d540726",
  "txId": "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 (forma e unidade do corpo entregue):

json
{
  "eventType": "pix.payout.confirmed",
  "status": "settled",
  "accountId": 10011,
  "amount": 50000000,
  "feeAmount": 25000,
  "description": "PIX Cash-Out",
  "endToEndId": "E4602656220260411101530220db1672",
  "externalId": "T2604110715qx55o7E",
  "pixKey": "11077675046",
  "initiatedAt": "2026-04-11T10:15:31.141953Z",
  "recipient": {
    "name": "JOHN DOE",
    "document": "110***46",
    "ispb": "46026562",
    "institutionName": "MONETARIE SCD"
  },
  "transactionId": "PIXOUT8813809cc536884c83056900088b"
}

Exemplo de webhook pix.payout.failed (forma e unidade do corpo entregue):

json
{
  "eventType": "pix.payout.failed",
  "status": "rejected",
  "accountId": 10016,
  "amount": 8000000000,
  "feeAmount": 35000,
  "reason": "rejected",
  "reasonCode": "AC03",
  "reasonDescription": "Invalid creditor account number",
  "description": "tx-MONETARIE-1775664887942",
  "endToEndId": "E4602656220260408161448ad70215f0",
  "pixKey": "23bb00c0-9b4a-48f5-b62a-03546beb858f",
  "recipient": {
    "name": null,
    "document": null,
    "ispb": null,
    "institutionName": null
  },
  "initiatedAt": "2026-04-08T16:14:48.213978Z",
  "transactionId": "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 do provedor ou limite DICT 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 (crédito de um PIX que você ENVIOU)

A outra instituição devolveu um PIX que você havia enviado (ex.: você pagou a mais, o recebedor devolveu parcial/total, e você, como pagador original, recebe essa devolução de volta como um crédito).

SuperfícieQuandostatus retornado
Webhook pix.payout.returnedVocê recebeu um PIX de volta (crédito na sua conta)body status: "returned", direction: "credit" (entregue também a assinantes do alias pix.return.received)

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: "pending"
Webhook pix.refund.completedDevolução efetivada (MED fluxo completo)body status: "settled"
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"
       │        │
       │        │ Provedor 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 Monetarie (que verifica mesa operacional Monetarie e 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@monetarie.com
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 Monetarie 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-Monetarie-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 bytes) que você define nos POSTs de cash-in e cash-out. Quando presente, (account_id, external_id) é a identidade durável, única e idempotente da ordem nos dois fluxos: repita o mesmo valor apenas para repetir o mesmo comando; reutilizá-lo com payload ou direção diferente retorna 409. Ele também é retornado nas respostas e webhooks e permite consulta reversa via GET /api/external/transactions/ref/:external_id.

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ê ENVIOU um PIX, o recebedor pode iniciar uma devolução unilateral (PACS.004 D-prefix) - você recebe o evento canônico pix.payout.returned com status="returned" e direction="credit" (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.

MONETARIE SOCIEDADE DE CRÉDITO DIRETO S.A. - SCD · CNPJ 46.026.562/0001-05 · ISPB 46026562