归因与 Postback
服务端集成指南
面向接入方开发者的指南。介绍从分享链接、注册到首次成功完成 Link 的数据交互、认证方式和 API 行为。
https://ambassador.tanka.aiproduction集成概览#
免费用户也计入,付费不是条件。系统按上报的首次 Link 完成时刻匹配有效奖励规则。同一用户只产生一次推荐奖励。
- 01分享链接并保留来源
管理员登记 TANKA 签发的 URL 和推荐码。发送方保存触点与归因,需要时可查询历史映射。
- 02发送用户状态
在注册、首次成功完成 Link、归因确认或更正后,发送完整用户状态。
- 03验证签名、计算奖励并对账
接收内容持久化后异步处理,再与权威数据的完整快照对账。
服务端集成有两个入口:通过 GET /integration/v1/referral-links/{code} 查询推荐来源,通过 POST /integration/v1/events 发送用户状态。
凭据与认证#
Postback:Key ID + Secret
用接收方签发的 Key ID 标识密钥。Secret 保存在服务端,用于计算每个请求的 HMAC 签名。请求中发送的是签名,不能将 Secret 本身放入 header 或 body。
查询:专用 Bearer Token
推荐码查询使用专用 Token。定期获取补全 API 时,使用该 API 提供方另行签发的凭据。
AMBASSADOR_API_BASE_URL=https://ambassador.tanka.ai
TANKA_EVENT_KEY_ID=<issued Key ID>
TANKA_EVENT_SECRET=<issued signing Secret>
TANKA_LINK_LOOKUP_TOKEN=<issued lookup Token>使用集成专用凭据,无需 Supabase 或 Resend 管理密钥。不得把生产 Secret 放入文档、前端代码、分享链接或浏览器。
轮换密钥时,将发送方切换到新的 Key ID / Secret。接收方可在限定期限内兼容旧密钥。每次重试更新 timestamp 和签名,保留原 event_id 与业务数据 body。
推荐码查询与归因绑定#
/integration/v1/referral-links/{code}?at=<RFC3339>完整推荐 URL 和 code 由 TANKA 签发,管理员在启用时登记。URL 按输入原文展示。允许的域名为 *.tanka.ai;登记时使用推荐参数 ref 自动填充 code。
此查询 API 为可选接口。通过 at 传入真实点击时刻,查询历史触点是否有效。
curl --get "$AMBASSADOR_API_BASE_URL/integration/v1/referral-links/$REFERRAL_CODE" \
--header "Authorization: Bearer $TANKA_LINK_LOOKUP_TOKEN" \
--data-urlencode "at=$TOUCHED_AT"调用查询接口时,路径参数 code 和查询参数 at 均必填。示例变量 REFERRAL_CODE 填写分享链接中的真实推荐码,TOUCHED_AT 填写带时区的 RFC3339 点击时间,例如 2026-09-08T00:01:00+09:00。--data-urlencode 可正确编码时区中的加号。
成功响应结构 · 映射示例
{
"program_id": "tanka-jp-ambassador",
"ambassador_id": "11111111-1111-4111-8111-111111111111",
"referral_link_id": "22222222-2222-4222-8222-222222222222",
"referral_code": "JP-example-only",
"campaign_id": "33333333-3333-4333-8333-333333333333",
"valid": true,
"checked_at": "2026-09-08T00:01:00+09:00",
"model": "last_click",
"window_days": 30,
"rule_version": "attr-v1"
}- 只有
valid=true才是有效触点。HTTP 200 也可能返回valid=false。不得用无效推荐码覆盖此前有效的来源。 - 系统用上报的
referral_code核对已登记的归因。三个内部 ID 可以省略或为 null;传入时必须与登记内容一致,否则以mapping_mismatch进入待核查状态。 - 发送方保存一致的
touch_id和touched_at,在注册完成时绑定到产品的user_id,并记录bound_at。 - 来源信息需跨域名、邮箱验证和 App/Web 跳转保留。已有用户登录、加入其他团队或注册后点击其他链接,均不计为新注册。
- 按触点发生时的有效性判断。停用链接不会清除停用前已成立的归因。
示例使用初始默认值 last_click / 30 天 / attr-v1。实际映射和适用规则以查询响应为准。
发送完整用户状态#
/integration/v1/eventsContent-Type 为 application/json,UTF-8 body 上限为 65,536 字节。每次按固定字段结构发送完整快照,空值用 null 明示。不支持计数增量或局部 patch。
| 字段 | 填写说明 |
|---|---|
signup_completed | 注册正式完成时。first_link 和 first_link_completed_at 均为 null。仅注册不会产生推荐奖励。 |
first_link_completed | 注册后首次成功完成任意 Link 时。开始、处理中或失败均不计入;第二次及之后的 Link 不再产生推荐奖励。 |
attribution_updated | 归因更新时,例如确认此前待核查的来源。发送完整快照,并保留注册与首次 Link 的事实。 |
user_state_corrected | 更正、排除或恢复已记录事实时。使用新事件 ID、更高的用户状态版本,并提供 correction 依据。 |
以下 ID、时刻和 link_type 仅展示结构,使用时需替换为真实事件和登记信息。本页面不会自动向生产 API 发送示例。
首个 codeOnly.json 示例将三个内部 ID 全部设为 null。其他示例中的 ID 只是可选字段的填写演示,使用时必须换成真实查询结果。
按 referral_code 匹配:三个内部 ID 为 null
{
"schema_version": "1.0",
"event_id": "example.first-link.code-only",
"event_type": "first_link_completed",
"environment": "production",
"program_id": "tanka-jp-ambassador",
"user_id": "example-user-001",
"state_version": 2,
"occurred_at": "2026-09-08T00:05:00+09:00",
"state_updated_at": "2026-09-08T00:05:00+09:00",
"data": {
"is_new_user": true,
"registration_email": "[email protected]",
"counting_status": "valid",
"reason_code": null,
"attribution": {
"status": "attributed",
"ambassador_id": null,
"referral_code": "JP-example-only",
"referral_link_id": null,
"campaign_id": null,
"touch_id": "example-touch-001",
"touched_at": "2026-09-08T00:01:00+09:00",
"bound_at": "2026-09-08T00:03:00+09:00",
"model": "last_click",
"window_days": 30,
"rule_version": "attr-v1"
},
"milestones": {
"signup_at": "2026-09-08T00:03:00+09:00",
"first_link_completed_at": "2026-09-08T00:05:00+09:00"
},
"first_link": {
"event_id": "example-link-success-001",
"link_type": "calendar"
},
"qualification_rule_version": "first-link-v1",
"correction": null
}
}注册完成:尚未完成首次 Link
{
"schema_version": "1.0",
"event_id": "example.signup.001",
"event_type": "signup_completed",
"environment": "production",
"program_id": "tanka-jp-ambassador",
"user_id": "example-user-001",
"state_version": 1,
"occurred_at": "2026-09-08T00:03:00+09:00",
"state_updated_at": "2026-09-08T00:03:00+09:00",
"data": {
"is_new_user": true,
"registration_email": "[email protected]",
"counting_status": "valid",
"reason_code": null,
"attribution": {
"status": "attributed",
"ambassador_id": "11111111-1111-4111-8111-111111111111",
"referral_code": "JP-example-only",
"referral_link_id": "22222222-2222-4222-8222-222222222222",
"campaign_id": "33333333-3333-4333-8333-333333333333",
"touch_id": "example-touch-001",
"touched_at": "2026-09-08T00:01:00+09:00",
"bound_at": "2026-09-08T00:03:00+09:00",
"model": "last_click",
"window_days": 30,
"rule_version": "attr-v1"
},
"milestones": {
"signup_at": "2026-09-08T00:03:00+09:00",
"first_link_completed_at": null
},
"first_link": null,
"qualification_rule_version": "first-link-v1",
"correction": null
}
}首次 Link 完成:具备计算奖励所需的事实
{
"schema_version": "1.0",
"event_id": "example.first-link.001",
"event_type": "first_link_completed",
"environment": "production",
"program_id": "tanka-jp-ambassador",
"user_id": "example-user-001",
"state_version": 2,
"occurred_at": "2026-09-08T00:05:00+09:00",
"state_updated_at": "2026-09-08T00:05:00+09:00",
"data": {
"is_new_user": true,
"registration_email": "[email protected]",
"counting_status": "valid",
"reason_code": null,
"attribution": {
"status": "attributed",
"ambassador_id": "11111111-1111-4111-8111-111111111111",
"referral_code": "JP-example-only",
"referral_link_id": "22222222-2222-4222-8222-222222222222",
"campaign_id": "33333333-3333-4333-8333-333333333333",
"touch_id": "example-touch-001",
"touched_at": "2026-09-08T00:01:00+09:00",
"bound_at": "2026-09-08T00:03:00+09:00",
"model": "last_click",
"window_days": 30,
"rule_version": "attr-v1"
},
"milestones": {
"signup_at": "2026-09-08T00:03:00+09:00",
"first_link_completed_at": "2026-09-08T00:05:00+09:00"
},
"first_link": {
"event_id": "example-link-success-001",
"link_type": "calendar"
},
"qualification_rule_version": "first-link-v1",
"correction": null
}
}来源待核查:显式保留 null 字段
{
"schema_version": "1.0",
"event_id": "example.signup.pending",
"event_type": "signup_completed",
"environment": "production",
"program_id": "tanka-jp-ambassador",
"user_id": "example-user-pending",
"state_version": 1,
"occurred_at": "2026-09-08T00:03:00+09:00",
"state_updated_at": "2026-09-08T00:03:00+09:00",
"data": {
"is_new_user": true,
"registration_email": "[email protected]",
"counting_status": "hold",
"reason_code": "attribution_unverified",
"attribution": {
"status": "pending",
"ambassador_id": null,
"referral_code": null,
"referral_link_id": null,
"campaign_id": null,
"touch_id": null,
"touched_at": null,
"bound_at": null,
"model": "last_click",
"window_days": 30,
"rule_version": "attr-v1"
},
"milestones": {
"signup_at": "2026-09-08T00:03:00+09:00",
"first_link_completed_at": null
},
"first_link": null,
"qualification_rule_version": "first-link-v1",
"correction": null
}
}后续确认待核查的来源
{
"schema_version": "1.0",
"event_id": "example.attribution.confirmed",
"event_type": "attribution_updated",
"environment": "production",
"program_id": "tanka-jp-ambassador",
"user_id": "example-user-pending",
"state_version": 2,
"occurred_at": "2026-09-08T00:04:00+09:00",
"state_updated_at": "2026-09-08T00:04:00+09:00",
"data": {
"is_new_user": true,
"registration_email": "[email protected]",
"counting_status": "valid",
"reason_code": null,
"attribution": {
"status": "attributed",
"ambassador_id": "11111111-1111-4111-8111-111111111111",
"referral_code": "JP-example-only",
"referral_link_id": "22222222-2222-4222-8222-222222222222",
"campaign_id": "33333333-3333-4333-8333-333333333333",
"touch_id": "example-touch-001",
"touched_at": "2026-09-08T00:01:00+09:00",
"bound_at": "2026-09-08T00:04:00+09:00",
"model": "last_click",
"window_days": 30,
"rule_version": "attr-v1"
},
"milestones": {
"signup_at": "2026-09-08T00:03:00+09:00",
"first_link_completed_at": null
},
"first_link": null,
"qualification_rule_version": "first-link-v1",
"correction": null
}
}更正:排除误计入的内部测试用户
{
"schema_version": "1.0",
"event_id": "example.correction.001",
"event_type": "user_state_corrected",
"environment": "production",
"program_id": "tanka-jp-ambassador",
"user_id": "example-user-001",
"state_version": 3,
"occurred_at": "2026-09-08T00:08:00+09:00",
"state_updated_at": "2026-09-08T00:08:00+09:00",
"data": {
"is_new_user": true,
"registration_email": "[email protected]",
"counting_status": "invalid",
"reason_code": "internal_test",
"attribution": {
"status": "attributed",
"ambassador_id": "11111111-1111-4111-8111-111111111111",
"referral_code": "JP-example-only",
"referral_link_id": "22222222-2222-4222-8222-222222222222",
"campaign_id": "33333333-3333-4333-8333-333333333333",
"touch_id": "example-touch-001",
"touched_at": "2026-09-08T00:01:00+09:00",
"bound_at": "2026-09-08T00:03:00+09:00",
"model": "last_click",
"window_days": 30,
"rule_version": "attr-v1"
},
"milestones": {
"signup_at": "2026-09-08T00:03:00+09:00",
"first_link_completed_at": "2026-09-08T00:05:00+09:00"
},
"first_link": {
"event_id": "example-link-success-001",
"link_type": "calendar"
},
"qualification_rule_version": "first-link-v1",
"correction": {
"reason_code": "internal_test",
"reference_id": "example-audit-001",
"supersedes_state_version": 2
}
}
}字段与业务规则#
以下逐项列出事件 JSON 的全部字段,标明键是否必填、null 条件及数据来源。未定义的字段会被拒绝。类型、长度和基本格式可查看由同一校验定义生成的 OpenAPI JSON。
必填、可省略与 null 的区别
- 必填:JSON 中必须出现该键。即使允许值为
null,也不能省略键本身。 - 可省略:可以不发送该键。
ambassador_id、referral_link_id、campaign_id和registration_email可以省略,各字段的来源与 null 行为见下表。 - null 是一个值。例如普通事件中的
{"reason_code": null}合法,但删除 reason_code 键会被拒绝。标为“有条件”的 null 规则在对应行说明。 first_link或correction为null时,不发送其子字段;传入对象时,对应表中的所有子字段均必填且不能为 null。
| 字段 | 发送要求 | 数据来源 | 填写说明 |
|---|---|---|---|
schema_version | 必填null: 不允许 | 接入约定配置 | 固定为 "1.0"。 |
event_id | 必填null: 不允许 | 发送方生成并保存 | 业务事件唯一且不可变的 ID。重试时保持不变,更正内容时使用新 ID。 |
event_type | 必填null: 不允许 | 产品业务记录 | 上述四种事件类型之一。 |
environment | 必填null: 不允许 | 接入约定配置 | 接受值为 production。本项目仅使用生产环境。 |
program_id | 必填null: 不允许 | 接入约定配置 | 固定值:tanka-jp-ambassador。 |
user_id | 必填null: 不允许 | 产品业务记录 | TANKA 产品内不可变的用户 ID,不可用设备 ID、团队 ID 或邮箱代替。 |
state_version | 必填null: 不允许 | 发送方生成并保存 | 同一用户内单调递增的正整数,最大为 9007199254740991;每个用户可从 1 开始。 |
state_updated_at | 必填null: 不允许 | 产品业务记录 | 此完整状态的最后更新时间,必须不早于其中的注册与首次 Link 时刻。 |
occurred_at | 必填null: 不允许 | 产品业务记录 | 业务事件真实发生时刻,不是 HTTP 发送或重试时刻。 |
data | 必填null: 不允许 | 发送方组装 | 下述完整用户状态对象。 |
| 字段 | 发送要求 | 数据来源 | 填写说明 |
|---|---|---|---|
is_new_user | 必填null: 不允许 | 产品业务记录 | 表示是否为新注册用户的 boolean。已有用户不能设为 true。 |
registration_email | 可省略null: 允许 | 产品业务记录 | TANKA 注册成功时使用的邮箱,最多254个字符。为兼容旧格式,可省略或填 null;能够取得时,应在每份完整状态中携带同一个注册时邮箱。仅在管理员的推荐用户列表展示。 |
counting_status | 必填null: 不允许 | 发送方有效性判断 | valid / hold / invalid。即使为 valid,也须满足新用户、有效归因和首次成功 Link 的条件才计算奖励。 |
reason_code | 必填null: 有条件 | 发送方有效性判断 | counting_status 为 hold / invalid 时必须有非空原因码;普通 valid 事件可填 null。 |
attribution | 必填null: 不允许 | 发送方归因记录 | 完整来源对象,字段见下表。 |
milestones | 必填null: 不允许 | 产品业务记录 | 包含注册与首次 Link 时间的对象,不能传空对象。 |
milestones.signup_at | 必填null: 不允许 | 产品业务记录 | 真实注册完成时刻。 |
milestones.first_link_completed_at | 必填null: 有条件 | 产品业务记录 | 首次 Link 成功前为 null;成功后填写实际首次成功时间,并同时提供 first_link 对象。重试或后续 Link 不改变此时间。 |
first_link | 必填null: 有条件 | 产品业务记录 | 首次 Link 成功前为 null;成功后必须提供包含 event_id 和 link_type 的对象,并同时填写 first_link_completed_at。 |
qualification_rule_version | 必填null: 不允许 | 接入约定配置 | 初始值为 first-link-v1,表示注册后首次成功完成任意 Link。 |
correction | 必填null: 有条件 | 发送方更正记录 | user_state_corrected 必须提供对象,其他普通事件可填 null。只要传入对象,下表列出的三个子字段均必填。 |
| 字段 | 发送要求 | 数据来源 | 填写说明 |
|---|---|---|---|
status | 必填null: 不允许 | 发送方归因记录 | attributed / pending / unattributed。 |
ambassador_id | 可省略null: 允许 | 接收端查询结果 | 本系统的大使 ID。不需要时可省略或填 null;填写时须使用推荐码查询返回的 UUID。 |
referral_link_id | 可省略null: 允许 | 接收端查询结果 | 本系统登记的推广链接 ID。不需要时可省略或填 null;填写时须使用查询返回的 UUID。 |
campaign_id | 可省略null: 允许 | 接收端查询结果 | 本系统的活动 ID。不需要时可省略或填 null。填写的是本系统查询返回的 UUID,不是发送方广告平台的 campaign ID。 |
referral_code | 必填null: 有条件 | 发送方归因记录 | TANKA 签发并由管理员登记的推荐码,是归因匹配的唯一键。attributed 时必须有值,其他状态未知时可填 null,长度1–64。 |
touch_id | 必填null: 有条件 | 发送方归因记录 | 发送方为点击生成并保存的标识。attributed 时必须有值,其他状态未知时可填 null。 |
touched_at | 必填null: 有条件 | 发送方归因记录 | 真实点击时间。attributed 时必须有值,其他状态未知时可填 null;填写时不得晚于注册时间。 |
bound_at | 必填null: 有条件 | 发送方归因记录 | 归因绑定或确认到注册用户的时间。attributed 时必须有值,其他状态未知时可填 null;填写时不得早于注册时间或晚于 state_updated_at。 |
model | 必填null: 不允许 | 约定规则或查询结果 | 适用的归因模型,初始为 last_click,必须与接收端配置一致。 |
window_days | 必填null: 不允许 | 约定规则或查询结果 | 适用的归因窗口天数,初始为30,必须与接收端配置一致。 |
rule_version | 必填null: 不允许 | 约定规则或查询结果 | 适用的归因规则版本,初始为 attr-v1,使用查询结果或接入配置。 |
| 字段 | 发送要求 | 数据来源 | 填写说明 |
|---|---|---|---|
event_id | 父级为对象时必填null: 不允许 | 产品业务记录 | 产品内标识首次 Link 成功完成的真实事件 ID,与外层用于回传的 event_id 用途不同。 |
link_type | 父级为对象时必填null: 不允许 | 产品业务记录 | 首次成功完成的实际 Link 类型,不能猜测或传空字符串。 |
| 字段 | 发送要求 | 数据来源 | 填写说明 |
|---|---|---|---|
reason_code | 父级为对象时必填null: 不允许 | 发送方更正记录 | 说明更正原因的非空代码。 |
reference_id | 父级为对象时必填null: 不允许 | 发送方更正记录 | 可追溯更正依据的 ID,例如审计或操作记录编号。 |
supersedes_state_version | 父级为对象时必填null: 不允许 | 发送方更正记录 | 被更正的用户状态版本,必须是小于本次 state_version 的正整数。 |
attributed时,referral_code、touch_id、touched_at、bound_at 必填。ambassador_id、referral_link_id、campaign_id 可省略或为 null。first_link与first_link_completed_at必须同时有值或同时为 null。首次 Link 时刻不得早于注册时刻。- 时刻使用带时区的 RFC3339,最多 6 位小数秒。
occurred_at和state_updated_at最多可比接收方当前时刻晚 300 秒。补全可发送真实的历史业务时刻。 - 普通事件 ID 长度为 1–160,可包含英文字母、数字、下划线、句点、冒号或连字符。推荐码使用相同字符集,长度为 1–64。可选内部 ID 必须使用查询获得的真实 UUID。
- 首次提供的真实
registration_email会被保存。普通事件省略或填 null 不会清除已知的注册时邮箱。更改已知值需要更高状态版本和correction;在更正中明确将这个键设为 null,可以清除错误记录的邮箱。用户后来修改账号邮箱,不改变注册时记录。 registration_email用于管理员核对推荐转化,不向大使返回。用户身份和奖励去重仍使用user_id。姓名、银行信息、付款金额和订阅信息属于未定义字段,会被拒绝。奖励单价按业务时刻确定。
生成签名并发送请求#
以下四个 header 均为必填。
| 字段 | 填写说明 |
|---|---|
Content-Type | application/json |
X-Tanka-Key-Id | 填写另行收到的 Key ID,不填写 Secret。 |
X-Tanka-Timestamp | 发送时刻的 Unix 秒数,以整数字符串表示,不使用毫秒。 |
X-Tanka-Signature | 64 个小写十六进制字符 HMAC-SHA256 摘要,不加 sha256= 等前缀。 |
message = ASCII(timestamp) + ASCII(".") + raw_body_bytes
signature = lowercase_hex(HMAC_SHA256(UTF8(secret), message))原样发送参与签名的字节。签名后不得改变 JSON 格式、换行、字段顺序或字符编码。允许时钟偏差为 300 秒,需保持服务端时钟同步。HTTPS 保护传输,HMAC 验证密钥持有权与 body 完整性。系统按事件去重。
Node.js · 签名并原样发送文件字节
// Node.js 20+ / ESM. Run: node postback.mjs approved-real-event.json
import { readFileSync } from "node:fs";
import { createHmac } from "node:crypto";
function required(name) {
const value = process.env[name];
if (!value) throw new Error("Missing environment variable: " + name);
return value;
}
if (!process.argv[2]) throw new Error("Pass the path to a real event JSON file");
const url = new URL("/integration/v1/events", required("AMBASSADOR_API_BASE_URL"));
if (url.protocol !== "https:") throw new Error("HTTPS is required");
const keyId = required("TANKA_EVENT_KEY_ID");
const secret = required("TANKA_EVENT_SECRET");
const body = readFileSync(process.argv[2]);
JSON.parse(body.toString("utf8")); // Keep the ORIGINAL bytes for signing and sending.
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = createHmac("sha256", secret)
.update(timestamp).update(".").update(body).digest("hex");
const response = await fetch(url, {
method: "POST",
redirect: "error",
signal: AbortSignal.timeout(15000),
headers: {
"Content-Type": "application/json",
"X-Tanka-Key-Id": keyId,
"X-Tanka-Timestamp": timestamp,
"X-Tanka-Signature": signature,
},
body,
});
console.log("HTTP " + response.status + " " + (await response.text()).slice(0, 4096));
if (![200, 202].includes(response.status)) process.exitCode = 1;
// Queue retries outside this example. Keep event_id/body, refresh timestamp/signature.
Python · 用标准库生成相同签名
# Python 3.10+, standard library. Run: python3 postback.py approved-real-event.json
import hashlib
import hmac
import json
import os
from pathlib import Path
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
def required(name):
value = os.environ.get(name)
if not value:
raise SystemExit("Missing environment variable: " + name)
return value
if len(sys.argv) != 2:
raise SystemExit("Pass the path to a real event JSON file")
url = urllib.parse.urljoin(required("AMBASSADOR_API_BASE_URL"), "/integration/v1/events")
if urllib.parse.urlparse(url).scheme != "https":
raise SystemExit("HTTPS is required")
key_id = required("TANKA_EVENT_KEY_ID")
secret = required("TANKA_EVENT_SECRET")
body = Path(sys.argv[1]).read_bytes()
json.loads(body) # Keep the ORIGINAL bytes for signing and sending.
timestamp = str(int(time.time()))
signature = hmac.new(secret.encode("utf-8"), timestamp.encode("ascii") + b"." + body,
hashlib.sha256).hexdigest()
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
request = urllib.request.Request(url, data=body, method="POST", headers={
"Content-Type": "application/json",
"X-Tanka-Key-Id": key_id,
"X-Tanka-Timestamp": timestamp,
"X-Tanka-Signature": signature,
})
opener = urllib.request.build_opener(NoRedirect())
try:
response = opener.open(request, timeout=15)
except urllib.error.HTTPError as error:
response = error
with response:
status = response.code
print("HTTP", status, response.read(4096).decode("utf-8", errors="replace"))
if status not in (200, 202):
raise SystemExit(1)
# Queue retries outside this example. Keep event_id/body, refresh timestamp/signature.
示例从文件读取 body、从服务端环境变量读取凭据,仅发送一次。生产发送方需实现持久队列、失败通知和重试。OpenAPI 认证定义仅描述 header,签名逻辑由发送方实现。
响应、重试与更正#
| 字段 | 填写说明 |
|---|---|
202 · accepted | {event_id,status:"accepted"}:接收内容已持久化,可视为发送完成;奖励异步处理,未必已计入。 |
200 · duplicate | {event_id,status:"duplicate"}:已接收相同内容的同一事件,停止重试。 |
400 · INVALID_INPUT | 检查 issues 中的 path 和 message,修正后重试。请求支持时附上接收方签发的 request_id。 |
401 · UNAUTHORIZED | 响应仅包含 error: UNAUTHORIZED。检查 Key ID、发送时刻、原始 body 和签名;暂停重试,排查凭据与服务端时钟。 |
409 · EVENT_ID_CONFLICT | 同一事件 ID 的内容发生了变化,请检查发送记录。使用不同 body 重试不能覆盖已有事件。 |
413 · INVALID_INPUT | body 超过上限,需调整事件内容并移除不必要的数据。 |
422 · ENVIRONMENT_MISMATCH / FUTURE_EVENT | 检查生产环境与计划 ID,或修正错误的未来业务时刻。 |
503 / 其他 5xx / 超时 | 可能尚未接收,也可能已接收但响应丢失。保留 event_id 和 body,更新 timestamp 与签名,采用退避和随机抖动后重试。 |
429(由边缘服务返回时) | 接收 API 本身未定义 429。若上游返回该状态,有 Retry-After 时遵循其要求,等待后重试。 |
发送方先保存业务数据和发送任务,再后台投递。重试间隔逐步延长,例如 5 秒、30 秒、2 分钟、10 分钟,并加入随机抖动。超期失败移入待核查队列。不能仅因超时就生成新事件 ID。
同一用户的旧版本不会覆盖新状态。同版本内容不一致时,会在异步处理中隔离。HTTP 202 不保证对账一致。新的事件 ID 也不会为同一用户重复计奖。更正需使用新的 event_id、更大的 state_version 和可追溯依据的 correction。已确认明细可能通过后续差额条目调整。
补全与对账 API#
大使系统定期通过 GET 拉取接入方 API。完整请求路径配置为 TANKA_BACKFILL_BASE_URL,专用 Bearer Token 配置为 TANKA_BACKFILL_TOKEN。
GET <partner feed URL>?program_id=tanka-jp-ambassador&limit=500&cursor=<next cursor>
Authorization: Bearer <backfill Token>{
"items": [
{
"schema_version": "1.0",
"event_id": "example.first-link.001",
"event_type": "first_link_completed",
"environment": "production",
"program_id": "tanka-jp-ambassador",
"user_id": "example-user-001",
"state_version": 2,
"occurred_at": "2026-09-08T00:05:00+09:00",
"state_updated_at": "2026-09-08T00:05:00+09:00",
"data": {
"is_new_user": true,
"registration_email": "[email protected]",
"counting_status": "valid",
"reason_code": null,
"attribution": {
"status": "attributed",
"ambassador_id": "11111111-1111-4111-8111-111111111111",
"referral_code": "JP-example-only",
"referral_link_id": "22222222-2222-4222-8222-222222222222",
"campaign_id": "33333333-3333-4333-8333-333333333333",
"touch_id": "example-touch-001",
"touched_at": "2026-09-08T00:01:00+09:00",
"bound_at": "2026-09-08T00:03:00+09:00",
"model": "last_click",
"window_days": 30,
"rule_version": "attr-v1"
},
"milestones": {
"signup_at": "2026-09-08T00:03:00+09:00",
"first_link_completed_at": "2026-09-08T00:05:00+09:00"
},
"first_link": {
"event_id": "example-link-success-001",
"link_type": "calendar"
},
"qualification_rule_version": "first-link-v1",
"correction": null
}
}
],
"next_cursor": null,
"watermark": "2026-09-08T00:06:00+09:00",
"snapshot_id": "example-snapshot-001",
"complete": true
}- 返回截至
watermark时点、与本项目相关的全部用户完整状态,包括待归因、无效、撤销和更正。仅汇总人数或仅有效用户列表无法用于对账。 - 每页
items最多 500 条。返回完整事件,或包含environment / program_id / user_id / state_version / state_updated_at / data的完整用户状态。 - 以
next_cursor=null表示结束。首次请求省略 cursor,后续页原样传入返回的游标。空页不能代替结束标记。 - 获取同一快照期间保持
watermark和快照标识不变。所有状态更新时间不得晚于 watermark,避免翻页期间的数据变动造成遗漏。 snapshot_id可省略,此时用 watermark 标识快照。complete也可省略;传入时须与是否为最后一页一致。- 每页上限 4 MiB,最多 2,000 页。达到页数上限后仍有下一页时,以
FEED_TOO_LONG失败。请求超时为 30 秒,不跟随重定向。失败后从快照首页重新拉取,已接收数据会去重。 - 月度确认要求最近 24 小时内完成一次完整对账,且 watermark 覆盖上月末。普通 CSV/JSON 导入不能作为完整性证明。