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

# Webhooks

> Get a signed HTTPS request when something changes.

Instead of polling, register an HTTPS endpoint. BrandGate sends a small signed event when an order, invoice or stock level changes; you then fetch the object for its current state.

## Create an endpoint

A company admin can add one in **Settings → Developers → Webhooks**, or your integration can create one with the `webhooks:write` scope:

```bash theme={null}
curl https://api.brandgate.dev/v1/webhook_endpoints \
  -H "Authorization: Bearer $BRANDGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://erp.example.com/brandgate/webhooks","events":["order.created","order.updated"]}'
```

The response contains a `secret` (`whsec_…`). It is shown **once**; store it with your other secrets.

Endpoint rules: `https://` on port 443, no user name or password in the URL, and a public address. Requests to private or internal networks are refused. A company can have up to 20 endpoints.

## Events

| Type | When | Needs scope |
| - | - | - |
| `order.created` | A reseller placed an order | `orders:read` |
| `order.updated` | An order changed (status, lines, acknowledgement) | `orders:read` |
| `invoice.created` | An invoice was created | `invoices:read` |
| `invoice.updated` | An invoice changed, for example it was paid | `invoices:read` |
| `product.stock_low` | A product became low on stock or sold out | `inventory:read` |

An endpoint created with an API key or OAuth connection only receives events that the key or connection is allowed to read, and stops receiving them when the key is revoked or the app is disconnected.

## Payload

Events are thin on purpose. They tell you what changed, not the data itself, so a delayed or repeated event can never overwrite newer data:

```json theme={null}
{
  "id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
  "object": "event",
  "type": "order.created",
  "created_at": "2026-10-20T08:15:02.000Z",
  "data": { "object": "order", "id": "6f1c0a52-3c1e-4f43-9a55-2a1f0e7d9b10" }
}
```

Fetch the current object with `GET /v1/orders/{id}`.

## Headers

| Header | Value |
| - | - |
| `BrandGate-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>` |
| `BrandGate-Event-Id` | The event `id`. Use it to ignore duplicates. |
| `BrandGate-Delivery-Attempt` | `1` for the first attempt, then `2`, `3`… |
| `Content-Type` | `application/json` |

## Verify the signature

The signature is an HMAC-SHA256, keyed with the **whole secret string including `whsec_`**, over `<t>.<raw request body>`. Always verify against the raw bytes you received, before parsing JSON, and reject timestamps more than 5 minutes old.

```javascript Node.js theme={null}
import crypto from "node:crypto";

export function verifyBrandGate(secret, rawBody, header) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
  const timestamp = Number(parts.t);
  if (!Number.isSafeInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const received = Buffer.from(parts.v1 ?? "", "hex");
  return received.length === 32 && crypto.timingSafeEqual(received, Buffer.from(expected, "hex"));
}
```

```python Python theme={null}
import hashlib, hmac, time

def verify_brandgate(secret: str, raw_body: bytes, header: str) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    try:
        timestamp = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > 300:
        return False
    message = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

```csharp C# theme={null}
using System.Security.Cryptography;
using System.Text;

static bool VerifyBrandGate(string secret, string rawBody, string header)
{
    var parts = header.Split(',').Select(p => p.Split('=', 2)).ToDictionary(p => p[0], p => p.Length > 1 ? p[1] : "");
    if (!long.TryParse(parts.GetValueOrDefault("t"), out var t)) return false;
    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return false;
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{t}.{rawBody}"))).ToLowerInvariant();
    return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(parts.GetValueOrDefault("v1", "")));
}
```

## Respond quickly

Answer with any `2xx` status within 10 seconds, then do the work in the background. Redirects are not followed and count as failures.

## Retries

A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, so up to 8 attempts over about two days. Events can arrive more than once and out of order: use `BrandGate-Event-Id` to skip duplicates and always fetch the current object.

After 50 failed deliveries in a row the endpoint is paused and shown as **Paused** in BrandGate. Create it again once your receiver works.

## Ordering and catching up

Webhooks are a signal, not a log. If your receiver was down for longer than the retry window, catch up with `GET /v1/orders?updated_since=<last sync time>` (see [ERP sync](/guides/erp-sync)).


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