MMOLove Docs
Referral KitSDK guides

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.

A complete Python integration: capture the mmref token, mint the anchor with registered, credit the referrer with qualified, all correctly signed.

The official mmolove-referral package is zero-dependency (stdlib only) and does the signing + POST for you:

pip install mmolove-referral
from mmolove_referral import ReferralClient

client = ReferralClient(
    server_id=os.environ["MMOLOVE_SERVER_ID"],
    secret=os.environ["MMOLOVE_REFERRAL_SECRET"],  # Referrals tab
    # endpoint defaults to https://mmolove.gg/api/referral/events
)

# referee_identity is REQUIRED on registered — the first-touch anchor:
client.registered(token, referee_identity="player-7842", server_event_id="reg-7842")

# When they hit your "qualified" bar:
res = client.qualified(token, server_event_id="qual-7842")
if not res.ok:
    print(res.status, res.body)

registered / qualified return a SendResult (.status, .ok, .body); a 200 covers new events and idempotent duplicates. Non-2xx is surfaced on the result (not raised); only a transport failure raises RuntimeError. Need to sign without sending? Use client.sign(...) or the pure sign_referral_body(...). Or just drop mmolove_referral.py in (no install needed).

The rest of this page is the from-scratch path — a stdlib-only 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.py — load from env; NEVER hardcode secrets.
import os

MMOLOVE_ENDPOINT = "https://mmolove.com/api/referral/events"
MMOLOVE_SECRET = os.environ["MMOLOVE_REFERRAL_SECRET"]  # from the Referrals tab
MMOLOVE_SERVER_ID = os.environ["MMOLOVE_SERVER_ID"]     # your server id

The client

mmolove_referral.py — the whole integration. Build the body once, sign those exact bytes, send those exact bytes.

import hashlib
import hmac
import json
import time
import urllib.error
import urllib.request

ENDPOINT = "https://mmolove.com/api/referral/events"


def mmolove_send_event(secret: str, server_id: str, event: str, extra: dict) -> tuple[int, str]:
    """Report a referral lifecycle event to MMOLove with an HMAC-signed body.

    Returns (status_code, response_body). status_code 0 means a transport error.
    """
    # Build the body ONCE. Sign these exact bytes; send these exact bytes.
    payload = {"event": event, "server_id": server_id, "ts": int(time.time()), **extra}
    raw = json.dumps(payload, separators=(",", ":"))   # compact, stable

    # HMAC-SHA256 over "<t>.<rawBody>" (timestamp, a dot, then the raw body).
    t = int(time.time())
    mac = hmac.new(secret.encode(), f"{t}.{raw}".encode(), hashlib.sha256).hexdigest()
    sig = f"t={t},v1=sha256={mac}"

    req = urllib.request.Request(
        ENDPOINT,
        data=raw.encode(),                              # the SAME bytes we signed
        headers={"Content-Type": "application/json", "X-MMOLove-Signature": sig},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as res:  # noqa: S310 (trusted)
            return res.status, res.read().decode()
    except urllib.error.HTTPError as e:
        # 4xx / 5xx come back here — read the JSON error body.
        return e.code, e.read().decode()
    except urllib.error.URLError as e:
        # Transport failure (DNS/timeout). Treat like a 5xx: retry with backoff.
        return 0, str(e)

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.

from flask import request

# In your registration view, BEFORE creating the account.
def capture_mmref():
    return request.args.get("mmref") or request.cookies.get("mmref")

@app.post("/register")
def register():
    mmref = capture_mmref()
    account = create_account(**request.form, mmref=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.

from mmolove_referral import mmolove_send_event
from config import MMOLOVE_SECRET, MMOLOVE_SERVER_ID

if account.mmref:
    status, body = mmolove_send_event(MMOLOVE_SECRET, MMOLOVE_SERVER_ID, "registered", {
        "token": account.mmref,                  # the captured token
        "referee_identity": str(account.id),     # STABLE id
        "server_event_id": f"reg-{account.id}",  # idempotency key
    })
    handle_result(status, body)                  # 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.

def on_player_qualified(player):
    if not player.mmref:
        return                                   # not a referred player

    status, body = mmolove_send_event(MMOLOVE_SECRET, MMOLOVE_SERVER_ID, "qualified", {
        "token": player.mmref,                   # SAME token as register
        "server_event_id": f"qual-{player.id}",  # NEW idempotency key
    })

    if handle_result(status, body):
        # MMOLove credited the referrer. Now YOU grant the reward in-game.
        grant_referrer_reward(player)

Error handling

import json

def handle_result(status: int, body: str) -> bool:
    """Return True if the event was accepted (any 200)."""
    try:
        data = json.loads(body)
    except ValueError:
        data = {}

    if status == 200:
        # {"ok": true, ...} — advanced, duplicate, first_touch_conflict, or test.
        # All terminal; do NOT retry.
        return True

    if status == 0 or status >= 500:
        enqueue_retry(status, body)              # backoff; same server_event_id
        return False

    # 4xx — a request bug; retrying won't help.
    print(f"[mmolove] event rejected ({status}): {data.get('error', body)}")
    return False
You getMeaningDo
200Accepted (incl. duplicate / first-touch / test)Stop. On qualified, grant the reward.
400Malformed requestFix the payload — Errors.
401Bad signature or stale tUsually the raw-body bug; or wrong/old secret; or clock drift.
404Unknown server/token, or referrals offCheck server_id, enable referrals, verify the token.
422Invalid transition (e.g. qualified-first)Send registered first.
0 / 5xxTransport / server errorRetry with backoff (idempotent via server_event_id).

Test it first

status, body = mmolove_send_event(MMOLOVE_SECRET, MMOLOVE_SERVER_ID, "registered", {
    "token": "<mmref>",
    "referee_identity": "<player>",
    "server_event_id": "test-1",
    "test": True,                                # dry run — verified, not written
})
# Expect: 200, {"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 ship it to a client/browser, and never commit it. If it leaks, rotate it on the Referrals tab.

Next

On this page