Skip to content

PIX Lifecycle -- 权威视图

这是 Owem Pay 的 PIX 交易生命周期权威页面。如果您对某个状态的含义有疑问,请回到这里查看。

TL;DR -- 您只需要知道这一件事

要知道一笔交易是否最终状态,请等待以下事件之一:

  • PIX IN(收款): webhook pix.charge.paid,status: "paid"
  • PIX OUT(付款): webhook pix.payout.confirmed,status: "settled" (成功)pix.payout.failed,status: "rejected" (失败)

其他任何状态都是中间态。在这些事件之前不要在您的系统中记入或扣款任何金额。

黄金法则

唯一重要的词汇是 webhook 词汇。GET API 返回完全相同的值 — webhook 和 GET 之间没有翻译。


状态矩阵 -- 单一可信源

相同的状态值出现在三个接口:POST 响应 body、webhook body 和 GET /transactions/:id 响应 body。它们之间没有翻译。

PIX IN(Cash-In)-- 接收 PIX

接口何时返回 status
POST /api/external/pix/cash-in您生成 QR 码"active"
Webhook pix.charge.createdOwem 在创建 QR 时触发body status: "created"
Webhook pix.charge.paidPIX 已清算到账body status: "paid" ← 终态
GET /api/external/transactions/:id(支付后)查询 QR 的 tx_idtransaction_id"settled" ← 与 paid 相同,只是不同接口
GET 支付前查询尚未支付的 QR 的 tx_id"pending" / "expired" / "cancelled"

pix.charge.paid webhook 示例(真实生产环境 payload):

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)-- 发送 PIX

接口何时返回 status
POST /api/external/pix/cash-out(异步,99% 情况)请求已接受,转发至 SPIHTTP 202 "accepted"
POST /api/external/pix/cash-out(快速通道,罕见)BACEN 在响应返回前已清算HTTP 200 "settled"
Webhook pix.payout.processing(可选,可以跳过)等待 BACEN 时body status: "processing"
Webhook pix.payout.confirmedBACEN 确认清算body status: "settled" ← 终态成功
Webhook pix.payout.failedSPI 拒绝了交易body status: "rejected" ← 终态失败
Webhook pix.payout.returned已发送的 PIX 后被退回body status: "returned"
GET /api/external/transactions/:id(处理中)交易尚未清算"processing"
GET /api/external/transactions/:id(已清算)pix.payout.confirmed 之后"settled"
GET /api/external/transactions/:id(失败)pix.payout.failed 之后"failed"

pix.payout.confirmed webhook 示例(真实生产环境 payload):

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

pix.payout.failed webhook 示例(真实生产环境 payload):

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(BACEN UPPERCASE vs provider snake_case)

字段 reason_codereason_description两种并存的约定 — 不是不一致,而是反映错误的源:

