recalled.dev
Reference

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

  1. You configure a target URL in the project's notification settings.
  2. On save, Recalled generates an HMAC signing secret and shows it once. Store it in your environment next to your API keys.
  3. Every time an event matching your filter is ingested, Recalled sends a POST to your URL with a JSON body.
  4. The request carries X-Recalled-Timestamp and X-Recalled-Signature headers you verify on your side using the secret.
  5. 2xx = acknowledged. 5xx / timeout = transient, retried with backoff. 4xx = fatal, dropped.
  6. 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:

json
{
  "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

HeaderExamplePurpose
Content-Typeapplication/jsonAlways JSON
User-AgentRecalled-Webhooks/1.0Stable UA so you can allowlist
X-Recalled-Timestamp1713183780Unix seconds when the delivery was signed
X-Recalled-Signaturev1=9f86d081...HMAC-SHA256 of ${timestamp}.${rawBody}
X-Recalled-Event-Idevt_abcDedupe key for idempotence
X-Recalled-Delivery-Idwhd_abc123xyzUnique 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)

ts
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)

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

AttemptDelay from previousCumulative
1immediate0
21 min1 min
35 min6 min
430 min36 min
52 h2 h 36
66 h8 h 36
712 h20 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.