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
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.
- 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; 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.
Referral lookup and attribution binding#
/integration/v1/referral-links/{code}?at=<RFC3339>This system issues the referral URL and code (for example https://jp.tanka.ai/?ref=JP-XXXX-XXXX), and an administrator registers them on activation. TANKA can use any code that follows the format as-is. The URL is displayed exactly as registered. Allowed domains: *.tanka.ai. The referral parameter ref is used to pre-fill the code when an administrator registers the link.
Campaign URLs issued by TANKA (for example https://t.tanka.ai/campaign/example-token) can also be registered. The trailing token is the referral code itself and is registered manually by an administrator. When sending attribution for a user who signed up through such a URL, put the token unchanged in referral_code. Do not send users of unregistered tokens; send only users tied to a registered code. The attribution rules and reward conditions are the same as for every other referral 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",
"qualification_rule": "first-link-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. However, if the link is disabled or the ambassador is suspended at the time of the first Link, the reward is held as
inactive_at_qualificationfor administrator review rather than voided.
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/eventsContent-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.
| 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. Omitting a key and sending null produce different signed bytes, so a retry of the sameevent_idmust 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_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, 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_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 | The 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. |
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 shared 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 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_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. It must exactly match the receiver value last_click. A different value is accepted with 202 but held as unknown_rule. |
window_days | Requirednull: Not allowed | Configured rules / lookup | The attribution window in days. It must exactly match the receiver value 30. |
rule_version | Requirednull: Not allowed | Configured rules / lookup | The applicable attribution rule version. It must exactly match the receiver value attr-v1. Use the rule_version from the lookup response unchanged. |
| 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 an explicit offset.
TandZare uppercase, the offset includes a colon (for example+09:00), and fractional seconds have at most 6 digits.+0900,+09, a lowercasezand 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_atmust never decrease for a user; a value older than the stored state is quarantined even with a correction.occurred_atandstate_updated_atmay 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_idandcorrection.reference_idcontain 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_codeuses 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. 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 andstate_version, the webhook and the backfill feed must carry byte-identicaldata. A different offset spelling (+09:00versusZ) or a difference in optional-key presence alone quarantines the feed copy asVERSION_CONFLICT. 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#
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.
| 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",
// 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
# 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#
| 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 | Each 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 · 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. 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.
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. 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 inRECONCILE_DIFFERENCEand blocks monthly confirmation. 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. 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 withFEED_ITEM_REJECTED, and retrieval restarts from the beginning of the snapshot.next_cursor=nullmarks the end. Even on the final page, do not omit thenext_cursorkey; return an explicitnull(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 withREPEATED_CURSOR. 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. - The page envelope is strict. The only allowed top-level keys are
items,next_cursor,watermark,snapshot_idandcomplete; any other key fails the whole page.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.
