MMOLove Docs
Reward Callbacks — guidesHandler guides

cURL

Verify a reward callback with no framework — recompute the HMAC over a captured raw body with openssl, plus a CGI-style shell handler.

The reward callback is MMOLove POSTing to you, so there's nothing to "call" with cURL the way you would an outbound API. But the bare-metal recipe is still useful for two things:

  1. Verifying a captured callback by hand — confirm your secret and the signing string with just openssl, no framework.
  2. A CGI / shell-based handler — if your server tooling routes callbacks to a shell script.

Any language that can compute an HMAC and read a request body can do the same.

Prefer a packaged verifier? The official SDKs verify in one call — npm i @mmolove/callback, pip install mmolove-callback, dotnet add package MMOLove.Callback, or the single-file PHP mmolove-callback.php drop-in.

The signing recipe

The MAC is HMAC_SHA256(secret, "<t>.<rawBody>") in lower-case hex, where t is the t= field of X-MMOLove-Signature and rawBody is the exact bytes of the request body. The v1 field is the bare hex (no sha256= prefix).

SECRET='your-server-secret'   # from the Integration tab — keep it out of history

Don't paste a real secret into an interactive shell (it lands in ~/.bash_history). Read it from a file or env var: SECRET="$(cat /run/secrets/mmolove)".

Verify a captured callback

Suppose you logged a real delivery's header and body:

# Exactly what arrived — the t from the header and the raw body, byte-for-byte.
T='1733500000'
V1='3f8a…the-v1-hex-from-the-header…'
BODY='{"event":"heart.counted","server_id":"srv_123","username":"PlayerOne","heart_id":"h_1","period":"2026-06","timestamp":1733500000,"streak_day":7}'

# Recompute the MAC over "<T>.<BODY>", lower-case hex, stripping openssl's prefix.
MAC=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

if [ "$MAC" = "$V1" ]; then echo "OK — signature valid"; else echo "MISMATCH"; fi

Use printf '%s' (not echo) so no trailing newline is appended — that newline would change the signed bytes and the MAC wouldn't match. $BODY must be the exact body bytes that were delivered.

Forge a test request against your own handler

Useful to exercise your handler locally before wiring the real callback URL. Build a body, sign it the way MMOLove does, and POST it at your endpoint:

ENDPOINT='https://your-server.example/mmolove/callback'
T=$(date +%s)
BODY='{"event":"heart.counted","server_id":"srv_123","username":"PlayerOne","heart_id":"h_local_1","period":"2026-06","timestamp":'"$T"',"streak_day":7}'
MAC=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

curl -i -X POST "$ENDPOINT" \
  -H 'Content-Type: application/json' \
  -H "X-MMOLove-Event: heart.counted" \
  -H "X-MMOLove-Signature: t=$T,v1=$MAC" \
  -d "$BODY"
# Your handler should verify the signature and return 2xx.

This proves your verifier, not MMOLove. To prove the real round-trip, use Send test callback on the Integration tab — it fires a genuine signed heart.test from MMOLove. See Testing & self-verify.

A CGI-style shell handler

If callbacks route to a shell script, read the body from stdin and the signature from the CGI env var:

#!/usr/bin/env bash
set -euo pipefail
SECRET="$(cat /run/secrets/mmolove)"

RAW=$(cat)                                   # request body from stdin
HEADER="${HTTP_X_MMOLOVE_SIGNATURE:-}"       # CGI exposes headers as HTTP_*

# Parse t= and v1= out of "t=..,v1=..".
T=$(printf '%s' "$HEADER" | tr ',' '\n' | sed -n 's/^t=//p')
V1=$(printf '%s' "$HEADER" | tr ',' '\n' | sed -n 's/^v1=//p')

MAC=$(printf '%s' "$T.$RAW" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

if [ -z "$T" ] || [ "$MAC" != "$V1" ]; then
  printf 'Status: 401\r\n\r\n'; exit 0       # bad signature → MMOLove retries
fi

# Optional: reject stale deliveries (±5 min).
NOW=$(date +%s)
if [ "$(( NOW > T ? NOW - T : T - NOW ))" -gt 300 ]; then
  printf 'Status: 401\r\n\r\n'; exit 0
fi

# Verified — extract fields (jq) and reward, keyed on heart_id for idempotency.
EVENT=$(printf '%s' "$RAW" | jq -r '.event')
if [ "$EVENT" = "heart.test" ]; then
  printf 'Status: 200\r\n\r\n'; exit 0       # acknowledge, don't pay out
fi
USERNAME=$(printf '%s' "$RAW" | jq -r '.username')
HEART_ID=$(printf '%s' "$RAW" | jq -r '.heart_id')
STREAK=$(printf '%s' "$RAW" | jq -r '.streak_day // empty')   # absent → empty

# grant_reward "$USERNAME" "$STREAK"  (idempotent on "$HEART_ID")
printf 'Status: 200\r\n\r\n'                 # delivered

Reading the result

Whether you're forging a request or watching real traffic, the rule is the same:

Your handler returnsMMOLove does
2xxDelivered — stops.
401Retry (your signature check rejected it).
400Retry (unparseable / missing fields).
5xx / timeoutRetry.

If a hand-built verify mismatches but your real handler works (or vice-versa), it's nearly always a whitespace/quoting difference in the signed bytes. Print "$T.$BODY" and compare it byte-for-byte to the raw body you're verifying.

Next

On this page