> ## 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).
4. [Send a test event](#send-a-test-event) to check your endpoint, your firewall rules and your signature verification together.

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;

  // Test events are about a fictional brand.
  if (event.test) {
    console.log(`Test ${event.type} received`);
    await markProcessed(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.

## Send a test event

You don't have to wait for a brand to change to see a delivery arrive. `sendWebhookTestEvent` sends a test event to your endpoint straight away, so you can check your handler, your firewall rules and your signature verification in one go. A test needs no subscription and costs nothing. You can also send one from the [developer dashboard](https://developers.brandfetch.com/dashboard/webhooks): open the webhook and choose **Send test event**.

Name the event types to send in `events`, or leave it out to send every event type the webhook listens to. Brandfetch makes one delivery per event type.

<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 SendWebhookTestEvent($webhookUrn: URN!, $events: [EventName!]) {\n  sendWebhookTestEvent(webhook: $webhookUrn, events: $events) {\n    code\n    message\n    success\n    deliveries {\n      urn\n      status\n      test\n      payload {\n        type\n      }\n      result {\n        message\n      }\n    }\n  }\n}","variables":{"webhookUrn":"urn:brandfetch:organization:1234:webhook:5678","events":["brand.updated"]}}'
  ```

  ```GraphQL GraphQL theme={null}
  mutation SendWebhookTestEvent($webhookUrn: URN!, $events: [EventName!]) {
    sendWebhookTestEvent(webhook: $webhookUrn, events: $events) {
      code
      message
      success
      deliveries {
        urn
        status
        test
        payload {
          type
        }
        result {
          message
        }
      }
    }
  }

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

The call returns as soon as the deliveries are queued, usually with the status `PENDING`. Your endpoint's answer shows up in the webhook's [delivery history](#debugging-delivery-issues) a few seconds later, where test deliveries have `test: true`. A delivery's `urn` is the same as the `delivery` field in the body your endpoint received.

A test delivery matches a real delivery in everything that matters for checking your setup:

* It comes from the same [IP addresses](/delivery-methods/webhooks/best-practices#source-ips-and-allow-listing), with your `Authorization` header and the same signature headers.
* Its body has the same shape, plus `"test": true` at the top level. The signature covers that field, so you can rely on it.
* Its `webhook-id` is new for every test, so your duplicate check doesn't drop a second test.

It differs in a few ways:

* `data.object` is always the same fictional brand, Webhook Test Brand (`urn:brandfetch:brand:idWebhookTestBrand`), whichever brands you subscribe to. Don't look it up in the Brand API: it isn't there, and the request can still be charged. Its domain, `example.org`, is a reserved example domain that belongs neither to the test brand nor to you, so don't look that up either: the Brand API may return a different brand for it and charge for the lookup. Its logos and banner are Brandfetch's own images, used as placeholders, so don't save them as any brand's assets.
* You can't subscribe to the test brand, and you don't need to: a test reaches the webhook without a subscription.
* A test is attempted once and never retried. A failed test sends no email, and tests don't count toward [switching off a failing webhook](/delivery-methods/webhooks/delivery-behaviors#disable-behavior), either way.
* You can send a test while the webhook is switched off with `enabled: false` ([paused](#pause-a-webhook)), but not while your organization's webhooks are on a [billing hold](/delivery-methods/webhooks/overview#when-a-renewal-cannot-be-funded).

<Warning>
  Check `test` before your handler acts on an event, fetches its brand or saves its images. A test `brand.deleted` is about the fictional brand, not about a brand you track, and a handler that matches brands by domain would otherwise act on `example.org`.
</Warning>

<AccordionGroup>
  <Accordion title="The test brand in data.object">
    For every event type except `brand.deleted`, `data.object` is:

    ```json theme={null}
    {
      "__typename": "Brand",
      "claimed": true,
      "colors": [
        {
          "brightness": 73,
          "hex": "#ff00ff",
          "type": "accent"
        },
        {
          "brightness": 20,
          "hex": "#141414",
          "type": "dark"
        },
        {
          "brightness": 245,
          "hex": "#f5f5f5",
          "type": "light"
        }
      ],
      "company": {
        "employees": 2,
        "financialIdentifiers": {
          "isin": [],
          "ticker": []
        },
        "foundedYear": 2026,
        "industries": [
          {
            "emoji": "🖥",
            "id": "37",
            "name": "Programming and Developer Software",
            "parent": {
              "emoji": "🖥",
              "id": "28",
              "name": "Computers Electronics and Technology",
              "slug": "computers-electronics-and-technology"
            },
            "score": 1,
            "slug": "programming-and-developer-software"
          }
        ],
        "kind": "PRIVATELY_HELD",
        "location": {
          "city": null,
          "country": null,
          "countryCode": null,
          "region": null,
          "state": null,
          "subregion": null
        }
      },
      "description": "A fictional brand that Brandfetch sends in webhook test events.",
      "domain": "example.org",
      "fonts": [
        {
          "name": "Inter",
          "origin": "google",
          "originId": "Inter",
          "type": "title",
          "weights": []
        },
        {
          "name": "Inter",
          "origin": "google",
          "originId": "Inter",
          "type": "body",
          "weights": []
        }
      ],
      "id": "idWebhookTestBrand",
      "images": [
        {
          "formats": [
            {
              "background": null,
              "format": "jpeg",
              "height": 500,
              "size": null,
              "src": "https://cdn.brandfetch.io/brandfetch.com/w/1500/h/500/banner.jpeg?c=CLIENT_ID",
              "width": 1500
            }
          ],
          "tags": [],
          "type": "banner"
        }
      ],
      "isNsfw": false,
      "lastCrawledAt": "2026-10-01T00:00:00.000Z",
      "links": [],
      "logos": [
        {
          "formats": [
            {
              "background": null,
              "format": "png",
              "height": 400,
              "size": null,
              "src": "https://cdn.brandfetch.io/brandfetch.com/w/400/h/400/theme/dark/icon.png?c=CLIENT_ID",
              "width": 400
            }
          ],
          "tags": [
            "lettermark"
          ],
          "theme": "dark",
          "type": "icon"
        },
        {
          "formats": [
            {
              "background": "transparent",
              "format": "svg",
              "height": null,
              "size": null,
              "src": "https://cdn.brandfetch.io/brandfetch.com/theme/dark/logo.svg?c=CLIENT_ID",
              "width": null
            },
            {
              "background": "transparent",
              "format": "png",
              "height": 128,
              "size": null,
              "src": "https://cdn.brandfetch.io/brandfetch.com/w/800/h/128/theme/dark/logo.png?c=CLIENT_ID",
              "width": 800
            }
          ],
          "tags": [
            "combination"
          ],
          "theme": "dark",
          "type": "logo"
        },
        {
          "formats": [
            {
              "background": "transparent",
              "format": "svg",
              "height": null,
              "size": null,
              "src": "https://cdn.brandfetch.io/brandfetch.com/theme/dark/symbol.svg?c=CLIENT_ID",
              "width": null
            },
            {
              "background": "transparent",
              "format": "png",
              "height": 256,
              "size": null,
              "src": "https://cdn.brandfetch.io/brandfetch.com/w/256/h/256/theme/dark/symbol.png?c=CLIENT_ID",
              "width": 256
            }
          ],
          "tags": [
            "lettermark"
          ],
          "theme": "dark",
          "type": "symbol"
        }
      ],
      "longDescription": "Webhook Test Brand does not exist. Brandfetch sends it in every test event, so you can check your endpoint without waiting for a real brand to change. The Brand API does not know it.",
      "name": "Webhook Test Brand",
      "qualityScore": 0.5,
      "urn": "urn:brandfetch:brand:idWebhookTestBrand",
      "verified": true
    }
    ```

    Every `src` is a [Logo CDN](/logo-api/overview) URL for one of Brandfetch's own images. Its `c` parameter is a client ID that Brandfetch generates when it sends the test, so it differs from one test to the next.

    For `brand.deleted`, `data.object` carries only `__typename`, `id`, `domain`, `name` and `urn`, as it does for a real deletion.
  </Accordion>
</AccordionGroup>

### When a test is declined

When a test can't be sent, `sendWebhookTestEvent` returns `success: false`, a `code` and a `message`, and sends nothing.

| `code` | Message | What to do |
| - | - | - |
| `EVENT_NOT_ON_WEBHOOK` | This webhook does not listen to brand.deleted. Add it to the webhook's events with updateWebhook, then send the test again. | Add the event type with [`updateWebhook`](#edit-a-webhook), or leave `events` out to test the event types the webhook already listens to. A webhook with no event types can't be tested. |
| `TEST_EVENT_IN_PROGRESS` | A test sent to this webhook in the last two minutes is still being delivered. Wait for it to finish, then send another. | Wait until your previous test deliveries have settled, usually a few seconds. A test never blocks the next one for more than two minutes. |
| `TEST_EVENT_LIMIT_REACHED` | Too many test events. A webhook can be sent 12 test events a minute, and an organization 24 a minute and 120 an hour. Try again later. | Wait for the minute or the hour to pass. |
| `BILLING_HOLD` | Webhooks are paused for this organization. Resolve the billing hold to send test events. | See [When a renewal cannot be funded](/delivery-methods/webhooks/overview#when-a-renewal-cannot-be-funded). |

A call that goes through returns `success: true` and `code: "success"`. If only some of the event types could be sent, the message says how many were. A delivery that couldn't be sent is already `FAILED`, and its `result.message` reads "This test event could not be sent. Send it again." Go by that message rather than the status: a delivery that was sent can already be `FAILED` too, if your endpoint rejected it quickly.

If none of the event types could be sent, the result is `success: false` with the code `failure` and no deliveries. Send the test again in a moment.

## 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, apart from [test events](#send-a-test-event) you send yourself. 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. `sendWebhookTestEvent` has codes of its own, listed under [When a test is declined](#when-a-test-is-declined).
</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. Test deliveries appear here too, with `test: true`.

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          test\n          payload {\n            type\n          }\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
            test
            payload {
              type
            }
            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.

`filter` also takes `test`: set it to `false` to leave test deliveries out, or to `true` to see only them. With `test` set, a page can hold fewer deliveries than `first` while `hasNextPage` is still `true`, so keep paging by `endCursor`.

`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. Test deliveries are not counted. To count the deliveries in a window, page through the window with `test: false` 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.