API reference
The MMOLove Referral API — POST /api/referral/events and the heart.counted reward-callback webhook, with the full request/response contract and a downloadable OpenAPI 3.1 spec.
The machine-readable contract for the Referral API. This page mirrors the OpenAPI 3.1 spec — download it to drive Postman / Insomnia, a client generator, or your own tooling.
Download: /openapi/referral.json — an OpenAPI 3.1
document covering the ingest endpoint and the reward-callback webhook. Import it
into Postman, Insomnia, Stoplight, or openapi-generator to scaffold a client.
The prose references — REST reference, Events, and the reward-callback contract — remain the friendly, worked-example reads. This page is the at-a-glance, spec-aligned summary.
POST /api/referral/events
Your server's backend reports a signed referral lifecycle event as a referred player progresses. MMOLove handles attribution; you own the in-game economy.
POST /api/referral/events
Content-Type: application/json
X-MMOLove-Signature: t=<unix>,v1=sha256=<hex>[,kid=<key-id>]Security — X-MMOLove-Signature
HMAC-SHA256 over the raw request body, keyed by your per-server referral secret (Dashboard → Referrals tab):
v1 = HMAC_SHA256(secret, "<t>.<rawBody>") // lower-case hex
hdr = "t=" + t + ",v1=sha256=" + v1 [ + ",kid=" + kid ]t— Unix seconds at signing time; drives the ±5-minute replay window.v1—sha256=<hex>. Note thesha256=prefix (the reward callback omits it).kid— optional key id for secret rotation.
The endpoint reads the raw bytes and verifies the MAC before parsing JSON, so the body you sign must be byte-identical to the body you send. Re-serialising the parsed object (changing whitespace / key order) is the #1 integration bug.
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 (the first-touch anchor key). |
server_event_id | string | yes | Your idempotency key — dedup is on (token, type, server_event_id). |
ts | integer | recommended | Unix seconds — your event time (not the signing t). |
test | boolean | no | true dry-runs: signature-verified, never written. Returns { ok: true, test: true }. |
{
"event": "registered",
"token": "mmref_abc123",
"server_id": "srv_123",
"referee_identity": "player42",
"server_event_id": "reg-player42",
"ts": 1733500000
}Responses
| Status | Meaning | Example body |
|---|---|---|
200 | Accepted — advanced, duplicate, first-touch no-op, or test. Never retry a 200. | { "ok": true, "referral_id": "<uuid>", "state": "registered" } |
400 | Malformed — bad/missing header, unparseable body, missing/invalid field, or registered without referee_identity. | { "error": "referee_identity is required for a registered event" } |
401 | Signature rejected — bad signature or stale t (outside ±5 min). | { "error": "signature rejected: stale" } |
404 | Unknown server, referrals not enabled, no secret, or unknown mmref token. | { "error": "unknown referral token for this server" } |
422 | Invalid state transition (e.g. qualified before registered). | { "error": "invalid state transition", "from": "clicked", "event": "qualified" } |
500 | Internal error. Safe to retry with backoff (idempotent via server_event_id). | { "error": "internal error" } |
The 200 body varies with the outcome: { ok, referral_id, state } (advanced),
{ ok, duplicate: true } (idempotent replay), { ok, ignored: "first_touch_conflict" }
(another referrer already registered this referee), or { ok, test: true } (dry-run).
Webhook — heart.counted (reward callback)
Inbound to your server (MMOLove → you). When a player hearts your listing and
the vote counts, MMOLove POSTs this signed JSON to your configured reward
callback URL. Verify it, then reward username.
POST <your callback URL>
Content-Type: application/json
X-MMOLove-Event: heart.counted
X-MMOLove-Signature: t=<unix>,v1=<hex>The MAC core is identical — HMAC_SHA256(secret, "<t>.<rawBody>") over the raw
body — but v1 here is a bare hex (no sha256= prefix). See the
reward-callback signing notes.
{
"event": "heart.counted",
"server_id": "8f0e…-server-uuid",
"username": "PlayerOne",
"heart_id": "1a2b…-heart-uuid",
"period": "2026-06",
"timestamp": 1733500000,
"streak_day": 7
}| Field | Type | Always present | Notes |
|---|---|---|---|
event | string | yes | heart.counted (real) | heart.test (dashboard test). |
server_id | string (uuid) | yes | The server the heart was for. |
username | string | yes | The voter's in-game name — the reward recipient. |
heart_id | string (uuid) | yes | The vote's id — use it as your idempotency key. |
period | string | yes | Ranking period, YYYY-MM (UTC). |
timestamp | integer | yes | Unix seconds at delivery; also the signature t. |
streak_day | integer | no | Current daily streak. Omitted when unavailable — treat as "no info", not zero. |
Return any 2xx to mark the delivery delivered; a non-2xx or timeout is
retried with backoff. Key your grant on heart_id so a retry never double-rewards.
Use the spec
# Generate a typed client from the OpenAPI document
npx @openapitools/openapi-generator-cli generate \
-i https://mmolove.com/openapi/referral.json \
-g typescript-fetch -o ./mmolove-clientOr skip codegen entirely — the SDKs wrap this contract in ready-to-install packages for Node, PHP, Python, and .NET.