Outbound webhooks
Recalled can POST every ingested event to a URL you control, signed with HMAC-SHA256. Use it to route events to your back-end, a data warehouse, Zapier, n8n, a SIEM, or any tool that speaks HTTPS.
> Plan + role requirements. Generic webhooks are available on Pro (1 webhook) and Scale (up to 10). Only the project owner can create, edit or delete them. Admins and viewers invited to the project cannot see or manage webhooks even if they can edit the rest of the settings.
How it works
- You configure a target URL in the project's notification settings.
- On save, Recalled generates an HMAC signing secret and shows it once. Store it in your environment next to your API keys.
- Every time an event matching your filter is ingested, Recalled sends a
POSTto your URL with a JSON body. - The request carries
X-Recalled-TimestampandX-Recalled-Signatureheaders you verify on your side using the secret. - 2xx = acknowledged. 5xx / timeout = transient, retried with backoff. 4xx = fatal, dropped.
- After 7 consecutive failures the webhook is auto-disabled so we stop hammering a broken endpoint. Fix your endpoint, then re-enable from the dashboard.
Payload format
Every delivery is wrapped in an envelope so we can add new webhook event types later without breaking consumers:
{
"id": "whd_abc123xyz",
"type": "event.created",
"createdAt": "2026-04-15T14:23:00.123Z",
"projectId": "proj_xyz",
"event": {
"id": "evt_abc",
"action": "invoice.refunded",
"actor": { "id": "user_1", "email": "alice@acme.co", "name": "Alice" },
"organization": "org_acme",
"targets": [{ "type": "invoice", "id": "inv_42" }],
"metadata": { "amount": 4200, "currency": "eur" },
"occurredAt": "2026-04-15T14:22:58.000Z",
"hash": "sha256:..."
}
}Request headers
| Header | Example | Purpose |
|---|---|---|
Content-Type | application/json | Always JSON |
User-Agent | Recalled-Webhooks/1.0 | Stable UA so you can allowlist |
X-Recalled-Timestamp | 1713183780 | Unix seconds when the delivery was signed |
X-Recalled-Signature | v1=9f86d081... | HMAC-SHA256 of ${timestamp}.${rawBody} |
X-Recalled-Event-Id | evt_abc | Dedupe key for idempotence |
X-Recalled-Delivery-Id | whd_abc123xyz | Unique per delivery attempt |
Any custom headers you set on the channel (e.g. Authorization: Bearer ... for a private endpoint) are merged with these. Recalled-reserved headers always win.
Verify the signature (Node.js)
import crypto from "node:crypto";
const SECRET = process.env.RECALLED_WEBHOOK_SECRET!;
const MAX_SKEW_SECONDS = 5 * 60;
export function verifyRecalledSignature(req: {
rawBody: string;
headers: Record<string, string | undefined>;
}): boolean {
const timestamp = req.headers["x-recalled-timestamp"];
const signature = req.headers["x-recalled-signature"];
if (!timestamp || !signature) return false;
// Reject stale deliveries to stop a captured request from being replayed.
const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(skew) || skew > MAX_SKEW_SECONDS) return false;
const expected =
"v1=" +
crypto
.createHmac("sha256", SECRET)
.update(`${timestamp}.${req.rawBody}`, "utf8")
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Verify the signature (Python)
import hmac, hashlib, os, time
SECRET = os.environ["RECALLED_WEBHOOK_SECRET"]
MAX_SKEW = 5 * 60
def verify(raw_body: bytes, headers: dict) -> bool:
ts = headers.get("x-recalled-timestamp")
sig = headers.get("x-recalled-signature")
if not ts or not sig:
return False
if abs(int(time.time()) - int(ts)) > MAX_SKEW:
return False
msg = f"{ts}.{raw_body.decode('utf-8')}".encode("utf-8")
expected = "v1=" + hmac.new(SECRET.encode("utf-8"), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)Retry policy
| Attempt | Delay from previous | Cumulative |
|---|---|---|
| 1 | immediate | 0 |
| 2 | 1 min | 1 min |
| 3 | 5 min | 6 min |
| 4 | 30 min | 36 min |
| 5 | 2 h | 2 h 36 |
| 6 | 6 h | 8 h 36 |
| 7 | 12 h | 20 h 36 |
Transient errors (408, 429, 5xx, network, timeout) are retried. Fatal errors (4xx other than 408/429, 401/403/404) drop immediately.
After 7 consecutive failures across distinct events, the webhook flips to enabled = false and stops receiving deliveries until you manually re-enable it.
Idempotence
Deliveries are at-least-once: the same event may reach you twice if a first attempt timed out on our side but actually succeeded on yours. Dedupe on X-Recalled-Event-Id or the event.id field inside the body. Both are stable per event.
SSRF protection
At creation and before every delivery, Recalled refuses URLs that resolve to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, loopback, carrier-grade NAT, link-local, IPv6 ULA), blocked ports (SSH, SQL, Redis, metadata endpoints), and redirects are not followed. Your target must be reachable from the public internet over HTTPS.
Testing
Hit Send test notification in the dashboard. Recalled sends a test.ping event with synthetic fields so you can exercise your signature verification code without waiting for a real user action.