AmbassadorDevelopers
TANKA AMBASSADOR / API v1

Attribution and Postback
Server integration guide

A guide for developers integrating with this service. It covers the data exchanged, authentication and API behavior from shared links through signup and the first successful Link.

BASE URLhttps://ambassador.tanka.aiproduction

Integration overview#

A qualified user has completed signup and then successfully completed a Link of any type for the first time.

Free users are eligible; payment is not a requirement. The reward rule effective at the reported first Link completion time applies. Each user generates a referral reward only once.

  1. 01Share links and retain attribution

    On activation, an administrator registers the referral URL and code issued by this system. The sender stores the touchpoint and attribution, and can look up historical mappings when needed.

  2. 02Send user states

    Send the complete user state after signup, the first successful Link, attribution resolution or a correction.

  3. 03Verify signatures, calculate rewards and reconcile

    Received events are persisted and processed asynchronously, then reconciled against a complete snapshot of the authoritative data.

There are two server integration endpoints: GET /integration/v1/referral-links/{code} for referral lookup and POST /integration/v1/events for sending user states.

Credentials and authentication#

Postback: Key ID + Secret

The Key ID issued by the receiver identifies the key. Store the Secret on the server and use it to calculate each request's HMAC signature. Send only the signature; never include the Secret itself in headers or the body.

Lookup: dedicated Bearer Token

Referral lookup uses a dedicated Token. Scheduled retrieval from the backfill API uses separate credentials issued by that API's provider.

Sender environment variables · example
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>

Use credentials dedicated to this integration; the receiver's own platform admin keys are not needed. Keep production Secrets out of documentation, frontend code, shared links and browsers.

When rotating keys, switch the sender to the new Key ID / Secret. The receiver can accept the old key for a limited period. Refresh the timestamp and signature on every retry while retaining the original event_id and business-data body.

Send the complete user state#

POST/integration/v1/events

Content-Type application/json is recommended. The UTF-8 body limit is 65,536 bytes. Send a complete snapshot with the fixed field structure each time, using null for empty values. Count increments and partial patches are not supported.

When to send
FieldUsage
signup_completedWhen signup completes. Both first_link and first_link_completed_at are null. Signup alone does not generate a referral reward.
first_link_completedWhen a Link of any type first completes successfully after signup. Started, in-progress and failed operations do not qualify. Subsequent Links do not generate referral rewards.
attribution_updatedWhen attribution changes, such as resolving a pending referral. Send a complete snapshot retaining the signup and first Link facts.
user_state_correctedWhen correcting, excluding or restoring recorded facts. Use a new event ID, a higher user state version and supporting correction evidence.

The IDs, timestamps and link_type below illustrate the structure. Replace them with actual events and registered data. This page never sends these examples to the production API automatically.

The first example, codeOnly.json, sets all three internal IDs to null. IDs shown in other examples illustrate optional fields and must be replaced with actual lookup results.

Match by referral_code: three internal IDs are 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
  }
}
Signup complete: first Link not yet completed
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
  }
}
First Link complete: facts required for reward calculation are present
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
  }
}
Attribution pending: null fields remain explicit
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
  }
}
Resolve previously pending attribution
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
  }
}
Correction: exclude an incorrectly counted internal test user
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
    }
  }
}

Fields and business rules#

Every event JSON field is listed below with key presence, null rules and its source. Unknown fields are rejected. OpenAPI JSON, generated from the same validation schema, specifies types, lengths and basic formats.

Required, optional and null

  • Required: include the JSON key. Allowing a null value does not make the key optional.
  • Optional: the key may be left out. ambassador_id, referral_link_id, campaign_id and registration_email are optional. See each row for its source and null behavior. Omitting a key and sending null produce different signed bytes, so a retry of the same event_id must not change whether an optional key is present; doing so returns 409.
  • Null is a value. For example, {"reason_code": null} is valid in a normal event, but removing the reason_code key is rejected. Each conditional null rule is explained in its row.
  • When first_link or correction is null, do not send its child fields. When sending an object, all child fields listed in its table are required and cannot be null.

OpenAPI JSON

