Webhooks
Fire a signed payload to Zapier, Make, or any URL when a contact reaches a webhook block
Overview
A webhook block POSTs a signed, versioned JSON payload to a URL you choose the moment a contact reaches that step in a campaign. It is the generic building block behind Zapier (“Catch Hook”), Make, n8n, and your own endpoints — no native app required.
Webhook blocks are available on the Pro plan. (This is the in-campaign automation block; the account-level public API and org-wide webhooks are a separate, Enterprise feature that is still on the roadmap.)
Three things make a webhook block different from a message block, and all of them are intentional:
- It is not a message. There is no quiet-hours hold and no “Reply STOP” footer — it is automation you direct, like the report block.
- It exports contact details to a third party you choose. You are the data controller for whatever you send onward.
- Delivery is best-effort, at-least-once. A failing endpoint is retried with backoff, but a broken endpoint never blocks the rest of the campaign — after the final retry the contact advances anyway.
Payload
Every delivery is a POST with a JSON body in this envelope:
{
"version": "1",
"event": "campaign.block_reached",
"sent_at": "2026-07-15T12:34:56Z",
"data": {
"organization": { "id": "9f1b2c3d-...", "name": "Acme Plumbing" },
"campaign": { "id": "4e5f6a7b-...", "name": "Post-job review ask" },
"contact": {
"id": "8c9d0e1f-...",
"first_name": "Sarah",
"last_name": "Miller",
"email": "sarah.miller@example.com",
"phone": "+15550101234"
},
"enrollment": {
"status": "in_progress",
"section": "chase"
},
"block": { "id": "2a3b4c5d-..." }
}
}
Field notes:
version— the envelope schema version. Switch on it; it only changes on a breaking change.idfields — plain UUIDs (no type prefix). They are stable: the same contact keeps the sameidacross deliveries, so use them for dedup and mapping.event— currently alwayscampaign.block_reached.test— present andtrueonly for a payload sent from the builder’s Send test payload button. Real deliveries omit it. Use it to avoid acting on test fires.sent_at— RFC 3339 UTC send time.data.enrollment.section—chasewhile the contact is still being asked,on_reviewonce a review has been confirmed and the contact fell through to the completion section.data.enrollment.reviewed_at— present only when the review has been confirmed (typically in theon_reviewsection); omitted otherwise, as in the example above.
The payload deliberately carries a subset of each record — internal fields (opt-out flags, consent audit, CRM metadata) never cross the tenant boundary.
Verifying the signature
Every delivery carries an X-ReviewMe-Signature header so you can confirm it really came from us and was not replayed:
X-ReviewMe-Signature: t=1752582896,v1=3f8a...c1
tis the unix timestamp the payload was signed at.v1isHMAC-SHA256(secret, "<t>" + "." + rawBody), hex-encoded.
The secret is your organization’s webhook signing secret (a whsec_-prefixed string) — find it under Settings → Team → Webhook signing secret. The entire string, prefix included, is the HMAC key.
To verify a delivery:
- Recompute the MAC over
t + "." + rawBody, whererawBodyis the exact bytes you received — do not re-serialize the JSON first. - Compare your MAC to
v1in constant time. - Reject the delivery if
tis more than a few minutes (e.g. 5) from your clock — that window is your replay protection.
Node.js
const crypto = require("crypto");
function verify(secret, header, rawBody, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const { t, v1 } = parts;
if (!t || !v1) throw new Error("malformed signature");
const expected = crypto
.createHmac("sha256", secret)
.update(t + "." + rawBody)
.digest("hex");
const a = Buffer.from(v1);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error("signature mismatch");
}
const age = Math.abs(Math.floor(Date.now() / 1000) - parseInt(t, 10));
if (age > toleranceSeconds) throw new Error("timestamp outside tolerance");
return true;
}
Python
import hashlib, hmac, time
def verify(secret, header, raw_body, tolerance_seconds=300):
parts = dict(kv.split("=", 1) for kv in header.split(","))
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1:
raise ValueError("malformed signature")
expected = hmac.new(
secret.encode(), (t + "." + raw_body).encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(v1, expected):
raise ValueError("signature mismatch")
if abs(int(time.time()) - int(t)) > tolerance_seconds:
raise ValueError("timestamp outside tolerance")
return True
Rotating the secret
Rotating issues a new secret immediately and invalidates the old one. Any receiver still verifying against the previous secret will start rejecting deliveries until you update it there, so rotate only when you need to and update your receivers promptly.
Use with Zapier
- In Zapier, create a Zap and choose Webhooks by Zapier → Catch Hook as the trigger. Copy the custom webhook URL Zapier gives you.
- In ReviewMe, open your campaign in the builder, add a Webhook block where you want it to fire, and paste the URL into Endpoint URL. Save the block.
- Click Send test payload. Zapier’s Catch Hook receives the sample payload — use it to map the fields in the rest of your Zap. The toast in ReviewMe shows the HTTP status your endpoint returned.
- (Optional but recommended) Add a Zapier Code step that verifies
X-ReviewMe-Signaturewith your signing secret before the Zap acts, using the example above.
Make, n8n, and custom endpoints work the same way: paste the receiving URL into the block and verify the signature on your side.