Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.offthehook.dev/llms.txt

Use this file to discover all available pages before exploring further.

Off the Hook delivers webhooks as HTTP POST requests with JSON bodies to the destination URL you configure on a subscription. Your endpoint must be publicly reachable over HTTPS — Off the Hook validates the URL at subscription creation time and rejects any address that resolves to a private IP range.

Responding correctly

Return an HTTP 2xx status code within the timeout window. If your endpoint returns 5xx, 408, 429, or the connection fails entirely, Off the Hook treats the delivery as failed and queues a retry.

Retry schedule

Off the Hook retries a failed delivery up to 7 times with the following delays between attempts:
AttemptDelay after previous
1200 ms
21 s
35 s
41 min
55 min
630 min
72 h
After all 7 attempts are exhausted, the event status moves to failed. You can manually re-queue it using the retry endpoint.

Deduplication

Because retried deliveries re-send the same webhook-id header, you should treat that value as an idempotency key. Store the IDs of events you have already processed and skip any delivery that arrives with a duplicate ID.
const webhookId = req.headers["webhook-id"] as string;
if (alreadyProcessed(webhookId)) {
  return res.status(200).send("Duplicate, skipping");
}
Return 2xx even for duplicates — returning an error causes another retry.

SSRF protection

Off the Hook validates your destination URL when you create or update a subscription. URLs that resolve to private IP ranges — including 10.x.x.x, 172.16.x.x172.31.x.x, 192.168.x.x, and 127.x.x.x — are rejected with an ssrf_blocked error. The same check runs at delivery time to defend against DNS rebinding.
{
  "error": "ssrf_blocked",
  "message": "url resolves to a blocked range 10.0.0.0/8",
  "detail": { "ip": "10.0.0.5", "cidr": "10.0.0.0/8" }
}
Use https://webhook.site to get a free public HTTPS URL during development. It displays every incoming request in real time, making it easy to inspect headers and payloads without running a local server.

Handler examples

The examples below show minimal handlers that verify the signature and deduplicate events. See Verify Webhook Signatures for the full signature verification guide.
import express from "express";
import { Webhook } from "svix";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/webhooks", (req, res) => {
  const wh = new Webhook(process.env.WEBHOOK_SECRET!);
  let event;
  try {
    event = wh.verify(req.body, req.headers as any);
  } catch (err) {
    return res.status(400).send("Invalid signature");
  }

  // Deduplicate using webhook-id
  const webhookId = req.headers["webhook-id"] as string;
  if (alreadyProcessed(webhookId)) {
    return res.status(200).send("Duplicate, skipping");
  }

  console.log("Received event:", event);
  res.status(200).send("OK");
});