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:
- Verifying a captured callback by hand — confirm your secret and the signing
string with just
openssl, no framework. - 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 historyDon'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"; fiUse 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' # deliveredReading the result
Whether you're forging a request or watching real traffic, the rule is the same:
| Your handler returns | MMOLove does |
|---|---|
2xx | Delivered — stops. |
401 | Retry (your signature check rejected it). |
400 | Retry (unparseable / missing fields). |
5xx / timeout | Retry. |
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.