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.created | Owem 在创建 QR 时触发 | body status: "created" |
Webhook pix.charge.paid | PIX 已清算到账 | body status: "paid" ← 终态 |
GET /api/external/transactions/:id(支付后) | 查询 QR 的 tx_id 或 transaction_id | "settled" ← 与 paid 相同,只是不同接口 |
| GET 支付前 | 查询尚未支付的 QR 的 tx_id | "pending" / "expired" / "cancelled" |
pix.charge.paid webhook 示例(真实生产环境 payload):
{
"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% 情况) | 请求已接受,转发至 SPI | HTTP 202 "accepted" |
POST /api/external/pix/cash-out(快速通道,罕见) | BACEN 在响应返回前已清算 | HTTP 200 "settled" |
Webhook pix.payout.processing(可选,可以跳过) | 等待 BACEN 时 | body status: "processing" |
Webhook pix.payout.confirmed | BACEN 确认清算 | body status: "settled" ← 终态成功 |
Webhook pix.payout.failed | SPI 拒绝了交易 | 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):
{
"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):
{
"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_code 和 reason_description 有两种并存的约定 — 不是不一致,而是反映错误的源:
| 错误源 | reason_code | 示例 | 何时发生 |
|---|---|---|---|
| BACEN/SPI(通过 PACS.002 RJCT) | UPPERCASE 4 字符 | AC03、ED05、AM02、BE01、DUPL | 我们的 PACS.008 发送后 BACEN 拒绝 |
| Provider(BACEN 之前) | snake_case 小写 | dict_key_not_found、dict_bucket_exhausted、dict_client_rate_limited、provider_schema_error | 在到达 BACEN 之前 OnZ 或 bucket 故障 |
| 其他 | 混合时为 CamelCase_SNAKE | DICT_CLIENT_RATE_LIMITED、DICT_BUCKET_EXHAUSTED、DICT_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.requested 和 pix.refund.completed 自 2026 年 4 月起由后端自动触发。pix.refund.requested 在创建预防性封锁时触发;pix.refund.completed 在完成退款时触发。在 GET /med/:id 上轮询仍可作为替代方案工作。
自愿退款 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.confirmed 或 pix.payout.failed)仅在最终决定后触发 — 隔离没有中间 webhook |
| 中间状态 | 在隔离期间,GET /transactions/:id 和 GET /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/:id、GET /transactions/ref/:external_id 结构 2、GET /statement 带 status=processing)中,没有特定字段区分"隔离"和"正常处理" — 两者都显示为 status="processing" 和 payment_status="processing"。要识别隔离:
started_at超过 30 分钟前- 没有收到
pix.payout.confirmed或pix.payout.failedwebhook - 交易在此状态下保持 >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)时,钱才被确定扣除。
failed 和 rejected 有什么区别?
没有区别。它们是从不同接口看到的相同状态:
- Webhook body:
"rejected" - GET
/transactions/:idbody:"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-Keyheader,值来自您系统的唯一 ID。TTL 24 小时。 - 对于接收的 webhook: 通过
X-Owem-Event-Idheader 去重。如果您的端点出现 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 争议,而不是直接退款)。
快速集成
- Postman Collection v2.1: 下载 -- 直接导入 Postman
- Bruno Collection: 仓库中的
backend/bruno/external/ - Payload 示例: Webhook Payloads
- 身份验证: API Key + HMAC
一句话总结
对于 cash-out,只有收到
pix.payout.confirmed(settled)或pix.payout.failed(rejected)时才视为终态。对于 cash-in,只有收到pix.charge.paid(paid)时才视为终态。其他任何状态都不是终态。