> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brudcast.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a webhook endpoint

> Register a URL in Developers > Webhooks, choose its events, save the signing secret, and send a test delivery.

export const DemoVideo = ({id, title, embedUrl, src, poster, duration}) => {
  if (embedUrl) {
    return <figure className="brd-media" data-media-id={id}>
        <iframe className="brd-media-frame w-full aspect-video" src={embedUrl} title={title} allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture" allowFullScreen />
      </figure>;
  }
  if (src) {
    return <figure className="brd-media" data-media-id={id}>
        <video className="brd-media-frame w-full aspect-video" src={src} poster={poster} controls playsInline preload="metadata" />
      </figure>;
  }
  const label = duration ? `Video walkthrough coming soon · ${duration}` : "Video walkthrough coming soon";
  return <Placeholder id={id} kind="video" label={label} description={title} ratio="16 / 9" icon={<PlayIcon />} />;
};

export const Loop = ({id, src, alt, caption}) => {
  if (!src) {
    return <Placeholder id={id} kind="loop" label="Animation coming soon" description={alt} icon={<PlayIcon />} />;
  }
  return <figure className="brd-media" data-media-id={id}>
      <video className="brd-media-frame" src={src} autoPlay muted loop playsInline aria-label={alt} />
      {caption && <figcaption className="brd-media-caption">{caption}</figcaption>}
    </figure>;
};

export const Screenshot = ({id, src, srcDark, alt, caption}) => {
  if (!src) {
    return <Placeholder id={id} kind="screenshot" label="Screenshot coming soon" description={alt} icon={<ImageIcon />} />;
  }
  return <figure className="brd-media" data-media-id={id}>
      <img className="brd-media-frame block dark:hidden" src={src} alt={alt} />
      <img className="brd-media-frame hidden dark:block" src={srcDark || src} alt={alt} />
      {caption && <figcaption className="brd-media-caption">{caption}</figcaption>}
    </figure>;
};

Register the URL that should receive events. When you're done, you'll have an endpoint, its signing
secret, and a test delivery in its log.

<Screenshot id="ss-developers-create-an-endpoint-hero-overview" alt="An endpoint's Overview tab showing its URL, a 30s timeout, 5 max retries and a delivered test.event under Recent Deliveries" />

