> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developers.zorvia.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developers.zorvia.io/_mcp/server.

# Set up a webhook

> Send message events from a Zorvia channel to your application.

Use webhooks to keep your application in sync with message activity in Zorvia. When an event occurs, Zorvia sends an HTTP `POST` request to the endpoint configured for that channel.

Before you begin, create an HTTPS endpoint in your application that can accept JSON requests.

## Create a channel webhook

#### Open the channel

In your Zorvia workspace, select the channel that should send events.

#### Open Webhook settings

Go to **Settings → Webhook**, then select **Create Webhook**.

#### Enter the destination

Give the webhook a recognizable name and enter your HTTPS endpoint.

#### Secure the webhook

Add a signing secret so your application can verify each request. You can also add static request headers as a JSON object.

#### Enable delivery

Select **Save & Enable**. To finish the configuration without starting delivery, turn on **Save As Draft** before saving.

If your endpoint requires an API key or another fixed value, add it as a custom header:

```json
{
  "X-API-Key": "your-receiver-api-key",
  "X-Webhook-Source": "zorvia"
}
```

Header values must be strings, numbers, booleans, or `null`. Webhook bodies use `Content-Type: application/json`.

## Receive webhook requests

For each delivery, your endpoint should:

1. accept `POST` requests with a JSON body;
2. verify the request signature when a signing secret is configured;
3. process events idempotently; and
4. return a `2xx` response after accepting the event.

Zorvia retries a delivery when your endpoint times out or returns a non-`2xx` response. A delivery is attempted up to three times with exponential backoff, and each attempt has a 60-second timeout.

Return a response quickly and move long-running work to a background job. Because a delivery can be retried, use a combination of `message.id`, `event.type`, and `event.occurred_at` as an idempotency key. Including the event timestamp prevents a later edit to the same message from being mistaken for a duplicate.

## Verify a signed request

When you configure a signing secret, each request includes a `Signature` header. The value is a lowercase hexadecimal HMAC-SHA256 digest of the raw request body, calculated with your signing secret.

```js
import crypto from "node:crypto";

export function verifyZorviaWebhook(rawBody, signature, secret) {
  if (!signature) {
    return false;
  }

  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const received = Buffer.from(signature, "hex");
  const calculated = Buffer.from(expected, "hex");

  return (
    received.length === calculated.length &&
    crypto.timingSafeEqual(received, calculated)
  );
}
```

Use the unmodified request bytes when calculating the digest. Parsing and serializing the JSON again can change the body and cause verification to fail.

> **Warning**
>
> Treat the signing secret and any credential in a custom header as sensitive. Store them in a secret manager and never expose them in browser code.

Requests also include a `Timestamp` header containing the Unix time at which the delivery attempt was made. If you do not configure a signing secret, the request does not include a `Signature` header.

> **Warning**
>
> The signature covers only the raw request body; it does not include the `Timestamp` header. It verifies that the body was signed with your secret, but it does not prevent a captured request from being replayed. Do not rely on the timestamp alone for replay protection. Process deliveries idempotently using the event fields described above.

## Manage delivery

From **Channel → Settings → Webhook**, select **Manage** on a webhook to update its name, URL, signing secret, or static headers.

* **Disable Webhook** pauses delivery without removing the configuration.
* **Enable** resumes delivery for a draft or disabled webhook.
* **Delete** permanently removes the webhook from the channel.

The [webhook events reference](/webhooks/events) lists the event names and payload fields your endpoint can receive.