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

# Set up webhooks

> Learn how to set up webhooks in Sequence.

In Sequence, you can listen to various events to trigger your own workflows. These are called webhooks.

Sequence sends webhooks when something changes in your account - customers, invoices, billing schedules, credit notes, quotes. Point a webhook at your endpoint, and Sequence will POST the event data there whenever one of those changes occurs.

For the full event catalog, payload envelope, signature format, and delivery behaviour, see [Webhook events](/reference/webhook-events).

## Quickstart

This section walks you through setting up an endpoint, configuring a webhook in Sequence, and confirming a test delivery.

### 1. Spin up an endpoint

The fastest way to start is [webhook.site](https://webhook.site). It gives you a public URL and shows every request that lands. Copy your unique URL.

If you want to receive webhooks on your machine, set up a local server and use a tunnel like [ngrok](https://ngrok.com/docs/agent/overview) to receive webhooks locally.

### 2. Create a webhook

<Tabs>
  <Tab title="Dashboard">
    <Frame>
      <img src="https://mintcdn.com/sequence/-SlMgdh2LP9GpWSY/images/product/resources/webhook-configure.png?fit=max&auto=format&n=-SlMgdh2LP9GpWSY&q=85&s=dda33dc2571b5f585b83f8b5909c0226" alt="Webhooks in the Sequence dashboard" width="1677" height="980" data-path="images/product/resources/webhook-configure.png" />
    </Frame>

    Open **Settings → Webhooks** and create a new webhook. Paste your endpoint URL, choose the events you want, and save.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    curl --request POST \
      --url https://eu.sequencehq.com/api/notifications/policies \
      --header 'Authorization: <authorization>' \
      --header 'Content-Type: application/json' \
      --data '{
        "name": "Dev team integration webhooks",
        "recipients": ["https://api.company.com/webhooks"],
        "notificationTypes": ["INVOICE_ISSUED", "CUSTOMER_CREATED"],
        "channel": "WEBHOOK"
      }'
    ```

    Then go to **Webhooks → Show secret key** and copy the secret key - you will need it for verification.
  </Tab>
</Tabs>

### 3. Trigger a test event

Create a customer or an invoice in the dashboard. Within a few seconds the request should appear at your endpoint. Confirm the payload's `notificationType` matches the event you triggered.

## Verify the signature

Every webhook arrives with a `Sequence-Signature` header containing a timestamp and HMAC signature:

```
Sequence-Signature: t=1748866120822,s=6be6887a470c20978427ff89941ebecb83b2804ceed66fd93b3afc00de2e9f11
```

To verify a delivery:

1. Concatenate the timestamp and the raw request body with a `.` separator: `TIMESTAMP.RAW_BODY`. For example: `1779974397972.{"notificationType":"..."}`
2. Compute HMAC-SHA-256 of that string using your webhook's signing secret
3. Compare the result with `s` using a constant-time comparison

<Warning>
  **Use raw bytes, not re-serialised JSON.** The signature is computed over the exact bytes Sequence sent. If you parse the body to a JSON object and re-stringify it before verification, whitespace and key ordering differences will produce a different hash and verification will always fail.
</Warning>

### Full working example

Save the following as `verify-webhooks-server.ts`. It runs an HTTP server on port 8080 that verifies signatures on requests to `POST /webhook`.

Dependencies: `npm install express @types/express tsx`
Run with: `npx tsx verify-webhooks-server.ts`

```ts theme={null}
#!/usr/bin/env tsx
const express = require("express");
const cryptoModule = require("crypto");

// Your webhook secret (from webhooks settings in the Sequence dashboard)
const WEBHOOK_SECRET = "YOUR_WEBHOOK_SECRET";
const PORT = 8080;

const app = express();

function verifyWebhookSignature(payloadBytes: Buffer, signatureHeader: string, secret: string): boolean {
  const payloadString = payloadBytes.toString("utf-8");

  const parts = signatureHeader.split(",");
  const timestampPart = parts.find((p: string) => p.startsWith("t="));
  const signaturePart = parts.find((p: string) => p.startsWith("s="));

  if (!timestampPart || !signaturePart) return false;

  const timestamp = timestampPart.substring(2);
  const signatureFromHeader = signaturePart.substring(2);

  const dataToSign = `${timestamp}.${payloadString}`;
  const expectedSignature = cryptoModule
    .createHmac("sha256", secret)
    .update(dataToSign, "utf8")
    .digest("hex");

  const signatureBuffer = Buffer.from(signatureFromHeader, "hex");
  const expectedBuffer = Buffer.from(expectedSignature, "hex");

  if (signatureBuffer.length !== expectedBuffer.length) return false;
  return cryptoModule.timingSafeEqual(signatureBuffer, expectedBuffer);
}

// Ensure webhook handler receives the raw bytes via express.raw()
app.use("/webhook", express.raw({ type: "application/json" }));

app.post("/webhook", (req: any, res: any) => {
  const payloadBytes: Buffer = req.body;
  const signatureHeader = req.headers["sequence-signature"] as string;

  if (!payloadBytes?.length || !signatureHeader) {
    return res.status(400).send("Bad Request");
  }

  if (verifyWebhookSignature(payloadBytes, signatureHeader, WEBHOOK_SECRET)) {
    console.log("Webhook verified.");
    return res.status(200).send("OK");
  } else {
    console.log("Verification failed.");
    return res.status(403).send("Verification failed");
  }
});

app.listen(PORT, "0.0.0.0", () => {
  console.log(`Webhook server listening on http://0.0.0.0:${PORT}/webhook`);
});
```

### Verify manually

When you want to confirm an end-to-end setup without writing code, two tools together are enough:

1. Point your webhooks at [webhook.site](https://webhook.site) URL and trigger an event in Sequence
2. You should see the webhook event captured in webhook.site with the raw payload and the `Sequence-Signature` header
3. Open [metatoolhub.com](https://metatoolhub.com/tools/hmac-encrypt-decrypt)'s HMAC verification tool
4. Algorithm: select "HMAC-SHA-256"
5. Message/Data: paste the timestamp value, then `.`, then the raw event payload
6. Secret Key: paste your webhook's signing secret
7. HMAC to Verify: paste the `s` value from the `sequence-signature` header

A "Valid HMAC" message confirms your secret and the payload are correct.

<Frame>
  <img src="https://mintcdn.com/sequence/-SlMgdh2LP9GpWSY/images/product/resources/webhook-received-locally.png?fit=max&auto=format&n=-SlMgdh2LP9GpWSY&q=85&s=cf5ae71bd399ced4a76988debc1f8924" alt="A Sequence webhook received by webhook.site with its signature verified in metatoolhub.com's HMAC tool" width="4340" height="2808" data-path="images/product/resources/webhook-received-locally.png" />
</Frame>

## Handle webhooks well

* **Return 200 quickly.** Acknowledge the request and do any non-trivial work asynchronously (via a queue or background worker). A slow handler blocks the delivery and risks retries
* **Handle out-of-order events and out-of-date payloads.** You can use webhooks as a signal that something has changed, then re-read the resource from the Sequence API to get its current state
* **Be idempotent.** The same event may arrive more than once if a previous delivery failed
* **Ignore unknown event types and fields.** Sequence may add new event types or fields in the future. Accept and ignore types your handler doesn't recognise rather than throwing

## Troubleshooting

### Signature mismatch

Almost always caused by verifying against re-serialised JSON instead of the raw request body. Confirm your framework gives you the exact bytes Sequence sent.

A mismatch in the working example above prints lines like:

```
Verification failed.
Details: Got='abc123...', Expected='def456...'
```

You can use an online HMAC verification tool like [metatoolhub.com](https://metatoolhub.com/tools/hmac-encrypt-decrypt) to verify the expected signature.

### Webhook not received, or repeatedly retried

Your handler must return a `2xx` response. Confirm the endpoint is publicly reachable and isn't returning an error to Sequence. Any non-`2xx` response causes Sequence to retry. Failed deliveries are retried for up to **24 hours**.

## Next step

For the full event catalog, payload envelope, signature format, and delivery behaviour, see [Webhook events](/reference/webhook-events).
