アトリビューションと 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 自体をヘッダーや 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 に切り替えます。受信側では期限付きで旧鍵も使用できます。再送ごとにタイムスタンプと署名を更新し、元の 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の場合だけ有効な接点です。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 が初めて正常完了した時点。開始・処理中・失敗は対象外です。2 回目以降の 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 キーを削除した body は拒否されます。「条件付き」の条件は各行に記載しています。 first_linkまたはcorrectionがnullの場合、子フィールドは送りません。object を送る場合は、その表にあるすべての子フィールドが必須で、null は不可です。
| フィールド | 送信要件 | 取得元 | 指定方法 |
|---|---|---|---|
schema_version | 必須null: 不可 | 連携設定 | "1.0" 固定。 |
event_id | 必須null: 不可 | 送信側で生成・保存 | 業務イベントの一意で不変の ID。再送では変更せず、内容の訂正には新しい ID を使用します。 |
event_type | 必須null: 不可 | 製品の業務記録 | 上記 4 種類のイベントのいずれか。 |
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 の時刻を含む object。空の object は不可です。 |
milestones.signup_at | 必須null: 不可 | 製品の業務記録 | 実際の登録完了時刻。 |
milestones.first_link_completed_at | 必須null: 条件付き | 製品の業務記録 | 初回 Link の正常完了前は null。完了後は実際の初回成功時刻を送ります。first_link object と必ず同時に値を設定し、再送や後続 Link で変更しません。 |
first_link | 必須null: 条件付き | 製品の業務記録 | 初回 Link の正常完了前は null。完了後は event_id と link_type を含む object が必要です。first_link_completed_at と同時に値を設定します。 |
qualification_rule_version | 必須null: 不可 | 連携設定 | 初期値は first-link-v1。登録後にいずれかの Link が初めて正常完了したことを示します。 |
correction | 必須null: 条件付き | 送信側の訂正記録 | user_state_corrected の場合は object が必要です。それ以外の通常イベントは null にできます。object を送る場合の三つの必須子フィールドは下表に示します。 |
| フィールド | 送信要件 | 取得元 | 指定方法 |
|---|---|---|---|
status | 必須null: 不可 | 送信側の紹介元記録 | attributed / pending / unattributed。 |
ambassador_id | 省略可null: 可 | 受信側の照会結果 | 本システムの大使 ID。不要なら省略または null。送る場合は紹介コード照会が返す UUID をそのまま使用します。 |
referral_link_id | 省略可null: 可 | 受信側の照会結果 | 本システムに登録された紹介リンクの ID。不要なら省略または null。送る場合は照会結果の UUID を使用します。 |
campaign_id | 省略可null: 可 | 受信側の照会結果 | 本システムのキャンペーン ID。不要なら省略または null。送信側の広告キャンペーン ID ではなく、照会結果の UUID を使用します。 |
referral_code | 必須null: 条件付き | 送信側の紹介元記録 | TANKA が発行し管理者が登録した紹介コード。attributed では値が必須で、帰属の唯一の照合キーです。それ以外で不明なら null。長さ1–64。 |
touch_id | 必須null: 条件付き | 送信側の紹介元記録 | 送信側でクリックを識別する ID を生成・保存します。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 | 親が object の場合は必須null: 不可 | 製品の業務記録 | 製品内で初回 Link の正常完了を識別する実際のイベント ID。送信用の外側の event_id とは別の役割です。 |
link_type | 親が object の場合は必須null: 不可 | 製品の業務記録 | 初回に正常完了した実際の Link の種類。推測した値や空文字は送りません。 |
| フィールド | 送信要件 | 取得元 | 指定方法 |
|---|---|---|---|
reason_code | 親が object の場合は必須null: 不可 | 送信側の訂正記録 | 訂正理由を表す空でないコード。 |
reference_id | 親が object の場合は必須null: 不可 | 送信側の訂正記録 | 訂正根拠を追跡できる監査・作業記録などの ID。 |
supersedes_state_version | 親が object の場合は必須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 | HMAC-SHA256 の 64 桁の小文字 16 進ダイジェスト。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 の認証定義はヘッダーを示すもので、署名処理は送信側で実装します。
応答・再送・訂正#
| フィールド | 指定方法 |
|---|---|
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 を保持し、タイムスタンプと署名を更新して、待機時間と揺らぎを入れて再送します。 |
429(エッジから返された場合) | 受信 API 固有の 429 は定義していません。上流から返された場合は Retry-After があれば従い、待機して再送します。 |
送信側で業務データと送信タスクを先に保存し、バックグラウンドで配信します。再送間隔は 5 秒、30 秒、2 分、10 分など段階的に延ばし、揺らぎを加えます。期限を超えた失敗は確認用のキューに移します。タイムアウトだけを理由に新しいイベント ID を生成しません。
同一ユーザーの古いバージョンは新しい状態を上書きしません。同バージョンで内容が異なる場合は非同期処理で隔離します。HTTP 202 は照合の一致を保証しません。新しいイベント ID でも同一ユーザーへの報酬は重複しません。訂正には新しい event_id、より大きい state_version、根拠を追跡できる correction を指定します。確定済み明細は後続の差額で調整する場合があります。
補完・照合 API#
アンバサダーシステムが連携先の API を定期的に GET で取得します。完全な取得先パスは 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は 1 ページ最大 500 件です。完全なイベント、またはenvironment / program_id / user_id / state_version / state_updated_at / dataを含む完全なユーザー状態を返します。next_cursor=nullで終了を示します。最初は cursor を省略し、次ページ以降は返されたカーソルをそのまま渡します。空ページを終了マーカーの代用にしません。- 一つのスナップショットの取得中は
watermarkとスナップショット識別子を固定します。全状態の更新時刻は watermark 以下とし、取得中の変更による欠落を防ぎます。 snapshot_idは省略可能で、その場合は watermark を識別子とします。completeも省略可能ですが、指定時は最終ページかどうかと一致させます。- 1 ページ最大 4 MiB、最大 2,000 ページです。上限を超える後続ページがある場合は
FEED_TOO_LONGで失敗します。タイムアウトは 30 秒で、リダイレクトには追従しません。失敗後はスナップショットの先頭から取得し直し、受信済みデータの重複を排除します。 - 月次確定には直近 24 時間以内の完全な照合と、前月末をカバーする watermark が必要です。通常の CSV/JSON インポートでは完全性の証明を満たしません。