错误源reason_code示例何时发生
BACEN/SPI(通过 PACS.002 RJCT)UPPERCASE 4 字符AC03ED05AM02BE01DUPL我们的 PACS.008 发送后 BACEN 拒绝
Provider(BACEN 之前)snake_case 小写dict_key_not_founddict_bucket_exhausteddict_client_rate_limitedprovider_schema_error在到达 BACEN 之前 OnZ 或 bucket 故障
其他混合时为 CamelCase_SNAKEDICT_CLIENT_RATE_LIMITEDDICT_BUCKET_EXHAUSTEDDICT_RATE_LIMITED重试队列的特定情况(pix.payout.queued

reason_description 默认为英文(例:AC03"Invalid creditor account number")。要对重试进行分类:代码上的完全匹配 + direction=outbound + 确定性重试表。不要在 BACEN 和 provider 之间进行不区分大小写的匹配 — 这两种约定按设计是不同的。

遗留字段 reason(字符串)仅在后端无法提取结构化 BACEN 代码时出现;当 reason_code 已填充时,省略 reason(互斥,session 141+163)。

退款

有两种不同的退款场景。请注意方向。

场景 A -- 您收到了退款(inbound refund)

另一家机构退回了您已收到的 PIX(例:付款方发送了多余的 PIX,请求部分/全额退款,而您作为原始收款方,以贷记形式收到此退款)。

接口何时返回 status
Webhook pix.return.received您收到了 PIX 回来(您账户的贷记)body status: "settled"

场景 B -- 您发送了退款(outbound refund)

您通过 POST /api/external/pix/refund 发起退款(通常是 MED)或收到 PIX 并通过 PACS.004 手动退回。

接口何时返回 status
POST /api/external/pix/refund(异步)请求已接受HTTP 202 "accepted"
POST /api/external/pix/refund(快速通道)同步清算HTTP 200 "settled"
Webhook pix.refund.requested创建 MED 预防性封锁(争议开始)body status: "requested"
Webhook pix.refund.completed退款已完成(MED 完整流程)body status: "completed"
Webhook pix.payout.returned已发送的 PIX OUT 通过 PACS.004(D 前缀)退回body status: "returned"

自动触发的退款事件

事件 pix.refund.requestedpix.refund.completed 自 2026 年 4 月起由后端自动触发pix.refund.requested 在创建预防性封锁时触发;pix.refund.completed 在完成退款时触发。在 GET /med/:id 上轮询仍可作为替代方案工作。

违规 → 封锁 → 退款的完整流程在 违规(流程) 中。预防性封锁的查询在 列出 MED 中。

自愿退款 vs MED

  • POST /api/external/pix/refund(此场景 B 流程):由您发起的自愿退款。退款 E2E 有前缀 D
  • MED(特殊退款机制):当 BACEN 违规被接受(analysis_result=AGREED)时由系统执行的监管退款。不要为 MED 调用 /pix/refund — 后端会自动执行。参见 违规

流程图

PIX IN -- 收款

POST /api/external/pix/cash-in

       │ 响应:status "active",transaction_id(QR 的 tx_id)

[Webhook] pix.charge.created       ← status "created"

       │ 不确定时间(等待付款方支付 QR)

[付款方外部支付 QR]

       │ BACEN 清算 (<2s)

[Webhook] pix.charge.paid          ← status "paid"(终态)


GET /api/external/transactions/:id 返回 status "settled"

PIX OUT -- 付款

POST /api/external/pix/cash-out

       ├─ 路径 A(99% 情况): HTTP 202 + status "accepted"
       │        │
       │        │ Provider OnZ 已将 PACS.008 转发至 SPI/BACEN
       │        ▼
       │   [Webhook] pix.payout.processing(可选,可以跳过)
       │        │          ← body status "processing"
       │        │ BACEN 响应(典型 1.6-2s)
       │        │
       │        ├─ 成功 → [Webhook] pix.payout.confirmed
       │        │                 ← body status "settled"(终态成功)
       │        │                 ← GET 返回 "settled"
       │        │
       │        └─ 失败 → [Webhook] pix.payout.failed
       │                          ← body status "rejected"(终态失败)
       │                          ← GET 返回 "failed"

       └─ 路径 B(罕见,快速通道): HTTP 200 + status "settled"(已经终态)

隔离(无 BACEN 响应的操作)

如果 PIX OUT 在 processing 状态下停留 >30 分钟而没有 BACEN 确认/拒绝,系统将操作移至隔离(stage=5)而不是强制自动 void。客户余额保持封锁,直到 Owem 储备飞行员手动决策(检查 OnZ MGMT + Planner 驾驶舱 + 清算账户)。

方面行为
持续时间不确定 — 可能是分钟、小时或 D+1
升级在 6h/24h/48h 内无决定时自动发送电子邮件到 compliance@owem.com.br
自动解决如果 BACEN 稍后响应(通过 long-polling 的 PACS.002),操作无需手动干预即可解决 — 飞行员被告知回溯解决
对客户的可见性对应的 webhook(pix.payout.confirmedpix.payout.failed仅在最终决定后触发 — 隔离没有中间 webhook
中间状态在隔离期间,GET /transactions/:idGET /transactions/ref/:external_id(结构 2)中保持 "processing"
余额保持 pending(从 available 扣除,保留在 balance)。参见 余额

这取代了之前"30min 后 force_void"的行为,该行为导致财务损失风险(Owem 系统中的余额已恢复,而 BACEN 在收款方端已执行转账)。

隔离 vs 事件

如果您看到 PIX OUT 在 processing 状态超过 30 分钟,不是系统错误 — 是隔离等待手动验证。最终 webhook 将在解决后触发。仅当超过 48 小时无解决或您确认付款已进入 BACEN 但未收到 webhook 时联系支持。

如何通过 API 检测隔离

在查询端点(GET /transactions/:idGET /transactions/ref/:external_id 结构 2、GET /statementstatus=processing)中,没有特定字段区分"隔离"和"正常处理" — 两者都显示为 status="processing"payment_status="processing"。要识别隔离:

  1. started_at 超过 30 分钟前
  2. 没有收到 pix.payout.confirmedpix.payout.failed webhook
  3. 交易在此状态下保持 >1h

如果这 3 个信号得到确认,很可能是隔离 — 等待或联系支持。绝不重新发送相同的请求(当原始交易回溯清算时会产生重复)。

隔离中 PIX OUT 的重试

绝不重新发送处于隔离中的 PIX OUT。 原始交易可能随时清算。重新发送会产生重复。等待手动或自动解决 — 当解决时您将通过 webhook 收到通知。


常见问题

为什么 POST 返回 accepted 但 webhook 返回 settled

这是不同的阶段。POST = "我们收到了您的请求并排队"。Webhook confirmed = "BACEN 确认了清算"。在巴西,清算很快(~1.6 秒),但仍然是异步的 — 发起 POST 的 HTTP 客户端在 BACEN 回复之前已经收到了响应。

accepted 是否表示钱已被确定扣除?

不是。 这意味着钱处于 hold 状态 — 已预留,但如果 SPI 拒绝仍可释放。只有当您收到 pix.payout.confirmed(或 GET 返回 settled)时,钱才被确定扣除。

failedrejected 有什么区别?

没有区别。它们是从不同接口看到的相同状态:

  • Webhook body: "rejected"
  • GET /transactions/:id body: "failed"

两者都表示:交易被 SPI 拒绝,hold 已释放,余额已恢复。不要记入此付款。

旧文档说 completed。还存在吗?

实际上没有。completed 一词出现在旧示例中,是虚构文档 — 于 2026-04-12 修复。对于生产中的所有交易,字段返回 "settled"

代码中仍存在理论回退(helpers.ex:127):如果 transactions 的一行有 status=1(approved) payment_status IS NULL,后端返回 "completed"。实际上,TbFirst pipeline 始终在成功的 PIX IN/OUT 中填充 payment_status — 因此此回退在实际流量中从不触发。如果即便如此您的集成观察到 status="completed",请将其视为等同于 settled(安全回退)并报告给支持以调查差异行。

当我收到 pix.payout.processing 时应该怎么做?

什么都不做。 这只是一个通知。余额处于 hold。等待下一个事件:pix.payout.confirmed(成功)或 pix.payout.failed(失败)。

如果我在发送前需要审批怎么办?

今天 /pix/cash-out/approve 端点不存在。发送流程是单个 POST /pix/cash-out 调用,立即转发至 SPI。如果您需要手动审批,请在调用 API 之前在您的系统端实现。

如何保证幂等性?

  • 对于 POST cash-out/cash-in/refund: 使用 Idempotency-Key header,值来自您系统的唯一 ID。TTL 24 小时。
  • 对于接收的 webhook: 通过 X-Owem-Event-Id header 去重。如果您的端点出现 HTTP 失败,同一个事件可能会被重新发送多达 8 次。

external_id 是什么?

可选字段(最多 128 字符),您在每个 POST 中设置。在所有响应和 webhook 中返回,允许通过 GET /api/external/transactions/ref/:external_id 反向查询。用它将您系统中的订单与 Owem 交易关联。

我收到的 PIX 变为"已争议" — 这是什么?

付款方机构通过 BACEN DICT 打开了 PIX 违规。根据值和 E2E,可能会生成余额的预防性封锁直到解决。完整流程见 违规(流程)

我可以在没有发送的情况下收到退款吗?(inbound refund)

是。如果您收到 PIX,对方可以发起单方退款(PACS.004 D 前缀) — 您收到事件 pix.return.received,status="settled" 和贷记回来。这与违规不同(违规是正式的 BACEN 争议,而不是直接退款)。


快速集成


一句话总结

对于 cash-out,只有收到 pix.payout.confirmedsettled)或 pix.payout.failedrejected)时才视为终态。对于 cash-in,只有收到 pix.charge.paidpaid)时才视为终态。其他任何状态都不是终态。

Owem Pay Instituição de Pagamento — ISPB 37839059