Log inSign up
Reward Callbacks — guides

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>
HeaderValue
Content-Typeapplication/json
X-MMOLove-EventThe event name — heart.counted for a real vote, heart.test for a dashboard test callback. Lets you branch without parsing the body.
X-MMOLove-Signaturet=<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,
  "account_linked": true,
  "streak_day": 7,
  "longest_streak": 12,
  "streak_milestone": true,
  "monthly_streak": 3,
  "longest_monthly_streak": 5,
  "loyalty_score": 240,
  "loyalty_level": 3,
  "loyalty_title": "Devotee",
  "total_hearts": 18,
  "first_heart": false
}
FieldTypeAlways presentMeaning
eventstringyesheart.counted (real vote) or heart.test (dashboard test). Mirrors the X-MMOLove-Event header.
server_idstringyesYour MMOLove server id (UUID). The server the heart was cast for.
usernamestringyesThe voter's in-game name — the reward recipient. Exactly what the player entered when voting.
heart_idstringyesThe vote's unique id (UUID). Use as your idempotency key.
periodstringyesThe vote's ranking period, YYYY-MM in UTC, e.g. 2026-06.
timestampnumberyesUnix seconds at delivery time. Equals the t in the signature.
streak_daynumbernoThe voter's current daily vote streak (days) for this server at delivery time. Field is omitted when the streak is unavailable. Present only for voters signed in to MMOLove (streaks are account-based).
loyalty_scorenumbernoThe 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 score (first heart, no prior loyalty) and is sent.
account_linkedbooleanyesWhether the voter is signed in to an MMOLove account. false for anonymous voters, who never carry streak/loyalty. Gate streak rewards on this.
longest_streaknumbernoBest-ever daily streak for this server. Account-linked voters only.
streak_milestonebooleannotrue when this heart hit a 7-day multiple (7, 14, …) — a ready-made milestone trigger. Account-linked voters only.
monthly_streaknumbernoConsecutive calendar months (ending this month) the voter has voted for this server — for monthly reward cycles. Account-linked voters only.
longest_monthly_streaknumbernoBest-ever consecutive-months run for this server. Account-linked voters only.
loyalty_levelnumbernoLoyalty level (1–8) for this server, from loyalty_score. Present whenever loyalty_score is.
loyalty_titlestringnoLevel title (NewcomerAscended). Present whenever loyalty_level is.
total_heartsnumbernoLifetime counted hearts for this server.
first_heartbooleannotrue on the voter's first-ever counted heart for this server — for welcome rewards.

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, and present only for voters signed in to MMOLove — streaks are account-based, so anonymous votes never carry one. 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 reward

Don't default a missing streak_day to 0 and then withhold a base reward — a missing field still represents a real, counted vote.

loyalty_score — reward devotion

loyalty_score is the voter's cumulative Loyalty Score with your server — earned per counted heart (+10) and through streak milestones. Use it to tier rewards for your most devoted players (e.g. a cosmetic at 100, a mount at 1000). Streaks require an MMOLove account, but loyalty accrues for every voter.

Its semantics differ subtly from streak_day: a resolved 0 is sent — it's a real score meaning "first heart, no prior loyalty". The field is omitted only when MMOLove can't resolve the score at delivery time. So:

loyalty_score present → use it (0, 10, 240, …) — 0 is a real first-heart score
loyalty_score absent  → treat as "no loyalty info", grant the base reward

Streaks: daily vs monthly

Two streak granularities cover both habits:

  • streak_day / longest_streakconsecutive days. Reward not missing a day.
  • monthly_streak / longest_monthly_streakconsecutive calendar months with at least one vote (the period bucket). Reward not churning for a month — ideal for seasonal/monthly reward cycles. A vote anywhere in the month keeps it alive.
if (event.monthly_streak >= 3) grantMonthlyLoyalty(event.username, event.monthly_streak);

Both are present only for account-linked voters; a missing field means "no streak info".

Milestone & first-vote flags

  • streak_milestonetrue exactly when the daily streak just hit 7, 14, 21, … days.
  • first_hearttrue on the voter's very first counted heart for your server.
if (event.streak_milestone) grantMilestoneBonus(event.username, event.streak_day);
if (event.first_heart)      grantWelcome(event.username);

Loyalty level

loyalty_level (1–8) + loyalty_title save you hard-coding XP tiers: gate on loyalty_level >= 4 ("Champion and above") instead of a raw score. They derive from loyalty_score, so they appear together.

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,
  "loyalty_score": 120
}

Note: heart.test carries the all-zero heart_id, the username test-user, and a placeholder loyalty_score of 120, but (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 responseMMOLove does
2xxMarks the delivery delivered. Stops.
any non-2xxSchedules a retry with backoff (up to 6 attempts).
timeout / connection errorSame 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

On this page