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.
https://ambassador.tanka.aiproductionIntegration overview#
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.
- 01Share links and retain attribution
An administrator registers the URL and referral code issued by TANKA. The sender stores the touchpoint and attribution, and can look up historical mappings when needed.
- 02Send user states
Send the complete user state after signup, the first successful Link, attribution resolution or a correction.
- 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.
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. Supabase and Resend administrative 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.
Referral lookup and attribution binding#
/integration/v1/referral-links/{code}?at=<RFC3339>TANKA issues the complete referral URL and code; an administrator registers them on activation. The URL is displayed exactly as entered. Allowed domains: *.tanka.ai. The referral parameter ref is used to pre-fill the code when an administrator registers the link.
This lookup API is optional. Supply the actual click time in at to check whether the historical touchpoint was valid.
curl --get "$AMBASSADOR_API_BASE_URL/integration/v1/referral-links/$REFERRAL_CODE" \
--header "Authorization: Bearer $TANKA_LINK_LOOKUP_TOKEN" \
--data-urlencode "at=$TOUCHED_AT"When using lookup, the path parameter code and query parameter at are both required. In the example, set REFERRAL_CODE to the shared link's actual code and TOUCHED_AT to the click time in RFC3339 with a timezone, for example 2026-09-08T00:01:00+09:00. --data-urlencode correctly encodes the plus sign in the timezone.
Successful response · example mapping
{
"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"
}- A touchpoint is valid only when
valid=true. HTTP 200 can also containvalid=false. An invalid code must not overwrite a previously valid attribution. - The submitted
referral_codeis matched against the registered attribution. The three internal IDs may be omitted or null. If supplied, they must match the registration; a mismatch is held for review asmapping_mismatch. - The sender stores a consistent
touch_idandtouched_at, binds them to the product'suser_idwhen signup completes, and recordsbound_at. - Retain attribution across domains, email verification and App/Web transitions. Existing-user logins, joining another team and clicking a different link after signup do not count as new signups.
- Validity is evaluated at the touchpoint time. Disabling a link does not remove attribution established before it was disabled.
Examples use the initial defaults last_click / 30 days / attr-v1. Use the lookup response for the mapping and rules that actually apply.
Send the complete user state#
/integration/v1/eventsUse Content-Type application/json. 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.
| Field | Usage |
|---|---|
signup_completed | When signup completes. Both first_link and first_link_completed_at are null. Signup alone does not generate a referral reward. |
first_link_completed | When 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_updated | When attribution changes, such as resolving a pending referral. Send a complete snapshot retaining the signup and first Link facts. |
user_state_corrected | When 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
{
"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
{
"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
{
"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
{
"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
{
"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
{
"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
nullvalue does not make the key optional. - Optional: the key may be left out.
ambassador_id,referral_link_id,campaign_idandregistration_emailare optional. See each row for its source and null behavior. - 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_linkorcorrectionisnull, do not send its child fields. When sending an object, all child fields listed in its table are required and cannot be null.
| Field | Requirement | Source | Usage |
|---|---|---|---|
schema_version | Requirednull: Not allowed | Integration configuration | Fixed value: "1.0". |
event_id | Requirednull: Not allowed | Generated by sender | A unique, immutable business-event ID. Retain it on retries; use a new ID when correcting the content. |
event_type | Requirednull: Not allowed | Product records | One of the four event types above. |
environment | Requirednull: Not allowed | Integration configuration | Accepted value: production. This project uses production only. |
program_id | Requirednull: Not allowed | Integration configuration | Fixed value: tanka-jp-ambassador. |
user_id | Requirednull: Not allowed | Product records | The immutable TANKA product user ID. Do not substitute a device ID, team ID or email address. |
state_version | Requirednull: Not allowed | Generated by sender | A positive integer that increases monotonically per user, up to 9007199254740991. Each user may start at 1. |
state_updated_at | Requirednull: Not allowed | Product records | The last update time of this complete state. It must be at or after the signup and first Link times included in the state. |
occurred_at | Requirednull: Not allowed | Product records | The actual business-event time, rather than the HTTP send or retry time. |
data | Requirednull: Not allowed | Assembled by sender | The complete user-state object described below. |
| Field | Requirement | Source | Usage |
|---|---|---|---|
is_new_user | Requirednull: Not allowed | Product records | A boolean indicating a newly registered user. Do not set it to true for an existing user. |
registration_email | Optionalnull: Allowed | Product records | The email address used when TANKA signup completed, up to 254 characters. It may be omitted or null for compatibility with older senders. When available, include the same registration-time value in every full state. It is shown only in the administrator referral-user list. |
counting_status | Requirednull: Not allowed | Sender validity assessment | valid / hold / invalid. Even with valid, rewards require a new user, valid attribution and a first successful Link. |
reason_code | Requirednull: Conditional | Sender validity assessment | When counting_status is hold / invalid, a non-empty reason code is required. A normal valid event may use null. |
attribution | Requirednull: Not allowed | Sender attribution records | The complete attribution object; see the fields below. |
milestones | Requirednull: Not allowed | Product records | Object containing the signup and first Link timestamps. An empty object is not valid. |
milestones.signup_at | Requirednull: Not allowed | Product records | The actual signup completion time. |
milestones.first_link_completed_at | Requirednull: Conditional | Product records | Null 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_link | Requirednull: Conditional | Product records | Null 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_version | Requirednull: Not allowed | Integration configuration | Initial value: first-link-v1. It denotes the first successful completion of any Link after signup. |
correction | Requirednull: Conditional | Sender correction records | An 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. |
| Field | Requirement | Source | Usage |
|---|---|---|---|
status | Requirednull: Not allowed | Sender attribution records | attributed / pending / unattributed. |
ambassador_id | Optionalnull: Allowed | Receiver lookup result | The ambassador ID in this system. Omit it or send null when not needed; otherwise use the UUID returned by referral lookup. |
referral_link_id | Optionalnull: Allowed | Receiver lookup result | The ID of the referral link registered in this system. Omit it or send null when not needed; otherwise use the lookup UUID. |
campaign_id | Optionalnull: Allowed | Receiver lookup result | The 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_code | Requirednull: Conditional | Sender attribution records | The referral code issued by TANKA and registered by an administrator. A value is required for attributed and is the sole attribution matching key. Otherwise null is allowed when unknown. Length 1–64. |
touch_id | Requirednull: Conditional | Sender attribution records | An ID generated and stored by the sender to identify the click. A value is required for attributed; otherwise null is allowed when unknown. |
touched_at | Requirednull: Conditional | Sender attribution records | The 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_at | Requirednull: Conditional | Sender attribution records | The 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. |
model | Requirednull: Not allowed | Configured rules / lookup | The applicable attribution model; initially last_click. It must match the receiver configuration. |
window_days | Requirednull: Not allowed | Configured rules / lookup | The attribution window in days; initially 30. It must match the receiver configuration. |
rule_version | Requirednull: Not allowed | Configured rules / lookup | The applicable attribution rule version; initially attr-v1. Use the lookup result or integration configuration. |
| Field | Requirement | Source | Usage |
|---|---|---|---|
event_id | Required within objectnull: Not allowed | Product records | The actual product event ID identifying the first successful Link completion. It serves a different purpose from the outer delivery event_id. |
link_type | Required within objectnull: Not allowed | Product records | The actual type of the first successfully completed Link. Do not send a guessed value or an empty string. |
| Field | Requirement | Source | Usage |
|---|---|---|---|
reason_code | Required within objectnull: Not allowed | Sender correction records | A non-empty code describing the reason for the correction. |
reference_id | Required within objectnull: Not allowed | Sender correction records | An ID that traces the correction to supporting evidence, such as an audit or operation record. |
supersedes_state_version | Required within objectnull: Not allowed | Sender correction records | The 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_linkandfirst_link_completed_atmust both have values or both be null. The first Link must be at or after signup.- Use RFC3339 timestamps with a timezone and up to 6 fractional-second digits.
occurred_atandstate_updated_atmay be up to 300 seconds ahead of the receiver's current time. Backfill may contain actual historical business times. - Ordinary event IDs contain 1–160 letters, digits, underscores, periods, colons or hyphens. Referral codes use the same characters, with length 1–64. Optional internal IDs must be actual UUIDs obtained from lookup.
- The first supplied
registration_emailis saved. Ordinary omission or null does not erase a known registration email. Replacing a known value requires a higher state version andcorrection. Explicit null for this key in a correction clears an incorrectly recorded email. Later account email changes do not change the registration-time record. registration_emailis used by administrators to verify referrals and is not returned to ambassadors. User identity and reward deduplication continue to useuser_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#
All four headers below are required.
| Field | Usage |
|---|---|
Content-Type | application/json |
X-Tanka-Key-Id | Use the issued Key ID, never the Secret. |
X-Tanka-Timestamp | The send time as an integer string of Unix seconds, not milliseconds. |
X-Tanka-Signature | A 64-character lowercase hexadecimal HMAC-SHA256 digest, without a prefix such as sha256=. |
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
// 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 · generate the same signature with the standard library
# 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.
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#
| Field | Usage |
|---|---|
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_INPUT | Inspect path and message in issues, correct the input and retry. Include the receiver-issued request_id when requesting support. |
401 · UNAUTHORIZED | The 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_CONFLICT | The content for an existing event ID has changed. Check the send records. Retrying with a different body cannot overwrite the existing event. |
413 · INVALID_INPUT | The body exceeds the limit. Adjust the payload and remove unnecessary data. |
422 · ENVIRONMENT_MISMATCH / FUTURE_EVENT | Check the production environment and program ID, or correct an invalid future business timestamp. |
503 / other 5xx / timeout | The 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.
Backfill and reconciliation API#
The ambassador system periodically retrieves the integration partner's API with GET. Configure the full endpoint path in TANKA_BACKFILL_BASE_URL and its dedicated Bearer Token in 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
}- 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. itemscontains at most 500 entries per page. Return complete events, or complete user states containingenvironment / program_id / user_id / state_version / state_updated_at / data.next_cursor=nullmarks the end. Omit cursor on the first request; pass the returned cursor unchanged for subsequent pages. An empty page is not an end marker.- Keep
watermarkand 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. snapshot_idis optional; when omitted, watermark identifies the snapshot.completeis 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.