MMOLove Docs

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.
  • v1sha256=<hex>. Note the sha256= prefix (the reward callback omits it).
  • kidoptional 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

FieldTypeRequiredNotes
eventstringyesregistered | qualified | reversed.
tokenstringyesThe mmref token your registration page captured.
server_idstringyesYour MMOLove server id (also selects the signing secret).
referee_identitystringon registeredThe referee's stable identity (the first-touch anchor key).
server_event_idstringyesYour idempotency key — dedup is on (token, type, server_event_id).
tsintegerrecommendedUnix seconds — your event time (not the signing t).
testbooleannotrue 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

StatusMeaningExample body
200Accepted — advanced, duplicate, first-touch no-op, or test. Never retry a 200.{ "ok": true, "referral_id": "<uuid>", "state": "registered" }
400Malformed — bad/missing header, unparseable body, missing/invalid field, or registered without referee_identity.{ "error": "referee_identity is required for a registered event" }
401Signature rejected — bad signature or stale t (outside ±5 min).{ "error": "signature rejected: stale" }
404Unknown server, referrals not enabled, no secret, or unknown mmref token.{ "error": "unknown referral token for this server" }
422Invalid state transition (e.g. qualified before registered).{ "error": "invalid state transition", "from": "clicked", "event": "qualified" }
500Internal 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
}
FieldTypeAlways presentNotes
eventstringyesheart.counted (real) | heart.test (dashboard test).
server_idstring (uuid)yesThe server the heart was for.
usernamestringyesThe voter's in-game name — the reward recipient.
heart_idstring (uuid)yesThe vote's id — use it as your idempotency key.
periodstringyesRanking period, YYYY-MM (UTC).
timestampintegeryesUnix seconds at delivery; also the signature t.
streak_dayintegernoCurrent 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-client

Or skip codegen entirely — the SDKs wrap this contract in ready-to-install packages for Node, PHP, Python, and .NET.

On this page