Receiving webhooks
How to build an endpoint that receives MonetizeIt events. The wire contract — envelope, headers, event catalog — is in Webhook events; this page is the receiving side.
Endpoint requirements
Section titled “Endpoint requirements”- HTTPS, reachable from the internet.
- Respond 2xx fast — verify, persist, return; process asynchronously. A slow handler gets timed out and retried.
- Hold the subscription’s signing secret server-side.
Verify every delivery
Section titled “Verify every delivery”Recompute the signature over {webhook-id}.{webhook-timestamp}.{raw body} and
compare in constant time. Read the body as raw bytes before any JSON parsing —
re-serialized JSON won’t match.
using System.Security.Cryptography;using System.Text;
static string Sign(string secret, string id, string timestamp, string body) => "v1," + Convert.ToBase64String( HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes($"{id}.{timestamp}.{body}")));
static bool VerifySignature(string secret, string id, string timestamp, string body, string header) => header.Split(' ').Any(candidate => CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(candidate), Encoding.UTF8.GetBytes(Sign(secret, id, timestamp, body))));The header can carry multiple space-separated signatures during a secret
rotation — accept the delivery if any one verifies. Also reject deliveries whose
webhook-timestamp is older than your tolerance (a few minutes) to shut down
replays.
// Nodeconst crypto = require("crypto");
const sign = (secret, id, timestamp, body) => "v1," + crypto.createHmac("sha256", secret).update(`${id}.${timestamp}.${body}`).digest("base64");
const verify = (secret, id, timestamp, body, header) => header.split(" ").some(candidate => { const expected = sign(secret, id, timestamp, body); return candidate.length === expected.length && crypto.timingSafeEqual(Buffer.from(candidate), Buffer.from(expected)); });Answer the registration challenge
Section titled “Answer the registration challenge”When a subscription is created, MonetizeIt immediately POSTs a
Webhooks.Registration.Challenge event to your URL. The subscription only
becomes Active once your endpoint proves it holds the secret. Your response
must:
- Return 2xx with a JSON body echoing the challenge:
{"challenge": "<data.challenge>"}. - Carry a
webhook-signatureresponse header — the same HMAC scheme, computed over{webhook-id}.{webhook-timestamp}.{your response body}using the request’s id and timestamp headers.
app.MapPost("/webhooks/monetizeit", async (HttpContext ctx) =>{ var id = ctx.Request.Headers["webhook-id"].ToString(); var timestamp = ctx.Request.Headers["webhook-timestamp"].ToString(); var body = await new StreamReader(ctx.Request.Body).ReadToEndAsync();
if (!VerifySignature(secret, id, timestamp, body, ctx.Request.Headers["webhook-signature"]!)) return Results.Unauthorized();
var envelope = JsonDocument.Parse(body).RootElement; if (envelope.GetProperty("type").GetString() == "Webhooks.Registration.Challenge") { var reply = JsonSerializer.Serialize(new { challenge = envelope.GetProperty("data").GetProperty("challenge").GetString() }); ctx.Response.Headers["webhook-signature"] = Sign(secret, id, timestamp, reply); return Results.Text(reply, "application/json"); }
await queue.EnqueueAsync(envelope, ctx.RequestAborted); // process async return Results.Ok();});Handle retries and duplicates
Section titled “Handle retries and duplicates”Delivery is at-least-once: a timeout after successful processing means the
same event arrives again. Record webhook-id (unique per event) and skip ids
you’ve already processed. Failed deliveries retry with exponential backoff;
what ultimately fails is kept for inspection and can be requeued from the portal.
Don’t assume ordering — two related events can arrive out of order after a retry. Treat each event as a signal to fetch current state when order matters.
Rotate the secret
Section titled “Rotate the secret”Rotate from the portal (Settings → Webhooks) or via
POST /api/v1/webhooks/admin/subscriptions/{id}/secret. During the overlap
window MonetizeIt signs with both secrets (space-separated in
webhook-signature), so a receiver that checks all candidates — like the
snippets above — rolls keys with zero dropped events.
Call back with the delivery token
Section titled “Call back with the delivery token”If the subscription requests callback scopes, each delivery carries a
short-lived JWT in webhook-callback-token. Use it as a bearer token to call
back into the API with exactly those scopes — react to
MeteredUsage.OverageStatusChanged by reading the balance, for example, with no
stored credential and no OAuth round-trip.
Test it
Section titled “Test it”POST /api/v1/webhooks/admin/subscriptions/{id}/test-fire (or Test-fire in
the portal, which lets you pick the event type and preview the exact payload)
sends a sample event through the full pipeline — signature, headers, retry
behavior — so you can validate the receiver before real traffic hits it.
The payload matches the documented schema for the chosen event type and always
carries "_test": true inside data, covered by the signature like the rest of
the body. Gate on it: never apply a test event to real state — the sample
payloads reference fixed placeholder ids, not your data.