Node / JS
A complete, copy-paste Node.js integration for the Referral Kit — one module using fetch + node:crypto, with signing, error handling, and a full Express example.
A complete Node.js integration: capture the mmref token, mint the anchor with
registered, credit the referrer with qualified — all correctly signed.
Use the package (recommended)
The fastest path is the official @mmolove/referral package — it
does the signing, the canonical body serialisation, and the POST for you:
npm i @mmolove/referralimport { ReferralClient } from "@mmolove/referral";
const client = new ReferralClient({
serverId: process.env.MMOLOVE_SERVER_ID,
secret: process.env.MMOLOVE_REFERRAL_SECRET, // Referrals tab
// endpoint defaults to https://mmolove.gg/api/referral/events
});
// When a referred player signs up (refereeIdentity is REQUIRED — the first-touch anchor):
await client.registered({ token, refereeIdentity: "player-7842", serverEventId: "reg-7842" });
// When they hit your "qualified" bar:
const res = await client.qualified({ token, serverEventId: "qual-7842" });
if (!res.ok) console.error(res.status, res.body);registered / qualified return { status, ok, body }; a 200 covers both new
events and idempotent duplicates (same serverEventId), so retries are safe. Need
to sign without sending (queue it, POST elsewhere)? Use client.sign(...) or the
pure signReferralBody(...) helper. Requires Node ≥ 18 (global fetch).
The rest of this page is the from-scratch path — a single dependency-free module if you'd rather not add the package. It's exactly what the SDK wraps.
What you need
- Your signing secret and server id from the Referrals tab.
- A registration route that can read a query param / cookie.
- A milestone hook (where you decide a player has "qualified").
// config.js — load from env; NEVER hardcode secrets.
export const MMOLOVE_ENDPOINT = "https://mmolove.com/api/referral/events";
export const MMOLOVE_SECRET = process.env.MMOLOVE_REFERRAL_SECRET; // Referrals tab
export const MMOLOVE_SERVER_ID = process.env.MMOLOVE_SERVER_ID; // your server idThe module
mmolove-referral.js — the whole client. Build the body once, sign those
exact bytes, send those exact bytes.
import crypto from "node:crypto";
const ENDPOINT = "https://mmolove.com/api/referral/events";
/**
* Report a referral lifecycle event to MMOLove with an HMAC-signed body.
* @returns {Promise<{status:number, body:string}>}
*/
export async function mmoloveSendEvent(secret, serverId, event, extra) {
// Build the body ONCE. Sign these exact bytes; send these exact bytes.
const payload = {
event, // "registered" | "qualified"
server_id: serverId,
ts: Math.floor(Date.now() / 1000), // your event time
...extra,
};
const raw = JSON.stringify(payload);
// HMAC-SHA256 over "<t>.<rawBody>" (timestamp, a dot, then the raw body).
const t = Math.floor(Date.now() / 1000);
const mac = crypto.createHmac("sha256", secret).update(`${t}.${raw}`).digest("hex");
const sig = `t=${t},v1=sha256=${mac}`;
try {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-MMOLove-Signature": sig,
},
body: raw, // <-- the SAME bytes we signed
});
return { status: res.status, body: await res.text() };
} catch (err) {
// Transport failure (DNS/timeout/network). Treat like a 5xx: retry w/ backoff.
return { status: 0, body: String(err) };
}
}1. Capture the mmref token at registration
MMOLove redirects referred players to your registration page with ?mmref=<token>
(and a first-party mmref cookie as fallback). Read it and persist it on the new
account.
// Express-style handler, BEFORE creating the account.
function captureMmref(req) {
// Query first, cookie fallback (needs cookie-parser for req.cookies).
return req.query.mmref ?? req.cookies?.mmref ?? null;
}
app.post("/register", async (req, res) => {
const mmref = captureMmref(req);
const account = await createAccount({ ...req.body, mmref }); // store it
// ... continue to step 2
});2. Report registered — mint the anchor
Right after the account exists, fire registered. This binds the referrer to this
referee (first-touch). Use a stable identity (internal account id), not a
renamable name.
import { mmoloveSendEvent } from "./mmolove-referral.js";
import { MMOLOVE_SECRET, MMOLOVE_SERVER_ID } from "./config.js";
if (account.mmref) {
const res = await mmoloveSendEvent(MMOLOVE_SECRET, MMOLOVE_SERVER_ID, "registered", {
token: account.mmref, // the captured token
referee_identity: String(account.id), // STABLE id
server_event_id: `reg-${account.id}`, // idempotency key
});
handleResult(res); // see error handling below
}3. Report qualified at your milestone
When the referee reaches your milestone, fire qualified with the same
token and a new server_event_id. Then grant the in-game reward.
async function onPlayerQualified(player) {
if (!player.mmref) return; // not a referred player
const res = await mmoloveSendEvent(MMOLOVE_SECRET, MMOLOVE_SERVER_ID, "qualified", {
token: player.mmref, // SAME token as register
server_event_id: `qual-${player.id}`, // NEW idempotency key
});
if (handleResult(res)) {
// MMOLove credited the referrer. Now YOU grant the reward in-game.
await grantReferrerReward(player);
}
}Error handling
/** @returns {boolean} true if the event was accepted (any 200). */
function handleResult({ status, body }) {
let data = null;
try { data = JSON.parse(body); } catch { /* non-JSON */ }
if (status === 200) {
// {ok:true,...} — advanced, duplicate, first_touch_conflict, or test.
// All terminal; do NOT retry.
return true;
}
if (status === 0 || status >= 500) {
enqueueRetry({ status, body }); // backoff; same server_event_id
return false;
}
// 4xx — a request bug; retrying won't help.
console.error(`[mmolove] event rejected (${status}):`, data?.error ?? body);
return false;
}| You get | Meaning | Do |
|---|---|---|
200 | Accepted (incl. duplicate / first-touch / test) | Stop. On qualified, grant the reward. |
400 | Malformed request | Fix the payload — Errors. |
401 | Bad signature or stale t | Usually the raw-body bug; or wrong/old secret; or clock drift. |
404 | Unknown server/token, or referrals off | Check server_id, enable referrals, verify the token. |
422 | Invalid transition (e.g. qualified-first) | Send registered first. |
0 / 5xx | Transport / server error | Retry with backoff (idempotent via server_event_id). |
Test it first
const res = await mmoloveSendEvent(MMOLOVE_SECRET, MMOLOVE_SERVER_ID, "registered", {
token: "<mmref>",
referee_identity: "<player>",
server_event_id: "test-1",
test: true, // dry run — verified, not written
});
// Expect: { status: 200, body: '{"ok":true,"test":true}' }See Testing & sandbox for the full sequence and the dashboard Send test event button.
MMOLOVE_SECRET is server-side only. Never bundle it into client/browser code,
and never commit it. If it leaks, rotate it on the Referrals tab.
Next
PHP
A complete, copy-paste PHP integration for the Referral Kit — a single-file drop-in in the vote-callback style, with signing, error handling, and a full worked example.
Python
A complete, copy-paste Python integration for the Referral Kit — stdlib only (hmac + hashlib + urllib), with signing, error handling, and a full Flask example.