TANKA アンバサダー / API v1

アトリビューションと Postback
サーバー連携ガイド

連携先の開発者向けガイドです。共有リンクから登録、初回 Link まで、送受信するデータ、認証方法、API の動作を説明します。

BASE URLhttps://ambassador.tanka.aiproduction

連携の概要#

有効ユーザー = 登録完了後、いずれかの形式の Link を初めて正常に完了したユーザー。

無料ユーザーも対象で、課金は条件ではありません。送信された初回 Link の完了時刻に有効な報酬ルールを適用します。同一ユーザーの紹介報酬は一度だけ発生します。

  1. 01リンク共有と紹介元の保存

    管理者が TANKA 発行の URL と紹介コードを登録します。送信元で接点と帰属を保存し、必要に応じて過去の対応関係を照会します。

  2. 02利用状況の送信

    登録、初回 Link、帰属の確定・訂正後に、ユーザー状態全体を送信します。

  3. 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 は保持します。

ユーザー状態全体の送信#

POST/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 が初めて正常完了した時点。開始・処理中・失敗は対象外です。2 回目以降の Link では紹介報酬は発生しません。
attribution_updated確認待ちだった紹介元の確定など、帰属の更新時。登録と初回 Link の事実を保持した全体スナップショットを送信します。
user_state_corrected記録した事実の訂正、除外、復元時。新しいイベント ID、より大きいユーザーバージョン、correction の根拠を指定します。

以下の ID、時刻、link_type は構造を示す例です。実際のイベントと登録情報に置き換えて使用します。このページから本番 API に例を自動送信することはありません。

最初の codeOnly.json は三つの内部 ID を null にした例です。他の例にある ID は任意フィールドの記入例であり、実際の照会結果に置き換えます。

referral_code による照合:三つの内部 ID は null
codeOnly.json
{
  "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 は未完了
signup.json
{
  "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 完了:報酬計算に必要な事実が揃った状態
firstLink.json
{
  "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 のフィールドも明示
pendingSignup.json
{
  "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
  }
}
確認待ちの紹介元を後から確定
attributionUpdated.json
{
  "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
  }
}
訂正:誤って記録した社内テストユーザーを除外
corrected.json
{
  "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_idreferral_link_idcampaign_idregistration_email が省略可能です。それぞれの取得元と null の扱いは下表を参照してください。
  • null は値です。たとえば通常の {"reason_code": null} は有効ですが、reason_code キーを削除した body は拒否されます。「条件付き」の条件は各行に記載しています。
  • first_link または correctionnull の場合、子フィールドは送りません。object を送る場合は、その表にあるすべての子フィールドが必須で、null は不可です。

OpenAPI JSON

イベントの共通フィールド
フィールド送信要件取得元指定方法
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: 不可送信側で組み立て以下の完全なユーザー状態オブジェクト。
data:完全なユーザー状態
フィールド送信要件取得元指定方法
is_new_user必須null: 不可製品の業務記録新規登録ユーザーに該当するかを示す boolean。既存ユーザーを true にしません。
registration_email省略可null: 製品の業務記録TANKA の登録完了時に使用したメールアドレス。最大254文字。旧形式との互換性のため省略・null 可。取得できる場合は、毎回の完全な状態に同じ登録時の値を含めてください。管理者の紹介ユーザー一覧だけに表示します。
counting_status必須null: 不可送信側の有効性判断valid / hold / invalid。valid でも、新規ユーザー・有効な帰属・初回 Link の条件を満たした場合に報酬を計算します。
reason_code必須null: 条件付き送信側の有効性判断counting_statushold / 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 を送る場合の三つの必須子フィールドは下表に示します。
data.attribution:完全な紹介元情報
フィールド送信要件取得元指定方法
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。照会結果または連携設定を使用します。
data.first_link:object を送る場合の子フィールド
フィールド送信要件取得元指定方法
event_id親が object の場合は必須null: 不可製品の業務記録製品内で初回 Link の正常完了を識別する実際のイベント ID。送信用の外側の event_id とは別の役割です。
link_type親が object の場合は必須null: 不可製品の業務記録初回に正常完了した実際の Link の種類。推測した値や空文字は送りません。
data.correction:object を送る場合の子フィールド
フィールド送信要件取得元指定方法
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_linkfirst_link_completed_at は両方とも値を持つか、両方とも null にします。初回 Link は登録時刻以降です。
  • 時刻はタイムゾーン付き RFC3339、小数秒は最大 6 桁です。occurred_atstate_updated_at は受信側の現在時刻より最大 300 秒後まで許可します。補完では実際の過去の業務時刻を送信できます。
  • 通常のイベント ID は英数字、アンダースコア、ピリオド、コロン、ハイフンで 1–160 文字です。紹介コードも同じ文字種で 1–64 文字です。任意の内部 ID を送る場合は照会で取得した実際の UUID を指定します。
  • registration_email の最初の実値を保存します。通常の省略・null は既知の登録時メールを消しません。既知の値を変更するには、より大きい状態バージョンと correction が必要です。訂正でこのキーを明示的に null にすると、誤って登録したメールを消去できます。ユーザーが登録後にメールを変更しても、登録時の記録は変えません。
  • registration_email は管理者による紹介実績の確認に使用し、大使には返しません。ユーザーの同一性と報酬の重複排除には引き続き user_id を使用します。氏名、銀行情報、支払額、サブスクリプション情報は未定義フィールドとして拒否します。報酬単価は業務時刻から決定します。

署名の生成と送信#

以下の四つの header はすべて必須です。

Postback リクエストヘッダー
フィールド指定方法
Content-Typeapplication/json
X-Tanka-Key-Id別途受け取った Key ID を指定します。Secret は指定しません。
X-Tanka-Timestamp送信時点の Unix 秒を整数の文字列で指定します。ミリ秒は使用しません。
X-Tanka-SignatureHMAC-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 · ファイルのバイト列に署名してそのまま送信
postback.mjs
// 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 · 標準ライブラリで同じ署名を生成
postback.py
# 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 の認証定義はヘッダーを示すもので、署名処理は送信側で実装します。

応答・再送・訂正#

HTTP ステータスと error に応じた処理
フィールド指定方法
202 · accepted{event_id,status:"accepted"}:受信内容を永続化済みです。送信完了として扱えますが、報酬は非同期処理のため計上済みとは限りません。
200 · duplicate{event_id,status:"duplicate"}:同じ内容の同一イベントを受信済みです。再送を終了します。
400 · INVALID_INPUTissues の path と message を確認し、修正して再送します。問い合わせ時は受信側が発行した request_id を添えてください。
401 · UNAUTHORIZED応答は error: UNAUTHORIZED のみです。Key ID、送信時刻、元の body、署名を確認します。再送を一旦止め、認証情報やサーバー時刻を調査します。
409 · EVENT_ID_CONFLICT同一イベント ID の内容が変更されています。送信記録を確認してください。body を変更した再送で既存イベントを上書きできません。
413 · INVALID_INPUTbody が上限を超えています。イベント構造を調整し、不要な情報を除いてください。
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>
照合 API のページ応答
{
  "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 インポートでは完全性の証明を満たしません。