REST reference
POST /api/referral/events — the authoritative referral event-ingest contract.
The referral event-ingest endpoint is the source of truth for the Referral Kit. Your server reports lifecycle events here as referees progress.
POST /api/referral/events
Content-Type: application/json
X-MMOLove-Signature: t=<unix>,v1=sha256=<hex>[,kid=<key-id>]Events
Your kit reports two forward events:
event | When you send it | Effect |
|---|---|---|
registered | The referee creates an account | Mints the referral anchor (first-touch). Requires referee_identity. |
qualified | The referee hits your milestone | Advances the referral; credits the referrer. Reward time. |
A third value, reversed (owner reversal), is accepted and modelled in the state
machine, but the current endpoint only acts on registered / qualified.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
event | string | yes | registered | qualified | reversed. |
token | string | yes | The mmref token your registration page captured. |
server_id | string | yes | Your MMOLove server id (also selects the signing secret). |
referee_identity | string | on registered | The referee's stable identity (account id / username). The anchor key. |
server_event_id | string | yes | Your idempotency key for this event (see Idempotency). |
ts | number | recommended | Unix seconds — your event time (for your own logs/ordering). |
test | boolean | no | true dry-runs: fully signature-verified, but never writes or advances state. Returns { ok: true, test: true }. |
The endpoint reads the raw request body first and verifies the signature over those exact bytes before parsing — so the body you sign must be byte-identical to the body you send.
Signing (X-MMOLove-Signature)
Every request is authenticated with an HMAC-SHA256 over the raw body, keyed by your per-server secret (rotate it any time from the Integration tab).
Header format:
X-MMOLove-Signature: t=<unix>,v1=sha256=<hex>[,kid=<key-id>]t— Unix seconds at signing time. Drives the replay window.v1—sha256=<hex>, where<hex>isHMAC_SHA256(secret, "<t>.<rawBody>"). Note the MAC is computed over the string<t>.<rawBody>(the timestamp, a literal dot, then the raw JSON body).kid— optional key id, for secret rotation. Carried through to your side; the mapping ofkid→ secret is yours to manage.
The #1 integration bug: signing a re-serialized object instead of the exact bytes you send. Re-serialization changes whitespace / key order and the MAC will (correctly) fail. Build the body string once, sign that string, and send that same string.
Replay window
The signature timestamp t must be within ±5 minutes of server time. The MAC
is verified before the clock check (so a forged timestamp can't be used to
probe the window). Outside the window → 401 (stale).
Idempotency
Deduplication is on the tuple (token, type, server_event_id) — never on
token alone (so registered and qualified can both land for one referral).
The event row is inserted before any processing, so a duplicate is absorbed
and returns 200 (never a 4xx). Safe to retry: reuse the same
server_event_id and a replay is a no-op.
First-touch is enforced too: the anchor is minted once per
(server_id, referee_identity). A second referrer registering the same referee is
acknowledged with 200 and ignored ({ ignored: "first_touch_conflict" }).
Status codes
| Code | Meaning |
|---|---|
200 | OK, or an idempotent duplicate / first-touch no-op. Body carries ok: true (+ referral_id, state, or duplicate/test/ignored). |
400 | Malformed — bad/missing header, unparseable body, missing/invalid fields, or registered without referee_identity. |
401 | Signature rejected — bad signature or stale timestamp (outside the ±5-min window). |
404 | Unknown server, referrals not enabled for it, no secret configured, or unknown mmref token. |
422 | Invalid state transition (e.g. qualified before registered). Body carries from + event. |
500 | Internal error. |
Worked example
For a body of {"event":"registered","token":"mmref_abc","server_id":"srv_123","referee_identity":"player42","server_event_id":"evt-1","ts":1733500000}
signed at t=1733500000 with secret s3cr3t, compute:
mac = HMAC_SHA256("s3cr3t", "1733500000.<rawBody>")
header = "t=1733500000,v1=sha256=" + macSkip the boilerplate with an SDK (npm i @mmolove/referral,
pip install mmolove-referral, dotnet add package MMOLove.Referral, or the
single-file PHP drop-in), or see the SDK guides for
complete copy-paste implementations in PHP, Node/JS, Python, .NET/C#, and cURL. The
Signing & security page covers the HMAC scheme in
depth, and this contract is also published as a downloadable
OpenAPI 3.1 spec — see the API reference.