PIX Cash-Out 按密钥
使用收款方的 PIX 密钥执行 PIX 转账。
端点
POST /api/external/pix/cash-out请求头
| 头 | 类型 | 必填 | 描述 |
|---|---|---|---|
Authorization | String | 是 | ApiKey {client_id}:{client_secret} |
Content-Type | String | 是 | application/json |
hmac | String | 是 | body 的 HMAC-SHA512 签名(十六进制) |
Idempotency-Key | String | 否 | 避免重复处理的唯一键(最大 256 字符) |
身份验证
参见 身份验证。HMAC 签名必须按照 HMAC-SHA512 中的说明生成。
Idempotency-Key - 重放行为
提供时,API 会在 24 小时内存储响应(仅 2xx),并为具有相同 (method, path, Idempotency-Key) 组合的任何新 POST 返回缓存的响应。缓存按端点作用域(/cash-out 和 /refund 中的相同键不冲突)。
- 在重放响应中,API 包含
X-Idempotent-Replay: true头并回显发送的Idempotency-Key。 - 超过 256 字符的键返回
400 Bad Request。 - 键是可选的。如果不发送,API 将每个 POST 作为新交易处理(确定性的
end_to_end_id仍然保证 BACEN/SPI 层的幂等性,但如果第一次尝试已结算,可能会被拒绝并返回failure_reason: "DUPL")。
必需权限
API Key 必须具有 transfer:write 权限才能发送 PIX。没有该权限,请求返回 403 Forbidden。参见 如何配置权限。
请求体
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
amount | Integer | 是 | 金额,以**分(centavos)**为单位。R$ 30,00 = 3000 |
pix_key | String | 是 | 收款方 PIX 密钥 |
pix_key_type | String | 否 | 密钥类型:cpf、cnpj、email、phone、evp。如省略,根据密钥值自动检测。 |
description | String | 否 | 转账描述(最多 140 个字符) |
external_id | String | 否 | 您系统中的标识符,用于追踪。trim 后最多 128 字符。仅 a-zA-Z0-9._:- 字符。在响应和 webhook 中返回。无效值(不允许的字符、> 128 字符、trim 后为空)被静默丢弃 - 交易继续,external_id: null。如需确保持久化,请在发送前自行验证。 |
recipient_ispb | String | 否 | 目的地机构的 ISPB,用于手动路由(8 位数字)。提供时,将付款定向到指定的 PSP。不要发送 Minha Konta 的 ISPB(04838403) - 机构内请求返回 same_institution 错误(不支持内部 PIX)。 |
end_to_end_id | String | 否 | BACEN 格式的 End-to-End ID(E{ISPB}{YYYYMMDDHHmm}{entropy})。建议省略 - 后端每次尝试生成确定性的 E2E(相同的 amount + pix_key + merchant_id → 相同的 E2E)。此确定性即使没有 Idempotency-Key 也能保证 SPI/BACEN 的幂等性。仅在协调的重新处理场景中手动发送。 |
purpose | String | 否 | 转账目的(用于内部使用和合规的自由文本字段)。 |
货币值
输入值以分为单位(R$ 1,00 = 100)。响应值以基础单位为单位(R$ 1,00 = 10000)。要将响应转换为雷亚尔,请除以 10.000。绝不使用浮点数。
示例
curl -X POST https://api.minhakonta.com/api/external/pix/cash-out \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-d '{
"amount": 3000,
"pix_key": "12345678901",
"pix_key_type": "cpf",
"description": "Pagamento fornecedor",
"external_id": "order-9876"
}'成功响应 -- 200 / 202
{
"worked": true,
"final": false,
"transaction_id": "PIXOUT20260309a1b2c3d4e5f6",
"end_to_end_id": "E04838403202603091530abcdef01",
"external_id": "order-9876",
"amount": 300000,
"fee_amount": 350,
"net_amount": 300350,
"status": "accepted",
"detail": "PIX enviado para processamento"
}HTTP 200 vs 202
- HTTP 200:交易已结算(
final: true,status: "settled")。 - HTTP 202:交易已被接受处理(
final: false)。通过轮询或 webhook 跟踪状态。status可能为"accepted"(正常流程)、"queued"(应用了 rate-limit - 每 3 秒自动重试最多 120 分钟)或"pending_approval"(等待双重授权工作流批准,启用时)。
| 字段 | 类型 | 描述 |
|---|---|---|
worked | Boolean | true 表示请求已被接受 |
final | Boolean | 当交易达到终止状态(已结算或已拒绝)时为 true。仍在处理中时为 false |
transaction_id | String | 交易唯一标识 |
end_to_end_id | String | SPI/BACEN 的 End-to-End 标识(格式 E{ISPB}...) |
external_id | String | 您的标识符,按发送原样返回。未提供时为 null |
amount | Integer | 转账值,以基础单位(÷ 10.000 得到雷亚尔)。300000 = R$ 30,00 |
fee_amount | Integer | 收取的手续费,以基础单位(÷ 10.000 得到雷亚尔) |
net_amount | Integer | 付款账户的总借记值,以基础单位。计算为 amount + fee_amount(总借记包括手续费)。不是收款方收到的值 - 他们只收到 amount。示例:amount=300000 + fee_amount=350 → net_amount=300350(从您账户借记 R$ 30,035,收款方入账 R$ 30,00) |
status | String | 其中之一:accepted(HTTP 202,正常同步处理)、settled(HTTP 200,立即结算 - 快速通道中罕见)、queued(HTTP 202,因 DICT 限制等待自动重处理)、pending_approval(HTTP 202,等待批准)。参见终止状态:按 ID 查询 Cash-Out -- status 字段的值 |
detail | String | 描述消息 |
cash-out 中的 net_amount 语义与 cash-in 不同
在 cash-out 中,net_amount = amount + fee_amount(付款账户的总借记)。在 cash-in(QR Code 支付)中,后端将 net_amount 视为扣除手续费后的净入账值。这种不对称性是历史性的 - 始终将 net_amount 视为"该方向上账户的实际变动"。对于会计对账,建议单独操作 amount 和 fee_amount 字段。
拒绝代码
API 可能因输入验证(发送到 SPI 之前)、与 provider / DICT 的集成错误(在同步发送期间)或带自动重试队列的 rate-limit 拒绝 cash-out。通过 PACS.002 RJCT 的 BACEN 拒绝是异步到达的,仅通过 状态查询 或 webhook pix.payout.rejected 出现。
错误响应格式
cash-out 的同步拒绝以两种不同的格式返回 - 根据错误的来源选择正确的 parser:
格式 A -- 验证或集成:HTTP 400 或 422,body {"status": "failed", "errors": [{"code": "<代码>", "params": [...]}]}。常见代码:same_institution_transfer、insufficient_balance、dict_key_not_found、dict_rate_limited、dict_bucket_exhausted。
格式 B -- payload 验证错误:HTTP 400,body {"errors": {"bad_request": "消息"}}。示例:invalid or missing amount、ambiguous key。
通过 data.status === "failed"(格式 A)vs data.errors.bad_request(格式 B)路由。
验证错误 (HTTP 400 / 422)
| HTTP | 格式 | 带代码的字段 | 含义 |
|---|---|---|---|
| 400 | B | errors.bad_request: "invalid or missing amount" | amount 缺失、零、负数或非整数 |
| 422 | A | errors[0].code: "pix_key_ambiguous" | 没有 pix_key_type 的 11 位密钥 - 可能是 CPF 或电话。通过 CPF 验证 解决并明确提供 pix_key_type |
| 400 | B | errors.bad_request: "invalid pix_key" | 密钥未通过格式规则(无效的 CPF 校验和、格式错误的电子邮件等) |
| 422 | A | errors[0].code: "same_institution_transfer" | recipient_ispb 是 Minha Konta 自己的 ISPB(04838403)。不支持机构内 PIX - 使用内部 TEF。注:此验证返回 HTTP 422(非 400),结构为 {status: "failed", errors: [{code: "same_institution_transfer", params: []}]} |
| 422 | A | errors[0].code: "insufficient_balance" | 可用余额小于 amount + fee_amount。考虑活动的 hold(gotcha min(TB, PG)) |
| 422 | A | errors[0].code: "ceiling_exceeded" | amount exceeds the configured per-transaction limit. errors[0].message = "PIX out limit reached: amount R$ X exceeds the R$ Y per-transaction limit"; errors[0].params.ceiling is the limit in subcents |
same_institution 的结构变化
这些文档的早期版本声称 HTTP 400 带 detail: "same_institution"。实际行为是HTTP 422 带格式 A 结构(errors 作为 {code, params} 数组)。执行 if (status === 400 && body.detail === "same_institution") 的客户端在实践中不会触发 - 使用 if (status === 422 && body.errors?.[0]?.code === "same_institution_transfer")。
与 provider / DICT 的集成错误 (HTTP 400)
当提供方在 BACEN 确认前返回同步错误时,API 返回格式 A,HTTP 400:
代码(errors[0].code) | 含义 | 建议操作 |
|---|---|---|
dict_key_not_found | DICT/BACEN 中未找到 PIX 密钥 | 与付款方核实;密钥可能已被删除或从未注册 |
dict_key_blocked | 密钥被封锁,例如疑似欺诈 | 联系密钥持有人 |
dict_lookup_failed | 查询 DICT 失败 | 5-30 秒后重试 |
dict_rate_limited | DICT 查询临时限制 | 等待自动重处理或在新请求前退避 |
dict_bucket_exhausted | DICT 限制暂时不可用 | 等待自动重处理;避免突发流量 |
provider_rejected | provider 以未分类的 4xx 通用错误拒绝 | 查看 errors[0].params 获取上下文;通过 Minha Konta 支持重新打开案例 |
provider_schema_error | 集成 payload 不兼容 | 立即报告,未经 Minha Konta 指引不要重试 |
provider_unknown_error | 进入此路径的 400..499 之外的状态 | 支持处提供完整日志 |
HTTP 是 400(不是 429)
这些文档的早期版本在同步路径中对 dict_rate_limited 和 dict_bucket_exhausted 显示 HTTP 429。当前合同是:同步集成错误返回 HTTP 400;带自动重处理的 DICT 限制返回 HTTP 202 queued。
带自动重试的 rate-limit (HTTP 202 queued)
当 Minha Konta 在发送交易到服务商前检测到 DICT 查询限制时,PIX OUT 会保持处理中并进入自动重试。此路径会在容量不可用时避免新的 DICT 调用。
两种场景触发队列:
| 来源 | reason_code(webhook pix.payout.queued) | 原因 |
|---|---|---|
| 客户额度 | DICT_CLIENT_RATE_LIMITED | 客户 DICT 查询量超过 Minha Konta 保护策略。 |
| 服务商 DICT 限制 | DICT_BUCKET_EXHAUSTED | DICT 查询能力暂时不可用。 |
队列时的 HTTP 响应:
{
"status": "queued",
"type": "pix",
"transaction_id": "PIXOUT20260309a1b2c3d4e5f6",
"end_to_end_id": "E04838403202603091530abcdef01",
"outbound_request_id": "0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D",
"amount": 300000,
"message": "Payment rate-limited, enqueued for automatic retry (TTL 120 min)",
"estimated_retry_seconds": 3,
"queue_ttl_seconds": 7200
}重试机制:
- Minha Konta 会在重试窗口有效期间自动重试。
- 总 TTL:7200 秒(120 分钟)。过期后,客户会收到
pix.payout.failed,reason_code: "DICT_QUEUE_TIMEOUT"。 - 立即 webhook:进入队列时,Minha Konta 触发
pix.payout.queued,包含reason_code(DICT_CLIENT_RATE_LIMITED或DICT_BUCKET_EXHAUSTED)和reason_description。下一个事件将是pix.payout.confirmed或pix.payout.failed。
非终态状态
始终将 queued 和 accepted 视为非终态。请通过 webhook 或交易查询接口跟踪最终结果。
权限与身份验证 (HTTP 401 / 403)
| HTTP | detail | 含义 |
|---|---|---|
| 401 | Invalid HMAC signature | HMAC 签名不匹配。检查序列化 body 中字段的字母顺序 - 参见 HMAC-SHA512 |
| 401 | Invalid API Key | client_id:client_secret 不正确 |
| 403 | permission 'transfer:write' required | API Key 没有 PIX 权限 |
| 403 | IP not whitelisted | 源 IP 不在 API Key 白名单中 |
代码词汇 - UPPERCASE × lowercase
cash-out 的结构化代码来自不同词汇表:
| 命名空间 | 约定 | 来源 | 示例 |
|---|---|---|---|
| BACEN SPI | UPPERCASE | 通过 PACS.002 RJCT 的异步拒绝(在 202 之后到达)- 在 GET /transactions/:id 和 webhook pix.payout.rejected 中可见 | AC03、AB03、ED05、DUPL、AM02、FF08、BE01 |
| 提供方 / DICT | lowercase snake_case | BACEN 确认前的同步拒绝 | dict_key_not_found、dict_rate_limited、same_institution_transfer、provider_schema_error |
| 重试队列 | UPPERCASE(前缀 DICT_) | 当有自动重试时的 webhook pix.payout.queued / pix.payout.failed | DICT_CLIENT_RATE_LIMITED、DICT_BUCKET_EXHAUSTED、DICT_QUEUE_TIMEOUT |
在程序化错误 switch 时,在您的一侧将其归一化为大写或小写以避免重复分支。不要期望在同步响应中出现 AM02 - BACEN 代码仅在 post-acceptance GET 查询中出现。
对应的 webhook
- 同步拒绝(上面的格式 A/B)不触发 webhook - 客户端已经在 HTTP 响应中收到错误。
- 按 rate-limit 入队(HTTP 202
queued)立即触发pix.payout.queued,带reason_code+reason_description。 - 异步拒绝(202 接受后的 PACS.002 RJCT)触发
pix.payout.rejected,带 BACENreason_code(AC03、AB03、ED05、DUPL 等)和英文reason_description。 - 孤儿 void(>30min 无 PACS.002)触发
pix.payout.failed,带reason_code: "orphan_force_voided"。 - 重试队列过期(120min)触发
pix.payout.failed,带reason_code: "DICT_QUEUE_TIMEOUT"。
PIX 密钥类型
| 类型 | 格式 | 示例 |
|---|---|---|
cpf | 11 位数字(无标点) | 12345678901 |
cnpj | 14 位数字(无标点) | 12345678000199 |
email | 电子邮件地址 | nome@empresa.com.br |
phone | 区号 + 号码(11 位数字) | 11999998888 |
evp | UUID v4 | a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d |
11 位密钥 - CPF vs 电话歧义
恰好 11 位的密钥可能是 CPF,也可能是手机号(DDD + 9xxxx-xxxx)。当密钥有歧义时,API 以 HTTP 400 和 failure_reason: "ambiguous key" 拒绝。
建议解决方案:
- 使用 CPF 验证 端点(
POST /api/external/cpf/validate)检查 11 位是否形成有效的 CPF - 如果
valid: true→ 在 cash-out 中发送pix_key_type: "cpf" - 如果
valid: false→ 是电话,发送pix_key_type: "phone"(API 自动添加前缀+55)
// 自动化流程示例
async function resolveKeyType(key) {
if (key.length !== 11 || /\D/.test(key)) return null; // 无歧义
const { data } = await api.post('/api/external/cpf/validate', { cpf: key });
return data.valid ? 'cpf' : 'phone';
}提示:以纯 11 位(DDD + 号码)发送电话。API 自动添加前缀 +55。避免手动发送 +55 - 可能在某些客户端中导致 HMAC 验证失败。
下一步
创建转账后,通过以下方式跟踪状态:
- 按 ID 查询
- 按 E2E ID 查询
- 按 Tag 查询
- 按 External ID 查询 --
GET /api/external/transactions/ref/{external_id}
或通过 Webhook 自动接收确认。
