MMOLove Docs
Reward Callbacks — guides

Testing & self-verify

Prove your reward-callback wiring with the dashboard "Send test callback" button and the delivery log — without waiting for a real vote.

You can prove your callback end-to-end without waiting for a real player to vote. The dashboard fires a real, signed delivery on demand, and the delivery log shows you exactly what happened.

"Send test callback"

On your server's Integration tab, Send test callback delivers a signed heart.test event to your configured callback URL using your current secret — the same code path a real heart.counted uses, just with placeholder values:

{
  "event": "heart.test",
  "server_id": "<your-server-id>",
  "username": "test-user",
  "heart_id": "00000000-0000-0000-0000-000000000000",
  "period": "2026-06",
  "timestamp": 1733500000
}

It's signed exactly like a real callback (X-MMOLove-Signature: t=<unix>,v1=<hex>, X-MMOLove-Event: heart.test), so a passing test proves your URL, your secret, and your signature verification are all correct end-to-end.

The test callback is always sent as a signed POST — it ignores the legacy GET toggle. If you run a legacy GET integration, test it by triggering a real vote (or hand-build a GET against your verifier — see Legacy GET).

Reading the result

The dashboard reports the outcome inline:

ResultMeaning
Test callback delivered (HTTP 2xx)Your endpoint received it and returned success. Wiring confirmed.
Test failed: unsafe_urlThe callback URL isn't a public https:// address — fix it and save.
Test failed: not_configuredNo callback URL or no secret yet — set both first.
Test failed: request_failedThe request never completed (DNS, TLS, timeout, connection refused). Check the endpoint is reachable from the public internet.
Test failed (HTTP 4xx/5xx)Your endpoint was reached but returned an error — almost always your signature check rejecting it. Compare the bytes you verified against the raw body.

A heart.test has the all-zero heart_id and the username test-user. Your handler should verify-and-acknowledge it (return 2xx) but not actually pay out — branch on event === "heart.test" (or the X-MMOLove-Event header).

Handling the test in your code

<?php
// After verifying the signature (see the PHP guide):
$body = json_decode($raw, true);
if (($body['event'] ?? '') === 'heart.test') {
    http_response_code(200);   // acknowledge — do NOT reward test-user
    exit;
}
grant_reward($body['username'], $body['streak_day'] ?? null);
http_response_code(200);

The delivery log

The Delivery log on the Integration tab is your window into every real heart.counted delivery, newest first. Each row shows:

  • a status chip — ok (delivered, with the HTTP status code), fail (failed / exhausted), or retry (pending a retry),
  • the event (heart.counted),
  • the attempt count (1 try, 3 tries, …), and
  • a relative time ("just now", "4 min ago", "2 h ago").

Use it to confirm:

  1. Did the delivery happen? A row appears for every counted heart.
  2. Did your endpoint accept it? An ok chip with a 2xx code means yes; a fail/retry chip with a 4xx/5xx means your endpoint rejected it (usually a signature mismatch) — fix it and the next vote will go through, or it'll be retried automatically.
  3. Is it retrying? Multiple tries on a row means earlier attempts didn't get a 2xx. See the retry schedule.

A clean smoke test

Configure + secret

Set the callback URL, Rotate secret, and store the secret where your handler reads it.

Send test callback

Click it. Expect Test callback delivered (HTTP 2xx). If your endpoint 4xxs, fix your signature verification now — a real vote would fail the same way.

Real vote

Cast a heart on your own listing (from a clean session). Watch a heart.counted row appear in the delivery log with an ok chip.

Idempotency

Confirm your grant is keyed on heart_id so a retried delivery (same heart_id) is a no-op. See Payload → idempotency.

See also

On this page