TEF -- Owem 账户间转账
两个 Owem 账户之间的转账(TEF)。无 BACEN 路由,无手续费,通过 TigerBeetle 立即结算。
端点
POST /api/external/transfers与 PIX Cash-Out 的区别
| 方面 | TEF (/transfers) | PIX Cash-Out (/pix/cash-out) |
|---|---|---|
| 目标 ISPB | 始终为 37839059(Owem) | 任意 PSP |
响应中的 endToEndId | 不存在 | E{ISPB}{YYYYMMDDHHmm}{entropy} |
feeAmount | 始终为 0 | 可能收取手续费 |
| 结算 | 通过 TigerBeetle 立即结算 | 通过 SPI/BACEN 异步结算 |
| Webhook 家族 | tef.transfer.* | pix.payout.* |
当目标密钥或账户位于 Owem Pay IP 时使用此端点。对于任何其他 PSP,请使用 PIX Cash-Out。
请求头
| 头 | 类型 | 必填 | 描述 |
|---|---|---|---|
Authorization | String | 是 | ApiKey {client_id}:{client_secret} |
Content-Type | String | 是 | application/json |
hmac | String | 是 | body 的 HMAC-SHA512 签名(十六进制)-- 参见 HMAC-SHA512 |
Idempotency-Key | String | 否 | 用于防止重复处理的唯一键(最大 256 字符) |
Body 中键的字母顺序
HMAC 验证在比较签名之前,会按字母顺序对 body 的 JSON 重新排序。请使用字母顺序序列化 body 的键,否则 HMAC 验证将以 401 失败。参见 HMAC-SHA512。
必需权限
API Key 必须具有 transfer:write 权限。没有该权限,请求返回 403 Forbidden。
请求体
接受两种互斥的目标模式:
通用
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
amount | Integer | 是 | 请求中以**分(centavos)**为单位。R$ 1,00 = 100。响应中后端以基础单位返回(1 BRL = 10000):发送 100 返回 amount: 10000。 |
description | String | 否 | 转账描述(最多 140 个字符) |
externalId | String | 否 | 您系统的标识符。trim 后最多 128 字符。仅 a-zA-Z0-9._:- 字符。无效值被静默丢弃(响应中为 null)。 |
模式 A -- 通过 Owem PIX 密钥指定目标
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
destinationKey | String | 是 | 目标 Owem 账户的 PIX 密钥 |
destinationKeyType | String | 是 | CPF | CNPJ | EMAIL | PHONE | EVP |
模式 B -- 通过 Owem 分行 + 账户指定目标
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
destinationAgency | String | 是 | 4 位数字(例如 0001) |
destinationAccountNumber | String | 是 | 目标 Owem 账户号码 |
互斥模式
在同一请求中发送 destinationKey 和 destinationAgency 返回 422 destination_ambiguous。两者都不发送返回 422 destination_required。
camelCase 或 snake_case
当 X-Key-Case: camelCase 头存在时,后端自动将 camelCase 转换为 snake_case。您可以在 body 中使用任一格式。规范文档使用 camelCase。
示例(模式 A -- 通过密钥)
curl -X POST https://api.owem.com.br/api/external/transfers \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-H "Idempotency-Key: 6f9c2b3e-1d4a-4f8b-9c2d-1e2f3a4b5c6d" \
-d '{
"amount": 100,
"description": "Internal transfer",
"destinationKey": "62188010000150",
"destinationKeyType": "CNPJ",
"externalId": "ord-2026-05-25-001"
}'示例(模式 B -- 通过分行+账户)
curl -X POST https://api.owem.com.br/api/external/transfers \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-H "Idempotency-Key: 6f9c2b3e-1d4a-4f8b-9c2d-1e2f3a4b5c6d" \
-d '{
"amount": 100,
"description": "Internal transfer",
"destinationAgency": "0001",
"destinationAccountNumber": "10001",
"externalId": "ord-2026-05-25-002"
}'计算 HMAC
HMAC-SHA512 签名通过对键按字母顺序排序后序列化的 body JSON 计算得出。完整算法参见 HMAC-SHA512。
成功响应 -- 200
对于 amount: 100(R$ 1,00)的请求:
{
"worked": true,
"final": true,
"transactionId": "TEFabcd1234...",
"externalId": "ord-2026-05-25-001",
"amount": 10000,
"feeAmount": 0,
"netAmount": 10000,
"channel": "tef",
"status": "settled",
"detail": "Settled in ledger"
}响应值以基础单位返回(1 BRL = 10000)
请求接受以分为单位的 amount(R$ 1,00 = 100),但响应返回的 amount、feeAmount 和 netAmount 以基础单位为单位(1 BRL = 10000)。要转换为 BRL,请除以 10000(例如,amount: 10000 表示 R$ 1,00)。与 PIX Cash-Out 端点行为相同。
推荐使用 X-Key-Case: camelCase 头
没有 X-Key-Case: camelCase 头时,响应以 snake_case 返回(transaction_id、external_id、fee_amount、net_amount)。在每个请求中发送该头以接收上面所示的 camelCase。
endToEndId 不存在
TEF 不生成 BACEN End-to-End 标识符。请使用 transactionId(前缀 TEF)或 externalId 来追踪交易。
| 字段 | 类型 | 描述 |
|---|---|---|
worked | Boolean | true 表示请求已被接受 |
final | Boolean | true 当交易已达到终态(已结算的 TEF 始终为 true) |
transactionId | String | 交易的唯一标识符(前缀 TEF) |
externalId | String | 您的标识符,原样返回。如未提供或被丢弃则为 null |
amount | Integer | 转账金额,以基础单位为单位(1 BRL = 10000)。要转换为 BRL,请除以 10000。 |
feeAmount | Integer | 始终为 0(内部 TEF 无手续费) |
netAmount | Integer | 等于 amount(无手续费) |
channel | String | 始终为 "tef" |
status | String | 立即结算时为 "settled" |
detail | String | 描述性消息 |
错误代码
验证错误(HTTP 422)
响应 shape: {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}
| 代码 | 描述 |
|---|---|
route_via_pix_cashout | 目标不是 Owem 客户。请使用 POST /api/external/pix/cash-out。 |
destination_required | 未提供任何目标模式(destinationKey+destinationKeyType 或 destinationAgency+destinationAccountNumber)。 |
destination_ambiguous | 在同一请求中发送了 destinationKey 和 destinationAgency。 |
destination_not_found | 目标密钥/账户在 Owem 中不存在或处于非活动状态。params 包含 account_number 和 agency。 |
self_transfer | 源账户与目标账户相同。params.account_id 携带 ID。 |
pix_key_ambiguous | 11 位密钥缺少 destinationKeyType,值为 CPF 校验通过且 DDD 有效、第三位为 9(可能是 CPF 或电话)。发送 destinationKeyType: "CPF" 或 "PHONE" 以消除歧义。 |
集成错误(HTTP 400)
响应 shape: {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}
| 代码 | 描述 |
|---|---|
insufficient_balance | 可用余额小于 amount。 |
pix_out_transaction_limit_exceeded | 金额超过账户配置的每笔交易限额。 |
输入错误(HTTP 400)
响应 shape: {"errors": {"bad_request": "<message>"}}
| 消息 | 原因 |
|---|---|
invalid or missing amount | amount 缺失、零、负数或非整数。 |
认证 / 授权错误
| HTTP | Shape | 描述 |
|---|---|---|
| 401 | {"worked": false, "detail": "Missing HMAC header"} | 缺少 hmac 头。 |
| 401 | {"worked": false, "detail": "Invalid HMAC signature"} | HMAC 签名不匹配(body 顺序错误或 secret 错误)。 |
| 401 | {"error": {"status": 401, "message": "..."}} | API Key 缺失、无效、非活动或已过期。查看 error.message。 |
| 403 | {"error": {"status": 403, "message": "Request IP not in API key whitelist"}} | 请求 IP 不在 API Key 白名单中。 |
| 403 | {"errors": {"forbidden": "Permission required: transfer:write"}} | API Key 没有 transfer:write 权限。 |
Webhooks
转账结算时,后端会触发两个 webhook(均为尽力而为)。每个 payload 都会被 dispatcher 自动注入 eventType 和 status: tef.transfer.sent 和 tef.transfer.received 收到 status: "settled"; tef.transfer.failed 收到 status: "failed"。货币值以基础单位为单位(1 BRL = 10000),与端点响应一致。
tef.transfer.sent
发送给 caller 的订阅者(源账户)。
{
"eventType": "tef.transfer.sent",
"status": "settled",
"transactionId": "TEFabcd1234...",
"accountId": 10001,
"senderAccountId": 10001,
"receiverAccountId": 10002,
"amount": 10000,
"description": "Internal transfer",
"merchantId": "e84b303c-007f-407d-ae20-f1056a24524d",
"entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
"settledAt": "2026-05-25T14:30:00Z"
}tef.transfer.received
发送给目标订阅者(接收账户)。transactionId 带后缀 _RCV 以区分贷方腿和借方腿。
{
"eventType": "tef.transfer.received",
"status": "settled",
"transactionId": "TEFabcd1234..._RCV",
"accountId": 10002,
"senderAccountId": 10001,
"receiverAccountId": 10002,
"amount": 10000,
"description": "Internal transfer",
"merchantId": "e84b303c-007f-407d-ae20-f1056a24524d",
"entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
"settledAt": "2026-05-25T14:30:00Z"
}tef.transfer.failed
如果 TigerBeetle 结算失败则触发(罕见)。
{
"eventType": "tef.transfer.failed",
"status": "failed",
"accountId": 10001,
"transactionId": "TEFabcd1234...",
"receiverAccountId": 10002,
"amount": 10000,
"merchantId": "e84b303c-007f-407d-ae20-f1056a24524d",
"entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
"failureReason": "tb error description",
"failedAt": "2026-05-25T14:30:00Z"
}幂等性
幂等性完全通过 Idempotency-Key 头控制。
- Header
Idempotency-Key(推荐,UUID): 每个请求的唯一键。服务器存储响应 24 小时,任何使用相同键 + 方法 + 路径的重试,**返回缓存的 body 和原始 HTTP 状态码(200 或 202)**以及两个响应头:idempotency-key: <发送的键>x-idempotent-replay: true
- 服务器在重试时不比较请求 body: 返回的 body 始终是首次成功调用填充缓存的那个。为不同的请求使用不同的键。
Idempotency-Key 不返回 409
与某些集成的预期不同,服务器在键已被使用时不会响应 409。相反,它返回与第一次调用相同的响应(200/202 带原始 body,加上 x-idempotent-replay: true 头)。通过检查该头来检测重试,而不是期望不同的状态码。
clientRequestId 在 body 中不被 TEF 接受
/transfers 端点接受 body 中的 clientRequestId 而不抱怨(后端不拒绝该字段),但不将其用作幂等性。每个具有相同 clientRequestId 但不同 Idempotency-Key 的请求都会产生独立的交易。请专门使用 Idempotency-Key 头来保证幂等性。
下一步
- PIX Cash-Out 按密钥 -- 用于 Owem 以外的转账。
- HMAC-SHA512 -- 签名详情。
- API Key -- 如何配置权限。
- Webhooks -- 如何订阅和验证事件。