> ## 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.

# Receive Webhook Deliveries

> Set up an HTTPS endpoint to receive Off the Hook webhook POSTs, handle retries, and respond correctly so deliveries are marked successful.

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:

| Attempt | Delay after previous |
| ------- | -------------------- |
| 1       | 200 ms               |
| 2       | 1 s                  |
| 3       | 5 s                  |
| 4       | 1 min                |
| 5       | 5 min                |
| 6       | 30 min               |
| 7       | 2 h                  |

After all 7 attempts are exhausted, the event status moves to `failed`. You can manually re-queue it using the [retry endpoint](/guides/retry-events).

## 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.

```typescript theme={null}
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.x`–`172.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.

```json theme={null}
{
  "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" }
}
```

<Tip>
  Use [https://webhook.site](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.
</Tip>

## Handler examples

The examples below show minimal handlers that verify the signature and deduplicate events. See [Verify Webhook Signatures](/guides/verify-signatures) for the full signature verification guide.

<CodeGroup>
  ```typescript TypeScript (Express) theme={null}
  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");
  });
  ```

  ```go Go theme={null}
  package main

  import (
    "io"
    "net/http"
    "os"

    svix "github.com/svix/svix-webhooks/go"
  )

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
      http.Error(w, "failed to read body", http.StatusInternalServerError)
      return
    }

    wh, _ := svix.NewWebhook(os.Getenv("WEBHOOK_SECRET"))
    if err := wh.Verify(body, r.Header); err != nil {
      http.Error(w, "invalid signature", http.StatusBadRequest)
      return
    }

    // Deduplicate using webhook-id
    webhookID := r.Header.Get("webhook-id")
    if alreadyProcessed(webhookID) {
      w.WriteHeader(http.StatusOK)
      return
    }

    // Process the event...
    w.WriteHeader(http.StatusOK)
  }
  ```
</CodeGroup>
