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.
This is the complete PHP integration — one self-contained file you can drop next
to your existing MMOLove vote-callback handler. It captures the mmref token,
mints the anchor with registered, and credits the referrer with qualified, all
correctly signed.
PHP is the first-class path because it matches the vote-callback pattern you already run. If you've wired the heart/reward callback, this will feel identical.
Use the drop-in (recommended)
The official mmolove-referral.php is a single-file drop-in — no
Composer needed. Vendor the file, require it, and call one function:
require 'mmolove-referral.php';
// referee_identity is REQUIRED on registered — the first-touch anchor:
$res = mmolove_referral_send($MMOLOVE_SECRET, $MMOLOVE_SERVER_ID, 'registered', $mmref, [
'referee_identity' => 'player-7842',
'server_event_id' => 'reg-7842', // idempotency key, stable across retries
]);
// When they hit your "qualified" bar:
$res = mmolove_referral_send($MMOLOVE_SECRET, $MMOLOVE_SERVER_ID, 'qualified', $mmref, [
'server_event_id' => 'qual-7842',
]);
if ($res['status'] !== 200) {
error_log("referral send failed: {$res['status']} " . json_encode($res['body']));
}mmolove_referral_send($secret, $serverId, $event, $token, $opts) returns
['status' => int, 'body' => mixed, 'raw_body' => string, 'header' => string, 'error' => string|null].
A 200 covers new events and idempotent duplicates (same server_event_id).
Need to sign without sending? Use mmolove_referral_build_body(...) +
mmolove_referral_sign(...) and POST the exact raw_body bytes.
The rest of this page is the from-scratch version of that same file, if you'd rather paste it inline and tweak it.
What you need
- Your signing secret and server id from the Referrals tab.
- A registration page that can read a query param / cookie.
- A milestone hook (where you already decide a player has "made it").
<?php
// config — load from env / secrets manager; NEVER hardcode in a public repo.
const MMOLOVE_ENDPOINT = 'https://mmolove.com/api/referral/events';
$MMOLOVE_SECRET = getenv('MMOLOVE_REFERRAL_SECRET'); // from the Referrals tab
$MMOLOVE_SERVER_ID = getenv('MMOLOVE_SERVER_ID'); // your server idThe drop-in helper
mmolove-referral.php — the whole integration. Build the body once, sign
those exact bytes, send those exact bytes.
<?php
/**
* mmolove-referral.php — single-file Referral Kit client (vote-callback style).
*
* Reports a referral lifecycle event to MMOLove with an HMAC-signed body.
* @return array{status:int, body:string}
*/
function mmolove_send_event(string $secret, string $serverId, string $event, array $extra): array
{
// Build the body ONCE. We sign these exact bytes and send these exact bytes.
$payload = array_merge([
'event' => $event, // 'registered' | 'qualified'
'server_id' => $serverId,
'ts' => time(), // your event time (your logs/ordering)
], $extra);
$raw = json_encode($payload, JSON_UNESCAPED_SLASHES);
// HMAC-SHA256 over "<t>.<rawBody>" (timestamp, a dot, then the raw body).
$t = time();
$mac = hash_hmac('sha256', $t . '.' . $raw, $secret); // lowercase hex
$sig = "t={$t},v1=sha256={$mac}";
$ch = curl_init(MMOLOVE_ENDPOINT);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $raw, // <-- the SAME bytes we signed
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"X-MMOLove-Signature: {$sig}",
],
]);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($body === false) {
// Transport failure (DNS/timeout). Treat like a 5xx: retry with backoff.
return ['status' => 0, 'body' => $err];
}
return ['status' => $status, 'body' => $body];
}1. Capture the mmref token at registration
When MMOLove redirects a referred player to your registration page it appends
?mmref=<token> and also drops a first-party mmref cookie. Read it (query
first, cookie fallback) and persist it against the new account.
<?php
// On your registration page, BEFORE creating the account.
$mmref = $_GET['mmref'] ?? ($_COOKIE['mmref'] ?? null);
if ($mmref !== null && $mmref !== '') {
// Store it on the new account row so you can echo it back below.
// e.g. INSERT INTO accounts (..., mmref) VALUES (..., :mmref)
$newAccount->mmref = $mmref;
}2. Report registered — mint the anchor
Right after the account is created, fire registered. This binds the referrer to
this referee (first-touch) and mints the durable referral_id. Use a stable
identity (an internal account id, not a renamable display name).
<?php
if (!empty($newAccount->mmref)) {
$res = mmolove_send_event($MMOLOVE_SECRET, $MMOLOVE_SERVER_ID, 'registered', [
'token' => $newAccount->mmref, // the captured token
'referee_identity' => (string) $newAccount->id, // STABLE id
'server_event_id' => 'reg-' . $newAccount->id, // idempotency key
]);
mmolove_handle_result($res); // see error handling below
}3. Report qualified at your milestone
When the referee reaches your definition of "qualified" (level 10, 2h played,
first purchase — your call), fire qualified. Reuse the same token; give it a
new server_event_id. Then grant the in-game reward to the referrer.
<?php
function on_player_qualified(object $player): void
{
global $MMOLOVE_SECRET, $MMOLOVE_SERVER_ID;
if (empty($player->mmref)) {
return; // not a referred player — nothing to report
}
$res = mmolove_send_event($MMOLOVE_SECRET, $MMOLOVE_SERVER_ID, 'qualified', [
'token' => $player->mmref, // SAME token as register
'server_event_id' => 'qual-' . $player->id, // NEW idempotency key
]);
if (mmolove_handle_result($res)) {
// MMOLove credited the referrer. Now YOU grant the reward in-game.
grant_referrer_reward_for($player);
}
}Error handling
Decode the response and branch on the status. 200 covers several success shapes
(advanced, duplicate, first-touch-ignored) — treat them all as "stop retrying."
<?php
/** @return bool true if the event was accepted (a 200 of any flavour). */
function mmolove_handle_result(array $res): bool
{
$status = $res['status'];
$data = json_decode($res['body'] ?: 'null', true);
if ($status === 200) {
// {ok:true,...} — advanced, OR duplicate, OR first_touch_conflict, OR test.
// All are terminal; do NOT retry.
return true;
}
if ($status === 0 || $status >= 500) {
// Transport/5xx — safe to retry with backoff (same server_event_id).
mmolove_enqueue_retry($res);
return false;
}
// 4xx — a bug in the request; retrying won't help. Log and fix.
$err = $data['error'] ?? 'unknown';
error_log("[mmolove] event rejected ({$status}): {$err}");
return false;
}| You get | What it means | Do |
|---|---|---|
200 | Accepted (incl. duplicate / first-touch / test) | Stop. On qualified, grant the reward. |
400 | Malformed request | Fix the payload — see Errors. |
401 | Bad signature or stale t | Almost always 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 before qualified. |
0 / 5xx | Transport / server error | Retry with backoff (idempotent via server_event_id). |
Test it first
Add 'test' => true to dry-run — fully signature-verified, never written:
<?php
$res = mmolove_send_event($MMOLOVE_SECRET, $MMOLOVE_SERVER_ID, 'registered', [
'token' => '<mmref>',
'referee_identity' => '<player>',
'server_event_id' => 'test-1',
'test' => true,
]);
// Expect: status 200, body {"ok":true,"test":true}See Testing & sandbox for the full smoke-test sequence and the dashboard Send test event button.
Keep $MMOLOVE_SECRET server-side. Never expose it in client/browser code, and
never commit it. If it leaks, rotate it on the Referrals tab (which invalidates the
old one immediately).
Next
Testing & sandbox
Dry-run referral events with test:true, use the dashboard "Send test event" button, and read the delivery log to confirm your wiring.
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.