Events reference
Per-event payload schemas for the referral ingest endpoint — registered and qualified — with field tables, responses, and server_event_id idempotency.
Your kit reports lifecycle events to a single endpoint:
POST /api/referral/events
Content-Type: application/json
X-MMOLove-Signature: t=<unix>,v1=sha256=<hex>[,kid=<key-id>]This page documents the payload for each event type. The transport, signing, and status codes are covered in Signing & security and Errors & troubleshooting; the REST reference is the at-a-glance contract.
Common fields
Every event shares this envelope. The endpoint validates these before anything else.
| Field | Type | Required | Description |
|---|---|---|---|
event | string | yes | One of registered, qualified, reversed. Any other value → 400. |
token | string | yes | The mmref token your registration page captured. Non-empty after trimming. |
server_id | string | yes | Your MMOLove server id. Also selects the signing secret server-side. |
server_event_id | string | yes | Your idempotency key for this event. Dedup is on (token, type, server_event_id). |
referee_identity | string | conditional | Required on registered (the anchor key). Ignored on later events. |
ts | number | recommended | Unix seconds — your event time, for your own logs/ordering. Not the signing t (that's in the header). |
test | boolean | no | true dry-runs: fully signature-verified but never writes or advances state. See Testing & sandbox. |
Empty strings count as missing. token, server_id, server_event_id, and (on
registered) referee_identity are trimmed and must be non-empty, or you get a
400.
Unknown extra fields in the body are not rejected — but remember the whole raw body is signed, so whatever you send must be exactly what you signed.
registered — mint the anchor
Send this as soon as the referred player creates an account. It mints the durable referral anchor and binds the referrer to this referee (first-touch).
Request
| Field | Type | Required | Description |
|---|---|---|---|
event | string | yes | "registered". |
token | string | yes | The captured mmref token. Must match a referral_clicks row for this server_id, or → 404 (unknown_token). |
server_id | string | yes | Your MMOLove server id. |
referee_identity | string | yes | Your stable id for the referee (account id / username — your chosen identity field). The anchor is unique per (server_id, referee_identity). Missing → 400. |
server_event_id | string | yes | Idempotency key for this registration, e.g. reg-<player>. |
ts | number | recommended | Your event time (unix seconds). |
{
"event": "registered",
"token": "mmref_abc123",
"server_id": "srv_123",
"referee_identity": "player42",
"server_event_id": "reg-player42",
"ts": 1733500000
}Responses
| Outcome | Status | Body |
|---|---|---|
| Anchor minted (or already minted — idempotent) | 200 | { "ok": true, "referral_id": "<uuid>", "state": "registered" } |
| Exact duplicate event replayed | 200 | { "ok": true, "duplicate": true } |
| A different referrer already registered this referee | 200 | { "ok": true, "ignored": "first_touch_conflict" } |
referee_identity missing | 400 | { "error": "referee_identity is required for a registered event" } |
token has no click row for this server | 404 | { "error": "unknown referral token for this server" } |
referee_identity must be stable for the lifetime of the player. First-touch
keys off it — a value that changes (a renamable display name) breaks attribution.
Prefer an internal account id.
qualified — the milestone
Send this when the referee reaches your definition of "qualified" (level 10,
2 hours played, first purchase — your call). It advances the referral to
qualified and credits the referrer in the analytics + leaderboard. Then you
grant the in-game reward.
Request
| Field | Type | Required | Description |
|---|---|---|---|
event | string | yes | "qualified". |
token | string | yes | The same token as the registered event (the anchor must already exist). |
server_id | string | yes | Your MMOLove server id. |
server_event_id | string | yes | A new idempotency key, distinct from the registration's, e.g. qual-<player>. |
ts | number | recommended | Your event time (unix seconds). |
referee_identity is not needed here — the anchor is resolved from
(token, server_id).
{
"event": "qualified",
"token": "mmref_abc123",
"server_id": "srv_123",
"server_event_id": "qual-player42",
"ts": 1733600000
}Responses
| Outcome | Status | Body |
|---|---|---|
| Advanced to qualified (or already qualified — idempotent) | 200 | { "ok": true, "referral_id": "<uuid>", "state": "qualified" } |
| Exact duplicate event replayed | 200 | { "ok": true, "duplicate": true } |
qualified arrived with no anchor yet (sent before registered) | 422 | { "error": "invalid state transition", "from": "clicked", "event": "qualified" } |
Order matters: qualified before registered is rejected with 422, not
silently accepted. Always mint the anchor with registered first.
reversed — owner reversal
The endpoint accepts reversed and the state machine models it (advancing a
registered / qualified referral to the terminal reversed state, e.g. after a
refund / chargeback / abuse finding). The current phase models this transition; if
you have a reversal need, see Concepts
for the rules. Like every other event it must be correctly signed and carries the
same envelope (token, server_id, server_event_id).
server_event_id — idempotency
server_event_id is your key for a specific event, and it's how retries stay
safe.
- Dedup is on the tuple
(token, type, server_event_id)— never ontokenalone — soregisteredandqualifiedcan both land for one referral. - Resending the exact same
(token, type, server_event_id)is absorbed and returns200with{ "duplicate": true }. No state change. - Use a distinct
server_event_idper logical event:reg-<player>for the registration,qual-<player>for the qualification.
Pattern: derive it deterministically from your own ids so a retry naturally reuses the same key.
registered → server_event_id = "reg-" + <your account id>
qualified → server_event_id = "qual-" + <your account id>On retry, re-sign with a fresh t (the old one will be stale → 401) but keep
the same server_event_id so the retry is deduped, not double-counted.
See also
Signing & security
The exact X-MMOLove-Signature HMAC scheme — sign over the raw body, the header format, the replay window, idempotency, and secret rotation.
Errors & troubleshooting
Every status code the ingest endpoint returns, with cause and fix — plus the raw-body-signing gotcha and how to debug with the delivery log.