MMOLove Docs
Referral Kit

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.

FieldTypeRequiredDescription
eventstringyesOne of registered, qualified, reversed. Any other value → 400.
tokenstringyesThe mmref token your registration page captured. Non-empty after trimming.
server_idstringyesYour MMOLove server id. Also selects the signing secret server-side.
server_event_idstringyesYour idempotency key for this event. Dedup is on (token, type, server_event_id).
referee_identitystringconditionalRequired on registered (the anchor key). Ignored on later events.
tsnumberrecommendedUnix seconds — your event time, for your own logs/ordering. Not the signing t (that's in the header).
testbooleannotrue 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

FieldTypeRequiredDescription
eventstringyes"registered".
tokenstringyesThe captured mmref token. Must match a referral_clicks row for this server_id, or → 404 (unknown_token).
server_idstringyesYour MMOLove server id.
referee_identitystringyesYour 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_idstringyesIdempotency key for this registration, e.g. reg-<player>.
tsnumberrecommendedYour event time (unix seconds).
{
  "event": "registered",
  "token": "mmref_abc123",
  "server_id": "srv_123",
  "referee_identity": "player42",
  "server_event_id": "reg-player42",
  "ts": 1733500000
}

Responses

OutcomeStatusBody
Anchor minted (or already minted — idempotent)200{ "ok": true, "referral_id": "<uuid>", "state": "registered" }
Exact duplicate event replayed200{ "ok": true, "duplicate": true }
A different referrer already registered this referee200{ "ok": true, "ignored": "first_touch_conflict" }
referee_identity missing400{ "error": "referee_identity is required for a registered event" }
token has no click row for this server404{ "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

FieldTypeRequiredDescription
eventstringyes"qualified".
tokenstringyesThe same token as the registered event (the anchor must already exist).
server_idstringyesYour MMOLove server id.
server_event_idstringyesA new idempotency key, distinct from the registration's, e.g. qual-<player>.
tsnumberrecommendedYour 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

OutcomeStatusBody
Advanced to qualified (or already qualified — idempotent)200{ "ok": true, "referral_id": "<uuid>", "state": "qualified" }
Exact duplicate event replayed200{ "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 on token alone — so registered and qualified can both land for one referral.
  • Resending the exact same (token, type, server_event_id) is absorbed and returns 200 with { "duplicate": true }. No state change.
  • Use a distinct server_event_id per 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

On this page