Common event fields
FieldRequirementSourceUsage
schema_versionRequirednull: Not allowedIntegration configurationFixed value: "1.0".
event_idRequirednull: Not allowedGenerated by senderA unique, immutable business-event ID. Retain it on retries; use a new ID when correcting the content.
event_typeRequirednull: Not allowedProduct recordsOne of the four event types above.
environmentRequirednull: Not allowedIntegration configurationAccepted value: production. This project uses production only.
program_idRequirednull: Not allowedIntegration configurationFixed value: tanka-jp-ambassador.
user_idRequirednull: Not allowedProduct recordsThe immutable TANKA product user ID. Do not substitute a device ID, team ID or email address.
state_versionRequirednull: Not allowedGenerated by senderA positive integer that increases monotonically per user, up to 9007199254740991. Each user may start at 1.
state_updated_atRequirednull: Not allowedProduct recordsThe last update time of this complete state. It must be at or after the signup and first Link times included in the state.
occurred_atRequirednull: Not allowedProduct recordsThe actual business-event time, rather than the HTTP send or retry time.
dataRequirednull: Not allowedAssembled by senderThe complete user-state object described below.
data: complete user state
FieldRequirementSourceUsage
is_new_userRequirednull: Not allowedProduct recordsA boolean indicating a newly registered user. Do not set it to true for an existing user.
registration_emailOptionalnull: AllowedProduct recordsThe email address used when TANKA signup completed, up to 254 characters. It may be omitted or null, but sending it is recommended when available. Include the same registration-time value in every full state and in the backfill feed. It is shown only in the administrator referral-user list.
counting_statusRequirednull: Not allowedSender validity assessmentvalid / hold / invalid. Even with valid, rewards require a new user, valid attribution and a first successful Link.
reason_codeRequirednull: ConditionalSender validity assessmentWhen counting_status is hold / invalid, a non-empty reason code is required. A normal valid event may use null.
attributionRequirednull: Not allowedSender attribution recordsThe complete attribution object; see the fields below.
milestonesRequirednull: Not allowedProduct recordsObject containing the signup and first Link timestamps. An empty object is not valid.
milestones.signup_atRequirednull: Not allowedProduct recordsThe actual signup completion time.
milestones.first_link_completed_atRequirednull: ConditionalProduct recordsNull before the first successful Link. After completion, send its actual first success time together with the first_link object. Do not change it on retries or later Links.
first_linkRequirednull: ConditionalProduct recordsNull before the first successful Link. After completion, an object containing event_id and link_type is required together with first_link_completed_at.
qualification_rule_versionRequirednull: Not allowedIntegration configurationThe version of the rule denoting the first successful completion of any Link after signup. It must exactly match the receiver value first-link-v1. The lookup response also returns the same value as qualification_rule.
correctionRequirednull: ConditionalSender correction recordsAn object is required for user_state_corrected. Other normal events may use null. Whenever an object is sent, all three child fields listed below are required.
data.attribution: complete attribution
FieldRequirementSourceUsage
statusRequirednull: Not allowedSender attribution recordsattributed / pending / unattributed.
ambassador_idOptionalnull: AllowedReceiver lookup resultThe ambassador ID in this system. Omit it or send null when not needed; otherwise use the UUID returned by referral lookup.
referral_link_idOptionalnull: AllowedReceiver lookup resultThe ID of the shared link registered in this system. Omit it or send null when not needed; otherwise use the lookup UUID.
campaign_idOptionalnull: AllowedReceiver lookup resultThe campaign ID in this system. Omit it or send null when not needed. Use the lookup UUID, not the sender's advertising campaign ID.
referral_codeRequirednull: ConditionalSender attribution recordsThe referral code issued by this system and registered by an administrator. A value is required for attributed and is the sole key used to match attribution. Otherwise null is allowed when unknown. Length 1-64.
touch_idRequirednull: ConditionalSender attribution recordsAn ID generated and stored by the sender to identify the click. A value is required for attributed; otherwise null is allowed when unknown.
touched_atRequirednull: ConditionalSender attribution recordsThe actual click time. A value is required for attributed; otherwise null is allowed when unknown. Any supplied value must be at or before signup.
bound_atRequirednull: ConditionalSender attribution recordsThe time attribution was bound or resolved for the registered user. A value is required for attributed; otherwise null is allowed when unknown. It must be at or after signup and at or before state_updated_at.
modelRequirednull: Not allowedConfigured rules / lookupThe applicable attribution model. It must exactly match the receiver value last_click. A different value is accepted with 202 but held as unknown_rule.
window_daysRequirednull: Not allowedConfigured rules / lookupThe attribution window in days. It must exactly match the receiver value 30.
rule_versionRequirednull: Not allowedConfigured rules / lookupThe applicable attribution rule version. It must exactly match the receiver value attr-v1. Use the rule_version from the lookup response unchanged.
data.first_link: fields when sending an object
FieldRequirementSourceUsage
event_idRequired within objectnull: Not allowedProduct recordsThe actual product event ID identifying the first successful Link completion. It serves a different purpose from the outer delivery event_id.
link_typeRequired within objectnull: Not allowedProduct recordsThe actual type of the first successfully completed Link. Do not send a guessed value or an empty string.
data.correction: fields when sending an object
FieldRequirementSourceUsage
reason_codeRequired within objectnull: Not allowedSender correction recordsA non-empty code describing the reason for the correction.
reference_idRequired within objectnull: Not allowedSender correction recordsAn ID that traces the correction to supporting evidence, such as an audit or operation record.
supersedes_state_versionRequired within objectnull: Not allowedSender correction recordsThe user state version being corrected: a positive integer lower than the new state_version.
  • For attributed, referral_code, touch_id, touched_at and bound_at are required. ambassador_id, referral_link_id and campaign_id may be omitted or null.
  • first_link and first_link_completed_at must both have values or both be null. The first Link must be at or after signup.
  • Use RFC3339 timestamps with an explicit offset. T and Z are uppercase, the offset includes a colon (for example +09:00), and fractional seconds have at most 6 digits. +0900, +09, a lowercase z and 7 or more fractional digits are rejected with 400. Cross-field comparisons (touched_at ≤ signup_at ≤ bound_at ≤ state_updated_at, and state_updated_at ≥ first Link) are evaluated at second precision, so mixed precision within one event is tolerated. state_updated_at must never decrease for a user; a value older than the stored state is quarantined even with a correction. occurred_at and state_updated_at may be up to 300 seconds ahead of the receiver's current time. Backfill may contain actual historical business times.
  • All ID-like fields share one character set. event_id, user_id, program_id, touch_id, referral_link_id, ambassador_id, campaign_id, rule_version, qualification_rule_version, first_link.event_id and correction.reference_id contain 1–160 letters, digits, underscores, periods, colons or hyphens ([A-Za-z0-9_.:-]) and are case-sensitive. Values containing |, @, / or spaces are rejected with 400. referral_code uses the same characters with length 1–64. Optional internal IDs must be actual UUIDs obtained from lookup.
  • The first supplied registration_email is saved. Ordinary omission or null does not erase a known registration email. Replacing a known value requires a higher state version and correction. Explicit null for this key in a correction clears an incorrectly recorded email. A different email in an ordinary (non-correction) event is ignored; the first recorded value is kept. Later account email changes do not change the registration-time record. For the same user and state_version, the webhook and the backfill feed must carry byte-identical data. A different offset spelling (+09:00 versus Z) or a difference in optional-key presence alone quarantines the feed copy as VERSION_CONFLICT.
  • registration_email is used by administrators to verify referrals and is not returned to ambassadors. User identity and reward deduplication continue to use user_id. Names, bank details, payment amounts and subscription information are rejected as undefined fields. Reward rates are selected using the business time.

