.NET / C#
A complete, copy-paste .NET integration for the Referral Kit — HMACSHA256 + HttpClient, with signing, error handling, and a full ASP.NET example.
A complete .NET integration: capture the mmref token, mint the anchor with
registered, credit the referrer with qualified — all correctly signed.
Use the package (recommended)
The official MMOLove.Referral package (net8.0, zero NuGet deps)
does the signing + POST for you:
dotnet add package MMOLove.Referralusing MMOLove.Referral;
var client = new ReferralClient(
serverId: builder.Configuration["MMOLove:ServerId"]!,
secret: builder.Configuration["MMOLove:ReferralSecret"]!); // Referrals tab
// endpoint defaults to https://mmolove.gg/api/referral/events
// pass an HttpClient as the 4th arg to reuse a pooled client.
// refereeIdentity is REQUIRED on registered — the first-touch anchor:
await client.RegisteredAsync(token, refereeIdentity: "player-7842", serverEventId: "reg-7842");
// When they hit your "qualified" bar:
SendResult res = await client.QualifiedAsync(token, serverEventId: "qual-7842");
if (!res.Ok) Console.Error.WriteLine($"{res.Status} {res.Body}");RegisteredAsync / QualifiedAsync 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
InvalidOperationException. Need to sign without sending? Use client.Sign(...) or
the pure static ReferralClient.SignReferralBody(...).
The rest of this page is the from-scratch path — a single static class 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 endpoint that can read a query param / cookie.
- A milestone hook (where you decide a player has "qualified").
// Load from configuration / env; NEVER hardcode secrets.
var secret = builder.Configuration["MMOLove:ReferralSecret"]; // Referrals tab
var serverId = builder.Configuration["MMOLove:ServerId"]; // your server idThe client
MMOLoveReferral.cs — the whole integration. Build the body once, sign those
exact bytes, send those exact bytes.
using System.Security.Cryptography;
using System.Text;
public static class MMOLoveReferral
{
private const string Endpoint = "https://mmolove.com/api/referral/events";
private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(10) };
/// <summary>
/// Report a referral lifecycle event to MMOLove with an HMAC-signed body.
/// Returns (Status, Body). Status 0 means a transport error.
/// </summary>
public static async Task<(int Status, string Body)> SendEventAsync(
string secret, string serverId, string @event, IDictionary<string, object> extra)
{
// Build the body ONCE. Sign these exact bytes; send these exact bytes.
var payload = new Dictionary<string, object>(extra)
{
["event"] = @event, // "registered" | "qualified"
["server_id"] = serverId,
["ts"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
};
var raw = System.Text.Json.JsonSerializer.Serialize(payload);
// HMAC-SHA256 over "<t>.<rawBody>" (timestamp, a dot, then the raw body).
var t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var mac = Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes($"{t}.{raw}")))
.ToLowerInvariant(); // endpoint expects lower-case hex
var sig = $"t={t},v1=sha256={mac}";
try
{
using var msg = new HttpRequestMessage(HttpMethod.Post, Endpoint)
{
Content = new StringContent(raw, Encoding.UTF8, "application/json"), // SAME bytes
};
msg.Headers.Add("X-MMOLove-Signature", sig);
var res = await Http.SendAsync(msg);
return ((int)res.StatusCode, await res.Content.ReadAsStringAsync());
}
catch (HttpRequestException ex)
{
// Transport failure. Treat like a 5xx: retry with backoff.
return (0, ex.Message);
}
}
}Convert.ToHexString returns upper-case hex; lower-case it as shown. The
endpoint normalises incoming hex either way, but matching the other stacks keeps
your bytes identical.
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.
// In your registration handler, BEFORE creating the account.
static string? CaptureMmref(HttpRequest req)
=> req.Query["mmref"].FirstOrDefault() ?? req.Cookies["mmref"];
app.MapPost("/register", async (HttpRequest req) =>
{
var mmref = CaptureMmref(req);
var account = await CreateAccountAsync(req, 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.
if (!string.IsNullOrEmpty(account.Mmref))
{
var res = await MMOLoveReferral.SendEventAsync(secret, serverId, "registered",
new Dictionary<string, object>
{
["token"] = account.Mmref, // the captured token
["referee_identity"] = account.Id.ToString(), // STABLE id
["server_event_id"] = $"reg-{account.Id}", // idempotency key
});
HandleResult(res); // see error handling
}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 Task OnPlayerQualified(Player player)
{
if (string.IsNullOrEmpty(player.Mmref)) return; // not a referred player
var res = await MMOLoveReferral.SendEventAsync(secret, serverId, "qualified",
new Dictionary<string, object>
{
["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 GrantReferrerRewardAsync(player);
}
}Error handling
using System.Text.Json;
static bool HandleResult((int Status, string Body) res)
{
var (status, body) = res;
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(res); // backoff; same server_event_id
return false;
}
// 4xx — a request bug; retrying won't help.
string? error = null;
try { error = JsonDocument.Parse(body).RootElement.GetProperty("error").GetString(); }
catch { /* non-JSON */ }
Console.Error.WriteLine($"[mmolove] event rejected ({status}): {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
var res = await MMOLoveReferral.SendEventAsync(secret, serverId, "registered",
new Dictionary<string, object>
{
["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.
The signing secret is server-side only. Never bundle it into a client/game build, and never commit it. If it leaks, rotate it on the Referrals tab.
Next
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.
cURL
Integrate the Referral Kit with no SDK — sign and POST events from the shell with openssl + curl, including a reusable Bash helper and full worked examples.