<Info>
  **Before you start:** you need a URL that Brudcast can reach over the public internet. Use HTTPS.
  To test against your own machine first, see [Test from your own machine](#test-from-your-own-machine).
  Your plan sets how many endpoints the organization can have. To use the API instead of the
  dashboard, you need an API key with the `webhooks:write` scope.
</Info>

<Accordion title="Watch the walkthrough" icon="circle-play">
  <DemoVideo id="V20" title="Receive and verify webhooks" duration="3 min" />
</Accordion>

<Steps>
  <Step title="Open Webhooks">
    Go to **Developers > Webhooks** and select **Add Endpoint**. The **Create Webhook Endpoint**
    panel opens.

    <Screenshot id="ss-developers-create-an-endpoint-01-add-endpoint" alt="The Create Webhook Endpoint panel with the Endpoint URL and Description fields" />
  </Step>

  <Step title="Enter the URL">
    Type the full URL, including `https://`, in **Endpoint URL**. The form doesn't accept anything
    that isn't a valid URL. Optionally add a **Description** so your team knows what the endpoint
    is for.
  </Step>

  <Step title="Choose events">
    Under **Subscribe to events**, select each event you want. The events are grouped as **Email
    Events**, **Contact Events**, **Campaign Events** and **Push Events**. Each group has a
    **Select all** link. The count at the bottom of the panel shows how many events you've
    selected.

    Of these, only `push.token_invalidated` is delivered today. See
    [Event types](/developers/webhooks/event-types).

    <Loop id="lp-developers-create-an-endpoint-select-events" alt="Selecting push.token_invalidated and the selected-event counter changing to 1 event selected" />
  </Step>

  <Step title="Create the endpoint and copy the secret">
    Select **Create endpoint**. The **Signing Secret** dialog shows the secret, a 64-character hex
    string. Copy it into your secret manager now, because it isn't shown again. Then select
    **Done**.

    <Screenshot id="ss-developers-create-an-endpoint-04-signing-secret" alt="The Signing Secret dialog with the secret, a copy button and the warning that it will not be shown again" />

    <Note>
      On the endpoint's pages, the secret appears masked as `whsec_••••`. That's only a
      placeholder. The real secret has no prefix.
    </Note>
  </Step>

  <Step title="Set custom headers, timeout and retries">
    Select the endpoint's URL under **Your Endpoints**, then open the **Settings** tab.

    * **Custom Headers**: enter a **Header name** and a **Value**, then select **Add**. Brudcast
      sends these on every delivery.
    * **Retry Configuration**: choose **Max Retries**, the total number of attempts, and **Request
      Timeout**. Each change saves as soon as you make it.

    <Screenshot id="ss-developers-create-an-endpoint-05-settings" alt="The Settings tab with the Custom Headers form and the Max Retries and Request Timeout selectors" />
  </Step>

  <Step title="Send a test event">
    Select **Test Webhook** at the top right of the endpoint page. Brudcast sends a `test.event`
    within a few seconds. Open the **Delivery Logs** tab and select the row to see the HTTP status
    your server returned, its response body and the request body.
  </Step>
</Steps>

## Create an endpoint with the API

`POST /webhook-endpoints` takes everything the dashboard does, in one call. The API also accepts a
timeout up to 300 seconds and up to 20 attempts.

<CodeGroup>
  ```bash cURL theme={"system"}
  curl https://core-service.prod.brudcast.com/api/v1/user/webhook-endpoints \
    -H "X-API-Key: bk_live_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://example.com/webhooks/brudcast",
      "description": "Production event handler",
      "events": ["push.token_invalidated"],
      "customHeaders": [{ "key": "X-Environment", "value": "production" }],
      "timeoutSeconds": 10,
      "maxRetries": 8
    }'
  ```

  ```javascript Node.js theme={"system"}
  const res = await fetch(
    "https://core-service.prod.brudcast.com/api/v1/user/webhook-endpoints",
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.BRUDCAST_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        url: "https://example.com/webhooks/brudcast",
        description: "Production event handler",
        events: ["push.token_invalidated"],
        customHeaders: [{ key: "X-Environment", value: "production" }],
        timeoutSeconds: 10,
        maxRetries: 8,
      }),
    },
  );

  const { data } = await res.json();
  console.log(data.id, data.secret); // Save data.secret now
  ```

  ```python Python theme={"system"}
  import os
  import requests

  res = requests.post(
      "https://core-service.prod.brudcast.com/api/v1/user/webhook-endpoints",
      headers={"X-API-Key": os.environ["BRUDCAST_API_KEY"]},
      json={
          "url": "https://example.com/webhooks/brudcast",
          "description": "Production event handler",
          "events": ["push.token_invalidated"],
          "customHeaders": [{"key": "X-Environment", "value": "production"}],
          "timeoutSeconds": 10,
          "maxRetries": 8,
      },
  )
  res.raise_for_status()
  data = res.json()["data"]
  print(data["id"], data["secret"])  # Save the secret now
  ```

  ```php PHP theme={"system"}
  <?php
  $ch = curl_init('https://core-service.prod.brudcast.com/api/v1/user/webhook-endpoints');
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          'X-API-Key: ' . getenv('BRUDCAST_API_KEY'),
          'Content-Type: application/json',
      ],
      CURLOPT_POSTFIELDS => json_encode([
          'url' => 'https://example.com/webhooks/brudcast',
          'description' => 'Production event handler',
          'events' => ['push.token_invalidated'],
          'customHeaders' => [['key' => 'X-Environment', 'value' => 'production']],
          'timeoutSeconds' => 10,
          'maxRetries' => 8,
      ]),
  ]);
  $data = json_decode(curl_exec($ch), true)['data'];
  echo $data['id'], ' ', $data['secret']; // Save the secret now
  ```
</CodeGroup>

The response is `201` with the endpoint and its `secret` (abridged here):

```json theme={"system"}
{
  "success": true,
  "message": "Webhook endpoint created successfully",
  "data": {
    "id": "01a08ff7-0c3e-7a21-9d4f-6b2e8c1f3a57",
    "url": "https://example.com/webhooks/brudcast",
    "description": "Production event handler",
    "events": ["push.token_invalidated"],
    "customHeaders": [{ "key": "X-Environment", "value": "production" }],
    "timeoutSeconds": 10,
    "maxRetries": 8,
    "secret": "9f2c4e6a8b0d1f3e5a7c9b1d3f5e7a9c2b4d6f8e0a1c3e5b7d9f1a3c5e7b9d0f"
  }
}
```

Reading the endpoint later never returns the secret. If you lose it, rotate it.

## Rotate the signing secret

In the dashboard, open the endpoint's **Settings** tab. Under **Security**, select **Rotate**, then
**Rotate Secret**. The new secret replaces the masked value on the page. Reveal or copy it before
you leave the page: after a reload, it's masked again. With the API, call
`POST /webhook-endpoints/{id}/regenerate-secret`. The new secret is in `data.secret`.

<Warning>
  The old secret stops working the moment you rotate. There's no overlap period. Your server will
  reject deliveries signed with the new secret until you update it. Brudcast records those
  rejections as delivered and doesn't retry them. Update the secret right away. If you need to
  reprocess a delivery from the gap, its `data` is in the delivery details.
</Warning>

## Change, disable or delete an endpoint

* **URL and description:** **Settings > Endpoint Configuration**, then **Save**.
* **Events:** **Settings > Subscribe to events**, then **Save Changes**.
* **Disable:** turn on **Disable this endpoint** under **Danger Zone**, or send
  `PATCH /webhook-endpoints/{id}` with `"isActive": false`. Events that happen while the endpoint
  is disabled aren't stored, and they aren't sent when you turn it back on.
* **Delete:** **Delete Endpoint** under **Danger Zone**. The endpoint, its secret and its delivery
  history are removed. This can't be undone.

<Note>
  Custom headers are added after the built-in headers, so a custom header with the same name
  replaces the built-in one. Don't reuse `Content-Type`, `X-Webhook-Signature`,
  `X-Webhook-Delivery-Id` or `X-Webhook-Event`. Header values show in the endpoint settings and in
  API responses. Use a token made for this purpose, not a production credential.
</Note>

## Retries and deliveries

Each event sent to an endpoint becomes a **delivery**, and a delivery has one or more **attempts**.

### What counts as delivered

| Your server's reaction                              | Result                                                                                                                |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Any HTTP response with a status from `200` to `599` | **Delivered**. The status code and the first 1,000 characters of your response body are recorded. No further attempts |
| No response before the endpoint's timeout           | The attempt failed. Retried if attempts remain                                                                        |
| Connection refused or reset, DNS failure, TLS error | The attempt failed. Retried if attempts remain                                                                        |

Because a `4xx` or `5xx` response ends the delivery, a handler that crashes after it reads the
request doesn't get a second chance. Store the event, return `200`, then process it. Point the
endpoint at its final URL rather than at one that redirects.

### Attempts and backoff

**Max Retries** is the total number of attempts, including the first. The default is 5. The
dashboard offers 0 to 5, and `PATCH /webhook-endpoints/{id}` accepts up to 20.

After attempt *n* fails, the next attempt waits 5 × 2^(*n* − 1) seconds, up to a maximum of 24
hours. Brudcast checks for due deliveries every five seconds, so an attempt can start up to about
five seconds after it's due. The first attempt happens within a few seconds of the event.

| Attempt | Wait before this attempt | Time since the first attempt |
| ------- | ------------------------ | ---------------------------- |
| 2       | 5 s                      | 5 s                          |
| 3       | 10 s                     | 15 s                         |
| 4       | 20 s                     | 35 s                         |
| 5       | 40 s                     | 1 min 15 s                   |
| 10      | 21 min 20 s              | 42 min 35 s                  |
| 15      | 11 h 23 min              | 22 h 45 min                  |
| 20      | 24 h                     | 5 d 21 h 31 min              |

With the default of 5, Brudcast stops trying about 75 seconds after the first attempt. If your
endpoint can be down for longer than that, during a deploy for example, raise **Max Retries**.

<Warning>
  With **Max Retries** set to `0`, Brudcast makes no attempts. Every delivery, including test
  events, is marked failed without being sent. Use at least `1`.
</Warning>

### Delivery statuses

| Status       | Dashboard label | Meaning                                                                                                                           |
| ------------ | --------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `pending`    | Pending         | Waiting for its first attempt, or reset by a manual resend                                                                        |
| `processing` | Processing      | An attempt is in progress                                                                                                         |
| `delivered`  | Delivered       | Your server returned an HTTP response                                                                                             |
| `failed`     | Failed          | The last attempt got no response. If `nextRetryAt` has a time, another attempt is scheduled. If it's `null`, no attempts are left |
| `retrying`   | Retrying        | Accepted as a filter value. Deliveries waiting for a retry show as `failed`, with `nextRetryAt` set                               |

When the last attempt fails, `errorMessage` begins with `Max retries exceeded. Last error:`,
followed by the reason for that failure.

### Resend a delivery

On the endpoint's **Delivery Logs** tab, select the retry icon on a **Failed** row, or open any
delivery that isn't delivered and select **Resend**. With the API, call
`POST /webhook-deliveries/{id}/retry` (scope `webhooks:write`). A resend puts the delivery back to
`pending` and sends it straight away.

* **Delivered deliveries can't be resent.** The API returns `400` with
  `Cannot retry a delivery that has already been delivered`.
* **The attempt count isn't reset.** A delivery that has used all of its endpoint's attempts is
  marked failed again, without being sent. Raise the endpoint's **Max Retries** above the delivery's
  `attemptCount` first.
* **The body is the same.** A resend carries the original `delivery_id` and `timestamp`.

### Read the delivery log

The endpoint's **Overview** tab shows the last five deliveries under **Recent Deliveries**.
**Delivery Logs** has the full history, filtered by **Status** and **Event**. Its **HTTP** column
shows **Timeout** for any failed delivery that got no response. Select a row for its **Request
Body** (the event's `data`) and the **Response Body** your server returned.

With the API, `GET /webhook-deliveries` (scope `webhooks:read`) lists deliveries newest first.
Filter with `webhookEndpointId`, `status` (repeat the parameter for several values) and `type`, add
`includeAttempts=true` for each delivery's attempts, and page with `limit` and `cursor` until
`meta.hasMore` is `false`.

```bash theme={"system"}
curl "https://core-service.prod.brudcast.com/api/v1/user/webhook-deliveries?webhookEndpointId=01a08ff7-0c3e-7a21-9d4f-6b2e8c1f3a57&status=failed&includeAttempts=true&limit=50" \
  -H "X-API-Key: bk_live_your_api_key"
```

`GET /webhook-deliveries/{id}` returns one delivery with all of its attempts. A delivery record
carries `id` (sent to your server as `delivery_id`), `type`, `payload`, `status`, `attemptCount`,
`httpStatusCode`, `errorMessage`, `responseBody`, `nextRetryAt`, `deliveredAt` and `createdAt`.

## Test from your own machine

Brudcast can only reach public URLs. While you build your handler, give it a public address with a
tunnelling tool such as ngrok or Cloudflare Tunnel, and point a separate endpoint at it.

<CodeGroup>
  ```bash ngrok theme={"system"}
  ngrok http 3000
  ```

  ```bash cloudflared theme={"system"}
  cloudflared tunnel --url http://localhost:3000
  ```
</CodeGroup>

Create an endpoint with the tunnel's URL plus your handler's path, for example
`https://abc123.example-tunnel.dev/webhooks/brudcast`, then select **Test Webhook** and read the
result in **Delivery Logs**. Some tunnels hand out a new URL each time they start, so update the
endpoint when yours does.

Keep this endpoint separate from your production one: a production endpoint pointing at a tunnel
stops receiving events whenever your laptop sleeps. Delete it when you're done, because it counts
toward your plan's endpoint limit and fills its log with failed deliveries once the tunnel closes.

## Troubleshooting

<AccordionGroup>
  <Accordion title="“Select at least one event”" icon="circle-alert">
    **Why:** an endpoint must subscribe to at least one event.

    **Fix:** select an event under **Subscribe to events**, then select **Create endpoint** again.
  </Accordion>

  <Accordion title="“Webhook endpoint limit reached”" icon="circle-alert">
    **Why:** the organization already has as many endpoints as its plan allows.

    **Fix:** delete an endpoint you no longer use, or move to a larger plan. See
    [Plans and subscriptions](/billing/plans-and-subscriptions).
  </Accordion>

  <Accordion title="“Please enter a valid URL”" icon="circle-alert">
    **Why:** the URL is missing its scheme or isn't well formed.

    **Fix:** enter the full address, for example `https://example.com/webhooks/brudcast`.
  </Accordion>

  <Accordion title="The test delivery shows Failed, with Timeout in the HTTP column" icon="circle-alert">
    **Why:** Brudcast got no HTTP response. The URL may be wrong, the server may be down, a firewall
    may be blocking the request, or the server took longer than the request timeout.

    **Fix:** check that the URL is reachable from outside your network and that the handler replies
    quickly. The delivery is retried on its own while it has attempts left. See
    [Retries and deliveries](#retries-and-deliveries).
  </Accordion>

  <Accordion title="The test delivery shows Delivered, but my app didn't process it" icon="circle-alert">
    **Why:** any HTTP response counts as delivered, including `401`, `404` and `500`.

    **Fix:** open the delivery and check the **HTTP** status and **Response Body**. A `401` usually
    means the signature check failed. See
    [Verify signatures](/developers/webhooks/verifying-signatures).
  </Accordion>
</AccordionGroup>

<Columns cols={2}>
  <Card title="Verify signatures" icon="shield-check" href="/developers/webhooks/verifying-signatures">
    Reject requests that didn't come from Brudcast.
  </Card>

  <Card title="Event types" icon="list" href="/developers/webhooks/event-types">
    What each event means and which are delivered.
  </Card>

  <Card title="Payload reference" icon="braces" href="/developers/webhooks/payload-reference">
    The headers, the envelope and each event's data.
  </Card>
</Columns>