Generate signatures and send requests#

The three headers X-Tanka-Key-Id, X-Tanka-Timestamp and X-Tanka-Signature are required. Content-Type application/json is recommended. The receiver reads the raw body bytes rather than the declared type and verifies the signature over those bytes. Set a product User-Agent (for example ambassador-postback/1.0): the edge network rejects some default library agents (for example Python-urllib/3.x) with HTTP 403 before the request reaches the receiver.

Postback request headers
FieldUsage
Content-Typeapplication/json
X-Tanka-Key-IdUse the issued Key ID, never the Secret.
X-Tanka-TimestampThe send time as an integer string of Unix seconds, not milliseconds.
X-Tanka-SignatureA 64-character lowercase hexadecimal HMAC-SHA256 digest, without a prefix such as sha256=.
Signature formula
message = ASCII(timestamp) + ASCII(".") + raw_body_bytes
signature = lowercase_hex(HMAC_SHA256(UTF8(secret), message))

Send exactly the bytes that were signed. Do not change JSON formatting, line breaks, field order or encoding after signing. The clock tolerance is 300 seconds; keep server clocks synchronized. HTTPS protects transport, while HMAC verifies key possession and body integrity. Duplicate deliveries are deduplicated per event.

Node.js · sign and send the file's exact bytes
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",
    // 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 · generate the same signature with the standard library
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",
    # 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.

The examples read the body from a file and credentials from server environment variables, then send once. Production senders need a persistent queue, failure alerts and retry handling. OpenAPI's security definitions describe the headers; the sender must implement signing.

