Webhooks — Overview

Webhooks

Webhooks are how Rewarded Media notifies your server about things that happened to your members — they hit a reward threshold, they got flagged for fraud, and so on. Your server receives a real-time HTTP callback with transaction details so you can credit the user, scrub a payout, or update internal state.

Webhooks are fully configurable in the Rewarded Media dashboard: URL, HTTP method, custom headers, body template, signing secret, and which events this webhook subscribes to. At send time we render the body and headers by interpolating template variables (macros) into whatever you've configured.

Supported events — subscribe each webhook to the ones you care about. Branch on {{event}}:

completion Fires on every batch completion — the rawest per-batch signal, for analytics and progress tracking rather than reward crediting.
reward_unlocked Fires once when a member hits the promotion's cumulative reward threshold — the signal to credit the full reward.
fraud_flagged Fires when a member trips a block-severity fraud rule — payout is zeroed on our side; scrub your own crediting.

Prerequisites

  • An HTTPS endpoint that returns a 2xx status code
  • A shared secret (configured per webhook) used to generate the HMAC signature
  • Respond within the timeout budget (see Delivery & Retry Behavior below)

Template Variables

All macros use double curly-brace syntax: {{variable_name}}. You can reference them in any header value or in the body template. For GET requests you don't need to put macros in the URL — every variable is automatically appended as a query-string parameter.

Macro Description Example
{{event}} Name of the event this delivery represents — one of completion, reward_unlocked, or fraud_flagged. Branch your handler on this so reward crediting only runs on reward_unlocked and payout scrubbing only runs on fraud_flagged. reward_unlocked
{{member_id}} External member ID you passed in the signed link. Falls back to the internal UUID if no external ID was provided. abc123
{{points_earned}} Integer points earned, in the whole-number denomination ($0.01 → 1, not 0.01). 25
{{user_payout}} Dollar amount owed to the user on this transaction, formatted to 4 decimal places. 0.0050
{{cumulative_user_payout}} Total dollars the member has earned across every transaction on this promotion, including this one. Use this on reward_unlocked to credit the full reward in one shot. 0.2000
{{org_retention}} Dollars retained by the org/partner on this transaction (what you keep). 4 decimal places. 0.0010
{{org_gross}} Dollars in the org pool on this transaction (before the user split). 4 decimal places. 0.0060
{{platform_cut}} Rewarded Media's take on this transaction (including any CPC spread on flat-rate promotions). 4 decimal places. 0.0020
{{gross_revenue}} Advertiser-side gross (Σ campaign CPC) for this transaction. 4 decimal places. 0.0080
{{promotion_id}} Rewarded Media's internal numeric promotion ID. 42
{{promotion_slug}} Promotion slug (URL-safe identifier). winter-promo
{{transaction_id}} Rewarded Media transaction ID. Recommended for idempotency — store it on your side and ignore repeat deliveries. 1829
{{completed_at}} ISO 8601 timestamp of when the transaction occurred (UTC). 2026-04-21T16:01:42Z
{{signature}} HMAC signature of the rendered body, prefixed with the algorithm name. Header values only — not valid in the body or URL. sha256=9f3a...
{{field_signature}} HMAC over a fixed set of fields (event~transaction_id~member_id~points_earned~completed_at), prefixed with the algorithm name. Safe to place in the body, headers, or URL. Empty string when no secret is set. See Field signature. sha256=c41b...

HTTP Methods

  • GET — All template variables are automatically appended to the URL as query-string parameters. Any query-string params you already set on the URL are preserved.
  • POST / PUT / PATCH / DELETE — The body template is rendered with variable interpolation and sent as the request body. If the body template is left blank, the default payload is a JSON object containing every template variable, including field_signature (but excluding {{signature}}, which is header-only).

Signing the Payload

When a shared secret is configured, the {{signature}} macro expands to an HMAC of the rendered request body, keyed by the shared secret, encoded as lowercase hexadecimal, prefixed with the algorithm name:

sha256=9f3a5e8c...   # SHA-256 (default)
sha512=41d2ab...      # SHA-512

By default, whenever a shared secret is configured, we send the signature in the X-Signature header automatically — you don't need to configure anything extra:

X-Signature: sha256=<lowercase-hex>

Override the header name: If your server expects a different header (e.g. X-Hub-Signature-256, Authorization), configure a custom header in the webhook form with {{signature}} as the value. When a custom signature header is present, the default X-Signature is not sent.

To verify on your server: compute HMAC-SHA256(raw_request_body, shared_secret).hexdigest(), strip the sha256= prefix from the header, and compare the two values with a constant-time comparison. For GET requests the body is empty, so the signature is an HMAC over an empty string — if you need authenticated GETs, prefer POST.

Field signature (in-body / in-URL)

{{signature}} is an HMAC of the entire rendered body, so it can't live inside that body — substituting it would change the bytes it signs. If your endpoint needs the signature inside the payload (or in the query string of a GET), use {{field_signature}} instead. It signs a fixed, ordered subset of the event's fields rather than the container that carries it:

message = event ~ transaction_id ~ member_id ~ points_earned ~ completed_at
field_signature = "sha256=" + HMAC-SHA256(message, shared_secret).hexdigest()

# e.g.
message = "completion~1829~user_abc~5~2026-04-21T16:01:42Z"
  • Fields are joined with a literal tilde (~) in exactly this order, using the same string values that appear in the payload (completed_at is ISO 8601 UTC, member_id may be empty).
  • Output is lowercase hex, prefixed with the webhook's algorithm (sha256= or sha512=) — the same format as {{signature}}.
  • Included automatically in the default JSON payload as field_signature, and appended to GET query strings. In a custom body template, place it wherever you like.
  • Because event is signed, a fraud_flagged payload can't be replayed as a completion.
  • It does not cover the revenue fields — verify those with the whole-body X-Signature header if you rely on them. Both are sent when a secret is set; use whichever fits your endpoint.
// Body template
{"event":"{{event}}","tx":"{{transaction_id}}","user":"{{member_id}}",
 "points":"{{points_earned}}","at":"{{completed_at}}","sig":"{{field_signature}}"}

// Verify (Node)
const msg = [b.event, b.tx, b.user, b.points, b.at].join('~');
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(msg).digest('hex');
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(b.sig))) return res.status(401).end();

Delivery & Retry Behavior

  • Connect timeout: 5 seconds. Read timeout: 10 seconds.
  • Delivery is retried once per minute for up to 15 attempts on any failure — network errors, timeouts, or non-2xx HTTP responses. After 15 attempts the delivery is considered permanently failed.
  • Every attempt (success or failure) is recorded in the delivery log with the response status or error message.
  • Use {{transaction_id}} to deduplicate on your side in case of redelivery.