> ## 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: [verify every delivery](/delivery-methods/webhooks/best-practices#verify-events-are-sent-from-brandfetch).

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 Express handler verifies the delivery with the official [Standard Webhooks](https://www.standardwebhooks.com) library, puts the event on your queue with its `webhook-id`, and returns a 200 response. Your queue worker [skips any `webhook-id` it has already processed](/delivery-methods/webhooks/best-practices#handle-duplicate-events), handles the event by its type, and then records the `webhook-id`.

The sample is an ES module: save it as a `.mjs` file, or set `"type": "module"` in your `package.json`.

```javascript theme={null}
import express from "express";
import { Webhook } from "standardwebhooks";

const webhook = new Webhook(process.env.BRANDFETCH_WEBHOOK_SECRET);
const app = express();

// express.raw() keeps the body as the exact bytes Brandfetch signed.
// A delivery can be larger than its default 100 KB limit.
app.post(
  "/webhooks/brandfetch",
  express.raw({ type: "application/json", limit: "1mb" }),
  async (req, res) => {
    let event;
    try {
      event = webhook.verify(req.body, req.headers);
    } catch {
      return res.status(400).json({ message: "Invalid signature" });
    }

    try {
      // Define enqueue to hand the event to your queue, such as Amazon SQS.
      await enqueue({ webhookId: req.headers["webhook-id"], event });
    } catch {
      // Any status other than 2xx makes Brandfetch retry the delivery.
      return res.status(500).json({ message: "Could not queue the event" });
    }

    // Acknowledge receipt once the event is queued, before any slow work.
    res.json({ received: true });
  },
);

// Your queue worker calls this for each queued event.
export async function handleEvent({ webhookId, event }) {
  // Define alreadyProcessed and markProcessed on your datastore (see "Handle duplicate events").
  if (await alreadyProcessed(webhookId)) return;

  switch (event.type) {
    case "brand.updated":
      // Then define and call a method to handle the brand updated event.
      await handleBrandUpdated(event.data.object);
      break;
    case "brand.verified":
      // Then define and call a method to handle the brand verified event.
      await handleBrandVerified(event.data.object);
      break;
    // ... handle other event types
    default:
      console.log(`Unhandled event type ${event.type}`);
  }

  // Mark it only once the event is handled, so your queue can retry a failed run.
  await markProcessed(webhookId);
}

app.listen(process.env.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. Brandfetch doesn’t follow redirects, so register the URL that answers the delivery itself. Deliveries come from three fixed IP addresses you can allow-list. Protect the endpoint with an `Authorization` header and signature verification as well. See [Source IPs and allow-listing](/delivery-methods/webhooks/best-practices#source-ips-and-allow-listing).
</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      secret\n    }\n  }\n}","variables":{"input":{"description":"My new webhook","events":["brand.updated","brand.verified"],"url":"https://httpbin.org/status/200","urlHeaders":[{"name":"Authorization","value":"Bearer YOUR_RANDOM_TOKEN"}]}}}'
  ```

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

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

`urlHeaders` is optional. It sets an `Authorization` header that Brandfetch sends with every delivery. Have your perimeter drop any request whose `Authorization` header is not exactly this value. Use a long random token, such as the output of `openssl rand -hex 32`. The value must be printable ASCII, with no line breaks. The API never returns it once it’s set.

The response includes the webhook’s `secret`, which you need to [verify deliveries](/delivery-methods/webhooks/best-practices#verify-events-are-sent-from-brandfetch). You can read it again later with the `webhook` query. A secret can’t be rotated. To replace it, delete the webhook with `deleteWebhook` and create a new one. Deleting a webhook also deletes its subscriptions and its delivery history, and drops any retries still pending. Subscribe the new webhook again. New subscriptions are charged as usual.

## 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, 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. The API never returns its value. See [Source IPs and allow-listing](/delivery-methods/webhooks/best-practices#source-ips-and-allow-listing). |
| `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` 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.

The following GraphQL query returns the webhook’s most recent delivery attempts, up to 100 and newest first, with your endpoint’s responses and the 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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.