Responses, retries and corrections#

Handling by HTTP status and error code
FieldUsage
202 · accepted{event_id,status:"accepted"}: the event has been persisted. Delivery is complete, but asynchronous reward processing may still be pending.
200 · duplicate{event_id,status:"duplicate"}: the same event with identical content was already received. Stop retrying.
400 · INVALID_INPUTEach element of issues carries the path of the violated item and a message stating the specific rule in English. For cross-field rules such as time ordering, path is an empty string. Correct the input according to the message and retry. Include the receiver-issued request_id when requesting support.
401 · UNAUTHORIZEDThe response contains only error: UNAUTHORIZED. Check the Key ID, send time, original body and signature. Pause retries while investigating credentials and server clocks.
409 · EVENT_ID_CONFLICTThe content for an existing event ID has changed. Check the send records. Retrying with a different body cannot overwrite the existing event.
413 · INVALID_INPUTThe body exceeds the limit. Adjust the payload and remove unnecessary data.
422 · ENVIRONMENT_MISMATCH / FUTURE_EVENTCheck the production environment and program ID, or correct an invalid future business timestamp.
503 / other 5xx / timeoutThe event may be undelivered, or received with its response lost. Retain event_id and body, refresh the timestamp and signature, and retry with backoff and jitter.
429 (if returned by an edge service)The receiver API does not define its own 429 response. If an upstream service returns one, respect Retry-After when present, wait and retry.

Persist business data and the send task before background delivery. Increase retry delays progressively, for example 5 seconds, 30 seconds, 2 minutes and 10 minutes, with jitter. Move expired failures to a review queue. A timeout alone is not a reason to generate a new event ID.

An older version cannot overwrite a newer state for the same user. Different content at the same version is quarantined during asynchronous processing. HTTP 202 does not guarantee a reconciliation match. A new event ID does not create duplicate rewards for the same user. Corrections require a new event_id, a higher state_version and a traceable correction. Confirmed line items may be adjusted through a later difference entry. Attribution is fixed at signup. An ordinary event without a correction that moves an attributed user to pending / unattributed, or changes the referral_code, is quarantined as FACT_CONFLICT.

Backfill and reconciliation API#

The ambassador system periodically retrieves the integration partner's API with GET. The receiver is configured with the full feed URL and the dedicated Bearer Token you provide.

Request direction: ambassador system → integration partner
GET <partner feed URL>?program_id=tanka-jp-ambassador&limit=500&cursor=<next cursor>
Authorization: Bearer <backfill Token>
Reconciliation API page response
{
  "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
}
  • Return the complete state of every user related to this program as of watermark, including pending attribution, invalidated, revoked and corrected states. Aggregate counts or a list of valid users alone cannot be reconciled. Every user_id ever reported must keep appearing in the feed. Express a deleted or merged user as a correction with counting_status=invalid. A missing known user results in RECONCILE_DIFFERENCE and blocks monthly confirmation.
  • items contains at most 500 entries per page. Return complete events, or complete user states containing environment / program_id / user_id / state_version / state_updated_at / data. A single invalid item (an unknown key, a different environment or program_id, or a business time further ahead of the receiver's current time than the tolerance) fails the whole run with FEED_ITEM_REJECTED, and retrieval restarts from the beginning of the snapshot.
  • next_cursor=null marks the end. Even on the final page, do not omit the next_cursor key; return an explicit null (a missing key fails the whole page). Omit cursor on the first request and pass the returned cursor unchanged for subsequent pages. Returning the same cursor value again fails with REPEATED_CURSOR. An empty page is not an end marker.
  • Keep watermark and the snapshot identifier fixed throughout retrieval of a snapshot. Every state update time must be at or before watermark to prevent omissions caused by changes during pagination.
  • The page envelope is strict. The only allowed top-level keys are items, next_cursor, watermark, snapshot_id and complete; any other key fails the whole page. snapshot_id is optional; when omitted, watermark identifies the snapshot. complete is also optional, but if present it must agree with whether this is the final page.
  • Each page is limited to 4 MiB, with at most 2,000 pages. If another page exists beyond this limit, reconciliation fails with FEED_TOO_LONG. Requests time out after 30 seconds and do not follow redirects. After failure, retrieval restarts at the beginning of the snapshot and already received data is deduplicated.
  • Monthly confirmation requires a complete reconciliation within the last 24 hours and a watermark covering the end of the previous month. Ordinary CSV/JSON imports do not establish completeness.