归因与 Postback
服务端集成指南
面向接入方开发者的指南。介绍从分享链接、注册到首次成功完成 Link 的数据交互、认证方式和 API 行为。
https://ambassador.tanka.aiproduction集成概览#
免费用户也计入,付费不是条件。系统按上报的首次 Link 完成时刻匹配有效奖励规则。同一用户只产生一次推荐奖励。
- 01分享链接并保留来源
管理员在启用大使时登记由本系统签发的推荐 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>使用集成专用凭据,无需接收方自身的平台管理密钥。不得把生产 Secret 放入文档、前端代码、分享链接或浏览器。
轮换密钥时,将发送方切换到新的 Key ID / Secret。接收方可在限定期限内兼容旧密钥。每次重试更新 timestamp 和签名,保留原 event_id 与业务数据 body。
推荐码查询与归因绑定#
/integration/v1/referral-links/{code}?at=<RFC3339>推荐 URL 和 code 由本系统签发(例:https://jp.tanka.ai/?ref=JP-XXXX-XXXX),管理员在启用时登记。符合格式的 code,TANKA 可直接使用。URL 按登记原文展示。允许的域名为 *.tanka.ai;登记时使用推荐参数 ref 自动填充 code。
TANKA 发行的 campaign URL(例:https://t.tanka.ai/campaign/example-token)同样可以登记。末尾的 token 就是推荐码本身,由管理员手工登记。回传通过此类 URL 注册的用户时,referral_code 请原样填入 token。未登记 token 的用户不要回传,只回传与已登记 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",
"qualification_rule": "first-link-v1"
}- 只有
valid=true才是有效触点。HTTP 200 也可能返回valid=false。不得用无效推荐码覆盖此前有效的来源。 - 系统用上报的
referral_code核对已登记的归因。三个内部 ID 可以省略或为 null;传入时必须与登记内容一致,否则以mapping_mismatch进入待核查状态。 - 发送方保存一致的
touch_id和touched_at,在注册完成时绑定到产品的user_id,并记录bound_at。 - 来源信息需跨域名、邮箱验证和 App/Web 跳转保留。已有用户登录、加入其他团队或注册后点击其他链接,均不计为新注册。
- 按触点发生时的有效性判断。停用链接不会清除停用前已成立的归因。但若首次 Link 时该链接已停用或大使处于暂停状态,奖励会以
inactive_at_qualification进入管理员核查(不作废)。
示例使用初始默认值 last_click / 30 天 / attr-v1。实际映射和适用规则以查询响应为准。
发送完整用户状态#
/integration/v1/events建议 Content-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 的签名字节不同,因此同一event_id重试时也不能改变可选键是否出现,否则返回 409。 - 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,但能够取得时建议发送。每份完整状态和补全 feed 都应携带同一个注册时邮箱。仅在管理员的推荐用户列表展示。 |
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: 不允许 | 接入约定配置 | 表示注册后首次成功完成任意 Link 的规则版本,必须与接收方配置值 first-link-v1 完全一致。查询响应中的 qualification_rule 也返回同一值。 |
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: 有条件 | 发送方归因记录 | 由本系统签发、管理员登记的推荐码,是归因匹配的唯一键。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 完全一致。其他值会以 202 接收,但按 unknown_rule 保留。 |
window_days | 必填null: 不允许 | 约定规则或查询结果 | 适用的归因窗口天数,必须与接收方配置值 30 完全一致。 |
rule_version | 必填null: 不允许 | 约定规则或查询结果 | 适用的归因规则版本,必须与接收方配置值 attr-v1 完全一致。直接使用查询响应中的 rule_version。 |
| 字段 | 发送要求 | 数据来源 | 填写说明 |
|---|---|---|---|
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。
T和Z大写,偏移带冒号(例如+09:00),小数秒最多 6 位。+0900、+09、小写z以及 7 位及以上小数秒会返回 400。跨字段比较(touched_at ≤ signup_at ≤ bound_at ≤ state_updated_at,state_updated_at ≥ 首次 Link)按秒精度进行,同一事件内精度不一致也可接受。state_updated_at按用户单调不减;比已保存状态更早的值即使带 correction 也会被隔离。occurred_at和state_updated_at最多可比接收方当前时刻晚 300 秒。补全可发送真实的历史业务时刻。 - 所有 ID 类字段使用同一字符集。
event_id、user_id、program_id、touch_id、referral_link_id、ambassador_id、campaign_id、rule_version、qualification_rule_version、first_link.event_id、correction.reference_id只允许英文字母、数字、下划线、句点、冒号或连字符([A-Za-z0-9_.:-]),长度 1–160,区分大小写。含|、@、/或空格的值会返回 400。referral_code使用相同字符集,长度 1–64。可选内部 ID 必须使用查询获得的真实 UUID。 - 首次提供的真实
registration_email会被保存。普通事件省略或填 null 不会清除已知的注册时邮箱。更改已知值需要更高状态版本和correction;在更正中明确将这个键设为 null,可以清除错误记录的邮箱。普通(非更正)事件中不同的邮箱值将被忽略,保留首次记录的值。用户后来修改账号邮箱,不改变注册时记录。同一用户、同一state_version在 Webhook 与补全 feed 中的data必须字节级一致。仅时区写法不同(+09:00与Z)或可选键出现与否不同,feed 侧状态就会以VERSION_CONFLICT隔离。 registration_email用于管理员核对推荐转化,不向大使返回。用户身份和奖励去重仍使用user_id。姓名、银行信息、付款金额和订阅信息属于未定义字段,会被拒绝。奖励单价按业务时刻确定。
生成签名并发送请求#
X-Tanka-Key-Id、X-Tanka-Timestamp、X-Tanka-Signature 三个 header 必填。Content-Type 建议使用 application/json。接收方读取的是 body 原始字节而非声明的类型,并对这些字节验证签名。请设置产品化的 User-Agent(例如 ambassador-postback/1.0):边缘网络会以 HTTP 403 拒绝个别库的默认 UA(例如 Python-urllib/3.x),该请求不会到达接收方。
| 字段 | 填写说明 |
|---|---|
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",
// Send an explicit product User-Agent: the edge blocks a few generic library agents (e.g. Python-urllib).
"User-Agent": "ambassador-postback/1.0",
"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",
# Explicit product User-Agent: the edge rejects urllib's default "Python-urllib/x.y" with HTTP 403.
"User-Agent": "ambassador-postback/1.0",
"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。跨字段规则(例如时刻先后关系)的 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。已确认明细可能通过后续差额条目调整。归因在注册时即固定。不带更正的普通事件若把已归属用户改为 pending / unattributed,或更改 referral_code,会以 FACT_CONFLICT 隔离。
补全与对账 API#
大使系统定期通过 GET 拉取接入方 API。接收方会配置您提供的完整拉取 URL 与专用 Bearer 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时点、与本项目相关的全部用户完整状态,包括待归因、无效、撤销和更正。仅汇总人数或仅有效用户列表无法用于对账。曾经回传过的每个 user_id 都必须持续出现在 feed 中。删除或合并的用户以 counting_status=invalid 的更正表达。缺少已知用户会导致RECONCILE_DIFFERENCE,月度确认随之阻塞。 - 每页
items最多 500 条。返回完整事件,或包含environment / program_id / user_id / state_version / state_updated_at / data的完整用户状态。只要有一条不合法(未定义键、environment 或 program_id 不符、业务时刻比接收方当前时刻超出容差地领先),整轮对账即以FEED_ITEM_REJECTED失败,并从快照起点重试。 - 以
next_cursor=null表示结束。最后一页也不能省略next_cursor键,必须显式返回null(缺键则整页失败)。首次请求省略 cursor,后续页原样传入返回的游标。再次返回相同的游标值会以REPEATED_CURSOR失败。空页不能代替结束标记。 - 获取同一快照期间保持
watermark和快照标识不变。所有状态更新时间不得晚于 watermark,避免翻页期间的数据变动造成遗漏。 - 分页 envelope 严格校验。顶层只允许
items、next_cursor、watermark、snapshot_id、complete五个键,出现任何其他键都会使整页失败。snapshot_id可省略,此时用 watermark 标识快照。complete也可省略;传入时须与是否为最后一页一致。 - 每页上限 4 MiB,最多 2,000 页。达到页数上限后仍有下一页时,以
FEED_TOO_LONG失败。请求超时为 30 秒,不跟随重定向。失败后从快照首页重新拉取,已接收数据会去重。 - 月度确认要求最近 24 小时内完成一次完整对账,且 watermark 覆盖上月末。普通 CSV/JSON 导入不能作为完整性证明。
