MMOLove Docs
Reward Callbacks — guides

Errors, retries & delivery

How MMOLove delivers reward callbacks — the retry schedule, delivery statuses, what counts as success, and how to troubleshoot failures.

A reward callback is delivered by a durable background worker, not inline with the vote. This page covers what "success" means, the retry schedule, the delivery statuses you'll see in the log, and how to debug a callback that isn't landing.

What counts as success

A delivery succeeds when your endpoint returns any 2xx status. Anything else is a failure and will be retried:

Your responseOutcome
2xxDelivered. No more attempts.
3xxFailure — redirects are not followed; point the callback URL at the final endpoint. Retried.
4xxFailure — retried. (A 4xx is almost always your signature check rejecting the request — see troubleshooting.)
5xxFailure — retried.
timeout / connection error / TLS errorFailure — retried.

Return 2xx as soon as you've durably accepted the vote (recorded the heart_id). If your reward grant is async, accept first and grant in the background — don't hold the connection open, and don't return non-2xx unless you genuinely want a retry.

Retry schedule

A failed delivery is retried with a fixed backoff, up to 6 attempts total. The delay before each retry:

AttemptDelay after the previous attempt
1(immediate — on the vote)
21 minute
35 minutes
430 minutes
52 hours
66 hours
(after 6)gives up — marked exhausted

After the 6th failed attempt the delivery is marked exhausted and not retried again. Because retries can span hours, your handler must be idempotent on heart_id (a success whose 2xx was lost will be retried, and you must not double-reward).

Don't rely on the exact delays — treat them as "retried for a while with growing gaps." Do rely on: retries happen, they can be hours apart, and the same heart_id can arrive more than once.

Delivery statuses

Each row in the Delivery log carries one of these statuses (the dashboard maps them to ok / fail / retry chips):

StatusChipMeaning
pendingretryQueued, waiting for its (next) attempt.
sendingretryA worker is delivering it right now.
deliveredokYour endpoint returned 2xx. Terminal success.
failedfailThe last attempt failed; another retry is scheduled.
exhaustedfailAll 6 attempts failed (or the delivery couldn't be sent) — given up.

The chip also shows the last HTTP status code and the attempt count (3 tries), so you can tell a 5xx-flapping endpoint from a signature-rejecting one at a glance.

Why a callback might never be sent

A delivery is only created when your integration is fully set up, and a few conditions cause MMOLove to skip or retire it before any HTTP request:

SituationWhat happens
No callback URL or no secret configuredNo delivery is created — configure both on the Integration tab.
Integration not verifiedNo delivery is created until ownership is verified.
Callback URL isn't a public addressThe delivery is skipped (SSRF guard): loopback, private, and link-local URLs are refused. Use a public https:// URL.
The vote has no resolvable usernameThe delivery is retired as exhausted — a callback with no recipient can never reward anyone. Make sure voters enter an in-game name.

Troubleshooting

My endpoint returns 4xx and the log shows fail/retries. Your signature check is rejecting the request. This is overwhelmingly the raw-body bug: you verified against a re-serialized body, not the exact bytes. Capture the raw body first, recompute HMAC_SHA256(secret, "<t>.<rawBody>"), and compare to v1. Also check you're using the current secret (a rotate invalidates the old one).

The log shows request_failed / nothing arrives. MMOLove couldn't reach your endpoint. Verify it's publicly reachable over HTTPS with a valid certificate, isn't behind auth/IP-allowlisting that blocks us, and isn't a redirect (we don't follow 3xx).

Deliveries pile up as retry. Your endpoint is consistently non-2xx or timing out. Use Send test callback to reproduce on demand, fix the handler, and the queued retries will then succeed on their next attempt.

I rewarded twice. Your grant isn't idempotent. Key it on heart_id so a retried delivery (or a lost 2xx) is a guaranteed no-op. See Payload → idempotency.

Nothing in the log at all. No counted hearts yet — or the integration isn't verified / configured. Confirm the callback URL + secret are saved and click Send test callback to force a delivery.

See also

On this page