Reward callbacks
The core vote→reward webhook — a player hearts your server, MMOLove POSTs a signed heart.counted event, you grant the in-game reward.
The reward callback is the core vote→reward loop. When a player gives your
server a heart, MMOLove sends a signed POST to your callback URL so you can
grant an in-game reward — more votes, more rewards, more reasons to vote. This is
the webhook the heart button on your listing fires.
This page is the overview + the authoritative payload and signing reference. For copy-paste handlers, the legacy GET mode, self-verification, and a full error table, follow the section pages below.
New here? The Quickstart is the 5-minute path: set your callback URL, copy your secret, send a test callback, watch it land in the delivery log. Then drop in your stack's handler.
How it works
A player hearts your server
On your MMOLove listing a player taps the heart. MMOLove records the vote (with fraud scoring + a 24-hour per-identity cooldown) and, if it counts, queues a delivery for your server.
MMOLove POSTs a signed heart.counted
A background worker delivers a signed JSON POST to your configured callback URL.
The body names the player (their in-game username), the vote, and the
player's current daily streak so you can scale the reward.
You verify the signature
Recompute the HMAC over the raw body and compare it to the X-MMOLove-Signature
header. Only then trust the request — it proves the call genuinely came from
MMOLove (and wasn't replayed or forged).
You grant the in-game reward
Look up the username, hand out the loot, return a 2xx. MMOLove marks the
delivery delivered. A non-2xx (or a timeout) is retried with backoff. You own
the economy; MMOLove just tells you who voted.
Skip the verify boilerplate with an SDK: npm i @mmolove/callback,
pip install mmolove-callback, dotnet add package MMOLove.Callback, or the
single-file PHP mmolove-callback.php drop-in — each validates the signature in one
call. You don't need one; any language with an HMAC-SHA256 can verify directly.
Set it up
Configure the callback on your server's dashboard under the Integration tab:
- Reward callback URL — a public
https://endpoint, e.g.https://your-server.example/mmolove/callback. We POST everyheart.countedhere. (Private / loopback / link-local URLs are rejected for SSRF safety.) - Signing secret — click Rotate secret to mint one. It's shown once; copy it and store it where your callback handler can read it (env var / secrets manager — never the browser or game client).
- Legacy GET callback — leave off for new integrations. It exists only for
older vote-callback scripts that expect a
GETwith query params. See Legacy GET mode.
Then click Send test callback to fire a signed heart.test at your endpoint
and confirm the wiring before real votes arrive.
Reward callbacks
When a heart is counted, MMOLove delivers a signed JSON POST to your
callback URL:
POST /your/callback/path
Content-Type: application/json
X-MMOLove-Event: heart.counted
X-MMOLove-Signature: t=<unix>,v1=<hex>The body (heart.counted) is:
{
"event": "heart.counted",
"server_id": "8f0e…-server-uuid",
"username": "PlayerOne",
"heart_id": "1a2b…-heart-uuid",
"period": "2026-06",
"timestamp": 1733500000,
"streak_day": 7,
"loyalty_score": 240
}| Field | Type | Always present | Meaning |
|---|---|---|---|
event | string | yes | heart.counted for a real vote; heart.test for the dashboard's Send test callback. |
server_id | string | yes | Your MMOLove server id (UUID). The server the heart was cast for. |
username | string | yes | The voter's in-game name — the reward recipient. This is the value the player typed when voting. |
heart_id | string | yes | The vote's unique id (UUID). Use it as an idempotency key so a retry never double-rewards. |
period | string | yes | The vote's ranking period, YYYY-MM (UTC month), e.g. 2026-06. |
timestamp | number | yes | Unix seconds at delivery time. Also the t used in the signature. |
streak_day | number | no | The voter's current daily vote streak (in days) for this server at delivery time, so you can grant escalating rewards (bigger bonuses at day 7, 30…). Omitted when the streak is unavailable — treat a missing field as "no streak info", not zero. Present only for voters signed in to MMOLove (streaks are account-based). |
loyalty_score | number | no | The voter's cumulative Loyalty Score with this server — earned per counted heart (+10) and streak milestones — so you can reward your most devoted players. Streaks require an MMOLove account; loyalty accrues for every voter. Omitted only when it can't be resolved at delivery time — a 0 is a real "no loyalty yet" score and is sent. |
The reward recipient is username — the in-game name the player entered when
voting. Resolve it to an account on your side. Don't reward server_id or
heart_id; those identify the vote, not the player.
See the Payload reference for the full contract,
including the heart.test shape and how to use heart_id for idempotency.
Verifying signatures
Every callback is signed with an HMAC-SHA256 keyed by your per-server secret (rotate it any time from the Integration tab). Verify the signature before granting a reward so you only act on requests that genuinely came from MMOLove.
The signature is in the X-MMOLove-Signature header:
X-MMOLove-Signature: t=<unix>,v1=<hex>t— Unix seconds at delivery time (equal to the body'stimestamp).v1—HMAC_SHA256(secret, "<t>.<rawBody>")in lower-case hex, whererawBodyis the exact request-body bytes. There is nosha256=prefix on the reward-callbackv1(this differs from the Referral Kit — see the note below).
To verify, recompute the MAC over "<t>.<rawBody>" and compare it to v1 with a
constant-time comparison:
expected = HMAC_SHA256(secret, t + "." + rawBody) // lower-case hex
trust the request ⇔ expected == v1 (timing-safe)Read the raw body first. Compute the MAC over the exact bytes you received, before parsing JSON. Re-serializing the parsed object changes whitespace / key order and the MAC will (correctly) fail to match — the single most common integration bug.
Relationship to the Referral Kit signature
Both webhooks use the same HMAC core: HMAC_SHA256(secret, "<t>.<rawBody>"),
lower-case hex, signed over the raw body, with t in the X-MMOLove-Signature
header. The only difference is the v1 encoding:
| Reward callback (this page) | Referral Kit | |
|---|---|---|
| Header | t=<unix>,v1=<hex> | t=<unix>,v1=sha256=<hex>[,kid=…] |
v1 value | bare lower-case hex | sha256= + lower-case hex |
| MAC | HMAC_SHA256(secret, "<t>.<rawBody>") | identical |
| Direction | MMOLove → you (you verify) | you → MMOLove (we verify) |
| Replay window | enforced on your side (recommended) | ±5 min, enforced by MMOLove |
So if you've already integrated the Referral Kit, your signing core is reusable —
just strip the sha256= prefix when verifying a reward callback's v1.
See Signing & verification for the full scheme, a worked example, and a replay-window recommendation.
Read next
Quickstart
Zero to a verified test callback in about five minutes.
Payload reference
Every field, the heart.test shape, and idempotency via heart_id.
Signing & verification
The exact HMAC scheme, a worked example, and the replay window.
Legacy GET mode
The older GET-with-query-params callback for legacy vote scripts.
Testing & self-verify
Send test callback + the delivery log.
Errors, retries & delivery
Retry schedule, statuses, and troubleshooting.
Handler guides
Complete copy-paste handlers: PHP, Node, Python, .NET, cURL.
SDKs
Installable verifiers: @mmolove/callback, mmolove-callback, MMOLove.Callback, mmolove-callback.php.
API reference
The machine-readable contract + downloadable OpenAPI 3.1 spec.
Publishing your listing
Listings start unpublished and you put them live yourself — no waiting on a review queue. Complete the five required items, hit Publish, done. Moderation is reactive and only intervenes on rule-breaking listings.
Overview
The reward callback — MMOLove POSTs a signed heart.counted event when a player votes, and you grant the in-game reward.