Skip to content

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.

  • 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.

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.

// Node
const 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));
});

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:

  1. Return 2xx with a JSON body echoing the challenge: {"challenge": "<data.challenge>"}.
  2. Carry a webhook-signature response 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();
});

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 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.

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.

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.