Payload reference
The exact heart.counted body and headers MMOLove delivers — every field, the heart.test shape, and idempotency via heart_id.
This is the authoritative contract for the reward-callback request. When a heart is counted, MMOLove POSTs this to your callback URL.
The request
POST /your/callback/path
Content-Type: application/json
X-MMOLove-Event: heart.counted
X-MMOLove-Signature: t=<unix>,v1=<hex>| Header | Value |
|---|---|
Content-Type | application/json |
X-MMOLove-Event | The event name — heart.counted for a real vote, heart.test for a dashboard test callback. Lets you branch without parsing the body. |
X-MMOLove-Signature | t=<unix>,v1=<hex> — verify this before trusting the body. See Signing. |
The body (heart.counted)
{
"event": "heart.counted",
"server_id": "8f0e1d2c-3b4a-5c6d-7e8f-901a2b3c4d5e",
"username": "PlayerOne",
"heart_id": "1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809",
"period": "2026-06",
"timestamp": 1733500000,
"streak_day": 7
}| Field | Type | Always present | Meaning |
|---|---|---|---|
event | string | yes | heart.counted (real vote) or heart.test (dashboard test). Mirrors the X-MMOLove-Event header. |
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. Exactly what the player entered when voting. |
heart_id | string | yes | The vote's unique id (UUID). Use as your idempotency key. |
period | string | yes | The vote's ranking period, YYYY-MM in UTC, e.g. 2026-06. |
timestamp | number | yes | Unix seconds at delivery time. Equals the t in the signature. |
streak_day | number | no | The voter's current daily vote streak (days) for this server at delivery time. Field is omitted when the streak is unavailable. |
The reward recipient is username — resolve it to an account on your side.
Don't reward server_id (that's your server) or heart_id (that's the vote).
streak_day — escalating rewards
streak_day is the voter's consecutive-daily-vote count for your server at the
moment of delivery, so you can scale rewards: a flat reward at day 1, a bonus at
day 7, a big bonus at day 30, and so on.
It is optional. When MMOLove can't resolve the voter's streak the field is
simply left out of the JSON (it is never sent as 0 to mean "unknown"). So:
streak_day present → use it (1, 2, 7, …)
streak_day absent → treat as "no streak info", grant the base rewardDon't default a missing streak_day to 0 and then withhold a base reward — a
missing field still represents a real, counted vote.
Idempotency — use heart_id
A delivery is retried until it gets a 2xx (see
Errors & delivery), so the same heart_id can
legitimately arrive more than once — e.g. you granted the reward but your 2xx
response was lost in transit.
Make granting idempotent by keying on heart_id:
if (already_rewarded(heart_id)) return 200; // safe replay — acknowledge
grant_reward(username, streak_day);
mark_rewarded(heart_id);
return 200;This way a retried delivery is a guaranteed no-op, and you can always return a
clean 2xx so MMOLove stops retrying.
The heart.test shape
The dashboard's Send test callback delivers a heart.test event — identical in
shape and signing to a real one, with placeholder values so your handler can run
end-to-end without a real vote:
{
"event": "heart.test",
"server_id": "<your-server-id>",
"username": "test-user",
"heart_id": "00000000-0000-0000-0000-000000000000",
"period": "2026-06",
"timestamp": 1733500000
}Note: heart.test carries the all-zero heart_id and the username test-user,
and (because there's no real voter) no streak_day. Branch on
event === "heart.test" (or the X-MMOLove-Event header) to verify-and-acknowledge
without actually paying out. See Testing & self-verify.
What you return
| Your response | MMOLove does |
|---|---|
2xx | Marks the delivery delivered. Stops. |
| any non-2xx | Schedules a retry with backoff (up to 6 attempts). |
| timeout / connection error | Same as non-2xx — retry. |
Return a 2xx as soon as you've durably accepted the vote (recorded the
heart_id), even if the in-game grant is async. Return non-2xx only when you truly
want a retry.
See also
Quickstart
From zero to a verified reward callback in about five minutes — set the URL, copy your secret, send a test callback, watch it land, then verify and reward.
Signing & verification
The exact X-MMOLove-Signature HMAC scheme for reward callbacks — verify over the raw body, the header format, a worked example, and a replay-window recommendation.