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

# Setup a webhook

> Start receiving event payloads

## Summary

To start receiving webhook events in your integration, create and register a webhook endpoint by following the steps below:

1. Create a webhook endpoint handler to receive event data POST requests.
2. Register your endpoint with Brandfetch via an API request.
3. Secure your webhook endpoint.

You can register and create one endpoint to handle several different event types at once, or set up individual endpoints for specific events.

## Create a handler

See the [events reference](/delivery-methods/webhooks/event-types) to identify the event types your webhook handler needs to process.

Set an HTTPS endpoint function that can accept webhook requests with a POST method.

Set up your endpoint function so that it:

1. Handles POST requests with a JSON payload consisting of an event object.
2. Quickly returns a successful status code (2xx) prior to any complex logic that could cause a timeout.

### Example endpoint

This code snippet is a webhook function configured to check that the event type was received, to handle the event, and return a 200 response.

Example code for `hasVerifiedPayload()` is available [here](/delivery-methods/webhooks/best-practices#example-code-for-handling-signature-and-timing-verification).

```javascript theme={null}
const express = require("express");
const app = express();

// Verify the signature by comparing the signature
// provided in the signature header with one we
// compute ourselves with the shared secret.
// If the signatures don't match, we return an error
function verifyWebhook(request, response, rawBodyBuffer, encoding) {
  if (!rawBodyBuffer || !rawBodyBuffer.length) {
    return response.status(400).json({ message: "Request body missing" });
  }

  const payload = rawBodyBuffer.toString(encoding || "utf8");
  const headers = request.headers;

  if (
    !hasVerifiedPayload({
      sharedWebhookSecret: process.env.SHARED_WEBHOOK_SECRET,
      headers,
      rawRequestBody: payload,
    })
  ) {
    return response.status(400).json({
      message: "Signature does not match.",
    });
  }
}

app.post(
  "/webhook",
  express.json({ type: "application/json", verify: verifyWebhook }),
  (request, response) => {
    const event = request.body;

    switch (event.type) {
      case "brand.updated":
        const brand = event.data.brand;
        const changes = event.data.delta;

        // Then define and call a method to handle the brand updated event.
        handleBrandUpdated(brand, changes);

        break;
      case "brand.verified":
        const brand = event.data.brand;

        // Then define and call a method to handle the brand verified event.
        handleBrandVerified(brand);

        break;
      // ... handle other event types
      default:
        console.log(`Unhandled event type ${event.type}`);
    }

    // Return a response to acknowledge receipt of the event
    response.json({ received: true });
  }
);

app.listen(8000, () => console.log("Running on port 8000"));
```

## Register your endpoint

Once your handler is deployed on the web and ready to go, register your endpoint with Brandfetch by creating a webhook using the GraphQL APIs `createWebhook` [mutation](/delivery-methods/graphql).

<Note>
  Registered webhook endpoint URLs must be publicly accessible HTTPS URLs.
</Note>

<CodeGroup>
  ```cURL cURL theme={null}
  curl --request POST \
      --header 'content-type: application/json' \
      --header 'authorization: Bearer YOUR_API_KEY_HERE' \
      --url 'https://graphql.brandfetch.io' \
      --data '{"query":"mutation CreateWebhook($input: CreateWebhookInput!) {\n  createWebhook(input: $input) {\n    code\n    message\n    success\n    webhook {\n      urn\n      enabled\n    }\n  }\n}","variables":{"input":{"description":"My new Webhoook","events":["brand.updated","brand.verified"],"url":"https://httpbin.org/status/200"}}}'
  ```

  ```GraphQL GraphQL theme={null}
  mutation CreateWebhook($input: CreateWebhookInput!) {
    createWebhook(input: $input) {
      code
      message
      success
      webhook {
        urn
      }
    }
  }

  # Example Variables: { "input": { "description": "Get updates when a brand's logo changes", "events": ["brand.updated"], "url": "https://httpbin.org/status/200"}}
  ```
</CodeGroup>

## Subscribe to brands by URN or domain

The final step is to subscribe to the objects (like brands) for which you want to receive events. You can subscribe to a few objects, or many thousands, one at a time or in batches.

For example, perhaps you want to receive events for the Brandfetch brand. The URN for this brand is `urn:brandfetch:brand:idL0iThUh6which` means we would subscribe to that URN.

To add a subscription we need two things: The URN for the webhook we created (`$webhookUrn: URN!`) and the URN for the object to which we want to subscribe to (`$subscriptions: [URN!]!`).

<CodeGroup>
  ```cURL cURL theme={null}
  curl --request POST \
      --header 'content-type: application/json' \
      --header 'authorization: Bearer YOUR_API_KEY_HERE' \
      --url 'https://graphql.brandfetch.io' \
      --data '{"query":"mutation AddWebhookSubscriptions($webhookUrn: URN!, $subscriptions: [URN!]!) {\n  addWebhookSubscriptions(webhook: $webhookUrn, subscriptions: $subscriptions) {\n    code\n    message\n    success\n    webhook {\n      urn\n    }\n  }\n}","variables":{"webhookUrn":"urn:brandfetch:organization:1234:webhook:5678","subscriptions":["urn:brandfetch:brand:idL0iThUh6"]}}'
  ```

  ```GraphQL GraphQL theme={null}
  mutation AddWebhookSubscriptions($webhookUrn: URN!, $subscriptions: [URN!]!) {
    addWebhookSubscriptions(webhook: $webhookUrn, subscriptions: $subscriptions) {
      code
      message
      success
      webhook {
        urn
      }
    }
  }

  # Example Variables: { "webhookUrn": "urn:brandfetch:organization:1234:webhook:1234", "subscriptions": ["urn:brandfetch:brand:id123456"] }
  ```
</CodeGroup>

### Subscribe by domain

If all you have is a website, you can skip the brand lookup: `addWebhookSubscriptions` also accepts a `domains` argument of type `[FQDN!]`, and resolves each domain to the brand registered for it before creating the subscription.

`subscriptions` is optional now, so one call can name `domains`, `subscriptions`, or both. A few things worth knowing:

* Subdomains resolve to the registrable domain, so subscribing to `blog.nike.com` subscribes you to the same brand as `nike.com`.
* One call can name at most 1,000 entries, counted across `subscriptions` and `domains` together. The cap counts the entries you send, so a brand named twice, once by URN and once by domain, counts twice towards it.
* A brand named both ways is still subscribed once and charged once.
* A domain Brandfetch has not indexed yet is refused, and the error names the domains at fault ("No brand is registered for these domains yet: example.com"), so you can tell which entry of a batch was the problem. Nothing in the call is subscribed when that happens: correct the list and send it again.
* A call that names neither argument, or names only empty lists, is refused with "Provide at least one of `subscriptions` or `domains`."

<CodeGroup>
  ```cURL cURL theme={null}
  curl --request POST \
      --header 'content-type: application/json' \
      --header 'authorization: Bearer YOUR_API_KEY_HERE' \
      --url 'https://graphql.brandfetch.io' \
      --data '{"query":"mutation AddWebhookSubscriptions($webhookUrn: URN!, $domains: [FQDN!]) {\n  addWebhookSubscriptions(webhook: $webhookUrn, domains: $domains) {\n    code\n    message\n    success\n    webhook {\n      urn\n    }\n  }\n}","variables":{"webhookUrn":"urn:brandfetch:organization:1234:webhook:5678","domains":["brandfetch.com","nike.com"]}}'
  ```

  ```GraphQL GraphQL theme={null}
  mutation AddWebhookSubscriptions($webhookUrn: URN!, $domains: [FQDN!]) {
    addWebhookSubscriptions(webhook: $webhookUrn, domains: $domains) {
      code
      message
      success
      webhook {
        urn
      }
    }
  }

  # Example Variables: { "webhookUrn": "urn:brandfetch:organization:1234:webhook:1234", "domains": ["brandfetch.com", "nike.com"] }
  ```
</CodeGroup>

## Edit a webhook

To change a webhook you already registered, send just the fields you want to change to the `updateWebhook` mutation. A field you leave out, or set to `null`, keeps its current value.

| Field         | What it changes                                                                                                   |
| ------------- | ----------------------------------------------------------------------------------------------------------------- |
| `url`         | The endpoint deliveries are sent to. It must be a publicly accessible HTTPS URL.                                  |
| `urlHeaders`  | The header we send with each delivery. Only a single `Authorization` header is supported. Pass `[]` to remove it. |
| `events`      | The event types this webhook listens for.                                                                         |
| `description` | The short name or description you gave the webhook.                                                               |
| `enabled`     | Whether the webhook delivers at all. See [Pause a webhook](#pause-a-webhook).                                     |

`events` replaces the stored set rather than adding to it, so send the complete list of event types you want, not just the ones you are adding. An empty list is refused with "A webhook must subscribe to at least one event.", because a webhook with no event types keeps its subscriptions but can never deliver anything.

<Warning>
  Subscriptions are not managed through `updateWebhook`. `UpdateWebhookInput` still accepts a `urns` field, but nothing is done with it: a call that passes `urns` comes back with `success: true` and no subscription is added, removed, or replaced. Use `addWebhookSubscriptions` and `removeWebhookSubscriptions` instead.
</Warning>

<CodeGroup>
  ```cURL cURL theme={null}
  curl --request POST \
      --header 'content-type: application/json' \
      --header 'authorization: Bearer YOUR_API_KEY_HERE' \
      --url 'https://graphql.brandfetch.io' \
      --data '{"query":"mutation UpdateWebhook($webhookUrn: URN!, $input: UpdateWebhookInput!) {\n  updateWebhook(webhook: $webhookUrn, input: $input) {\n    code\n    message\n    success\n    webhook {\n      urn\n      url\n      events\n      enabled\n    }\n  }\n}","variables":{"webhookUrn":"urn:brandfetch:organization:1234:webhook:5678","input":{"events":["brand.updated","brand.verified"],"url":"https://example.com/brandfetch-webhook"}}}'
  ```

  ```GraphQL GraphQL theme={null}
  mutation UpdateWebhook($webhookUrn: URN!, $input: UpdateWebhookInput!) {
    updateWebhook(webhook: $webhookUrn, input: $input) {
      code
      message
      success
      webhook {
        urn
        url
        events
        enabled
      }
    }
  }

  # Example Variables: { "webhookUrn": "urn:brandfetch:organization:1234:webhook:1234", "input": { "events": ["brand.updated", "brand.verified"], "url": "https://example.com/brandfetch-webhook" } }
  ```
</CodeGroup>

## Pause a webhook

Set `enabled` to `false` to stop deliveries without taking anything apart. The endpoint, its event types, and every one of its subscriptions stay exactly as they are, and nothing is delivered while the webhook is off. Set `enabled` back to `true` to resume.

An event that happens while the webhook is off is skipped rather than held back, so switching the webhook on again does not replay what it missed.

Re-enabling also clears the run of failures behind an automatic disable, so a webhook we switched off after 14 days without a single successful delivery starts again with a clean slate rather than resuming an outage you have already fixed.

<CodeGroup>
  ```cURL cURL theme={null}
  curl --request POST \
      --header 'content-type: application/json' \
      --header 'authorization: Bearer YOUR_API_KEY_HERE' \
      --url 'https://graphql.brandfetch.io' \
      --data '{"query":"mutation UpdateWebhook($webhookUrn: URN!, $input: UpdateWebhookInput!) {\n  updateWebhook(webhook: $webhookUrn, input: $input) {\n    code\n    message\n    success\n    webhook {\n      urn\n      enabled\n    }\n  }\n}","variables":{"webhookUrn":"urn:brandfetch:organization:1234:webhook:5678","input":{"enabled":false}}}'
  ```

  ```GraphQL GraphQL theme={null}
  mutation UpdateWebhook($webhookUrn: URN!, $input: UpdateWebhookInput!) {
    updateWebhook(webhook: $webhookUrn, input: $input) {
      code
      message
      success
      webhook {
        urn
        enabled
      }
    }
  }

  # Example Variables: { "webhookUrn": "urn:brandfetch:organization:1234:webhook:1234", "input": { "enabled": false } }
  ```
</CodeGroup>

<Warning>
  Pausing a webhook does not pause its subscriptions. They stay in place, renew every month, and keep costing one credit each per month while the webhook is off, apart from the free `brandfetch.com` subscription. To stop paying for them, [remove the subscriptions](#unsubscribe-from-objects) or delete the webhook.
</Warning>

## When a request is declined

`createWebhook` and `addWebhookSubscriptions` check your plan and your billing status before they change anything. When a check fails, the mutation returns `success: false` with a `code` you can branch on and a `message` you can show, and nothing is created or subscribed. A call that goes through returns `code: "success"`.

| `code`                   | Message                                                                                       | What to do                                                                                                                                                                                                                                                      |
| ------------------------ | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NO_ACTIVE_SUBSCRIPTION` | Webhooks require an active paid subscription.                                                 | Webhooks are available on every paid plan. Start a paid plan, then retry.                                                                                                                                                                                       |
| `PAYMENT_FAILED`         | Your last payment failed. Update your payment method to create new webhooks or subscriptions. | Update your payment method in Plans & Billing. Webhooks you already have keep delivering; only new webhooks and new subscriptions are refused.                                                                                                                  |
| `CAPACITY_EXCEEDED`      | Your plan does not have room for more webhook subscriptions.                                  | Your plan covers a set number of brand subscriptions. Remove subscriptions you no longer need, or move to a larger plan.                                                                                                                                        |
| `INSUFFICIENT_CREDITS`   | Your organization does not have enough API credits.                                           | A new brand subscription costs one credit, and every subscription you hold costs one credit per month. Retry with a smaller batch, or wait for your allowance to reset.                                                                                         |
| `BILLING_HOLD`           | Webhooks are paused for this organization. Resolve the billing hold to create webhooks.       | Your organization's webhooks are on hold, which happens when a paid plan ends or a monthly renewal cannot be covered by your credits. No events are delivered while the hold is in place. It clears on its own once the plan or the credit balance is restored. |
| `WEBHOOK_LIMIT_REACHED`  | Your organization holds the maximum number of webhooks. Delete unused webhooks first.         | Returned by `createWebhook` only. Delete a webhook you no longer use, or point an existing one at the new endpoint with `updateWebhook`.                                                                                                                        |

<Note>
  These codes come back on the mutation result. Problems with the request itself, such as an unknown domain, more than 1,000 entries in one call, or an empty `events` list, come back as a GraphQL error instead, with a message naming what to fix.
</Note>

## Unsubscribe from objects

If you no longer want to receive events for an object, remove it with the `removeWebhookSubscriptions` mutation. As with subscribing, you can remove one or many subscriptions in a single call by passing multiple URNs.

<CodeGroup>
  ```cURL cURL theme={null}
  curl --request POST \
      --header 'content-type: application/json' \
      --header 'authorization: Bearer YOUR_API_KEY_HERE' \
      --url 'https://graphql.brandfetch.io' \
      --data '{"query":"mutation RemoveWebhookSubscriptions($webhookUrn: URN!, $subscriptions: [URN!]!) {\n  removeWebhookSubscriptions(webhook: $webhookUrn, subscriptions: $subscriptions) {\n    code\n    message\n    success\n  }\n}","variables":{"webhookUrn":"urn:brandfetch:organization:1234:webhook:5678","subscriptions":["urn:brandfetch:brand:idL0iThUh6"]}}'
  ```

  ```GraphQL GraphQL theme={null}
  mutation RemoveWebhookSubscriptions($webhookUrn: URN!, $subscriptions: [URN!]!) {
    removeWebhookSubscriptions(webhook: $webhookUrn, subscriptions: $subscriptions) {
      code
      message
      success
    }
  }

  # Example Variables: { "webhookUrn": "urn:brandfetch:organization:1234:webhook:1234", "subscriptions": ["urn:brandfetch:brand:id123456"] }
  ```
</CodeGroup>

## Debugging delivery issues

To help debug your endpoint, or to later retrieve failed event deliveries when your endpoint has a long duration outage, you can review all of the events Brandfetch attempted to deliver to your webhook endpoint using the GraphQL API.

Performing the following GraphQL query on the Webhooks API will return a list of all attempted webhook deliveries, responses from your endpoint, and the respective HTTP status codes we received. Delivery history is kept for 90 days after which time it is irreversibly deleted.

<CodeGroup>
  ```cURL cURL theme={null}
  curl --request POST \
      --header 'content-type: application/json' \
      --header 'authorization: Bearer YOUR_API_KEY_HERE' \
      --url 'https://graphql.brandfetch.io' \
      --data '{"query":"query RetrieveWebhookDeliveries($webhookUrn: URN!) {\n  webhook(webhook: $webhookUrn) {\n    url\n    urn\n    description\n    enabled\n    deliveries {\n      totalCount\n      edges {\n        node {\n          createdAt\n          deliveredAt\n          status\n          result {\n            body\n            headers {\n              name\n              value\n            }\n            message\n            statusCode\n          }\n        }\n      }\n    }\n  }\n}","variables":{"webhookUrn":"urn:brandfetch:organization:1234:webhook:1234"}}'
  ```

  ```GraphQL GraphQL theme={null}
  query RetrieveWebhookDeliveries($webhookUrn: URN!) {
    webhook(webhook: $webhookUrn) {
      url
      urn
      description
      enabled
      deliveries {
        totalCount
        edges {
          node {
            createdAt
            deliveredAt
            status
            result {
              body
              headers {
                name
                value
              }
              message
              statusCode
            }
          }
        }
      }
    }
  }

  # Example Variables: { "webhookUrn": "urn:brandfetch:organization:1234:webhook:1234" }
  ```
</CodeGroup>

### Filter and page the history

`deliveries` takes three optional arguments:

```graphql theme={null}
deliveries(filter: WebhookDeliveryFilter, first: IntegerBetween1And100 = 100, after: ID)
```

`filter` narrows the history to a time window on `createdAt`. Both bounds are inclusive, and you can pass either on its own: omit `from` to start at the oldest delivery still retained, and omit `to` to end at the newest.

`first` sets the page size. It defaults to `100` and cannot exceed it. To read further back, pass the `endCursor` from `pageInfo` as `after` and repeat the query until `hasNextPage` is `false`. Every edge also carries its own `cursor`, so you can resume from a specific delivery rather than from the end of a page.

<Note>
  `totalCount` is the number of deliveries the webhook has recorded since it was created. It is a lifetime total rather than the size of the result: it counts deliveries that have aged out of the retention window, and a `filter` does not narrow it. To count the deliveries in a window, page through the window and count the edges.

  `pageInfo.currentPage` and `pageInfo.totalPages` are `null` here, because neither can be computed while paging by cursor. Use `hasNextPage` and `endCursor` to walk the history instead.
</Note>

The query below reads the failed deliveries from a single day, oldest page first.

<CodeGroup>
  ```cURL cURL theme={null}
  curl --request POST \
      --header 'content-type: application/json' \
      --header 'authorization: Bearer YOUR_API_KEY_HERE' \
      --url 'https://graphql.brandfetch.io' \
      --data '{"query":"query RetrieveWebhookDeliveries($webhookUrn: URN!, $filter: WebhookDeliveryFilter, $first: IntegerBetween1And100, $after: ID) {\n  webhook(webhook: $webhookUrn) {\n    urn\n    deliveries(filter: $filter, first: $first, after: $after) {\n      totalCount\n      pageInfo {\n        endCursor\n        hasNextPage\n      }\n      edges {\n        cursor\n        node {\n          createdAt\n          deliveredAt\n          status\n          result {\n            statusCode\n            message\n          }\n        }\n      }\n    }\n  }\n}","variables":{"webhookUrn":"urn:brandfetch:organization:1234:webhook:1234","filter":{"from":"2026-08-01T00:00:00Z","to":"2026-08-01T23:59:59Z"},"first":25}}'
  ```

  ```GraphQL GraphQL theme={null}
  query RetrieveWebhookDeliveries(
    $webhookUrn: URN!
    $filter: WebhookDeliveryFilter
    $first: IntegerBetween1And100
    $after: ID
  ) {
    webhook(webhook: $webhookUrn) {
      urn
      deliveries(filter: $filter, first: $first, after: $after) {
        totalCount
        pageInfo {
          endCursor
          hasNextPage
        }
        edges {
          cursor
          node {
            createdAt
            deliveredAt
            status
            result {
              statusCode
              message
            }
          }
        }
      }
    }
  }

  # Example Variables: { "webhookUrn": "urn:brandfetch:organization:1234:webhook:1234", "filter": { "from": "2026-08-01T00:00:00Z", "to": "2026-08-01T23:59:59Z" }, "first": 25 }
  ```
</CodeGroup>

<Warning>
  If you already read `deliveries` without passing `first`, check that query. `first` now defaults to `100` and is capped there, where it was previously ignored and the field returned the whole retained history in one response. A query that relied on that behavior needs to follow `endCursor` to see everything it used to.
</Warning>
