# 🔔 Webhooks (Beta): Real-Time Notifications for Your Bookings

> Set up Cowlendar webhooks: the 8 events, request headers, how to verify the HMAC-SHA256 signature with Node and Python code, retries and how to troubleshoot.

A webhook is a message that Cowlendar sends to your server the moment something happens: a booking is created, canceled, rescheduled and more. You do not have to keep asking the API.

Use webhooks together with the [Public API (Beta)](https://help.cowlendar.com/a/public-api-beta-generate-tokens-and-read-or-create-bookings) to build integrations with your CRM, ERP, or any custom app.

**Where to find it:** Shopify admin > Apps > Cowlendar Booking > **Settings** > **Webhooks**.

![The top of the Webhooks page with the Beta badge, the blue note and the Add endpoint button](https://orka-chat.fra1.cdn.digitaloceanspaces.com/prod/help/671a5e245d077ea954b2cd4c/1792171145810-mcp-claude-in-chrome-blob-1792171102134-sgig4b.webp)

## The Webhooks page

The table lists your endpoints with these columns:

| Column | What it shows |
| --- | --- |
| Endpoint | The name and the URL. |
| Events | How many events the endpoint receives. |
| Status | Active or Inactive. |
| Last delivery | The HTTP code your server answered (for example 200) and the date. |
| Actions | Test, See deliveries, pause or play, edit and delete. |

## Step 1: Add an endpoint

The signing secret is shown in full only once at creation. Store it securely. You will need it to verify incoming requests.

1. Open **Settings** > **Webhooks**.
2. Click **Add endpoint**.
3. In the window **Add webhook**, type a name in the **Name** field (up to 60 characters, for example *Zapier sync*). Then enter your server address in **URL**, which must use HTTPS. Under **Events**, tick each event you want to receive. Click **Create**.
4. Copy the signing secret and keep it safe.

![The Add endpoint form](https://orka-chat.fra1.cdn.digitaloceanspaces.com/prod/help/671a5e245d077ea954b2cd4c/1792171149233-mcp-claude-in-chrome-blob-1792171110511-ljbgxd.webp)

Your endpoint must be a public HTTPS address that answers with a 2xx status within 10 seconds.

## The 8 events

| Event | Sent when |
| --- | --- |
| `booking.created` | A booking is created. |
| `booking.confirmed` | A pending booking is confirmed. |
| `booking.declined` | A pending booking is declined. |
| `booking.canceled` | A booking is canceled. |
| `booking.rescheduled` | A booking is rescheduled. The `previous` field has the old dates and teammates. |
| `booking.attendance_changed` | The attendance of a booking changes. The `previous` field has the old attendance. |
| `subscription.created` | A subscription is created. |
| `subscription.billing_failed` | A subscription payment fails. The `previous` field has the previous failure count. |

Every payload has `id`, `type`, `created_at`, `shop_domain` and `data` (the booking or the subscription).

## Request headers

Every request Cowlendar sends has these headers:

| Header | Use |
| --- | --- |
| `X-Cowlendar-Event-Id` | Unique id of the event. Save it to avoid processing the same event twice. |
| `X-Cowlendar-Event-Type` | The event name, for example `booking.created`. |
| `X-Cowlendar-Timestamp` | Unix time in seconds when it was sent. |
| `X-Cowlendar-Signature` | The signature, in the format `t=<timestamp>,v1=<hex>`. |

## Verify the signature

Always verify the signature before you trust a request.

1. Read the raw body of the request, exactly as received (do not parse and re-serialize it).
2. Read `X-Cowlendar-Timestamp`.
3. Build the text `<timestamp>.<raw body>`: the timestamp, a dot, then the raw body.
4. Calculate HMAC-SHA256 of that text with your signing secret.
5. Compare the result, in hex, with the `v1=` value of `X-Cowlendar-Signature`. Use a constant-time comparison.
6. Reject the request if the timestamp is more than 5 minutes old.

Node.js:

```js
const crypto = require("crypto");

function verify(rawBody, timestamp, signatureHeader, secret) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (age > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const received = signatureHeader.split(",").find((p) => p.startsWith("v1=")).slice(3);
  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

Python:

```python
import hmac, hashlib, time

def verify(raw_body: bytes, timestamp: str, signature_header: str, secret: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    message = timestamp.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    received = next(p for p in signature_header.split(",") if p.startswith("v1="))[3:]
    return hmac.compare_digest(expected, received)
```

## Retries

If your server does not answer with a 2xx within 10 seconds, Cowlendar retries the delivery: after 1 minute, 5 minutes, 25 minutes, 2 hours, 10 hours and 50 hours. After about 20 failures in a row, the endpoint is turned off automatically and its status changes to **Inactive**. Fix the problem and turn it on again with the play button.

## Test and troubleshoot

- **Test**: sends a test request to your endpoint.
- **See deliveries**: shows the deliveries Cowlendar tried, to understand why one failed.
- **Pause / play**: turns an endpoint off or on.
- **Edit and delete**: change or remove an endpoint.

Quick fixes:

| Problem | What to check |
| --- | --- |
| The last delivery shows a red code | Your server answered with an error. A 302 means your URL redirects; use the final URL. |
| The signature never matches | You are using the parsed body instead of the raw body, or the wrong secret. |
| The endpoint became Inactive | It failed about 20 times in a row. Fix your server and press play. |
| You receive the same event twice | Use `X-Cowlendar-Event-Id` to ignore duplicates. |

![The Events, Status, Last delivery and Actions columns of the endpoints table, with a green 200 pill and a red 302 pill, without the Endpoint column](https://orka-chat.fra1.cdn.digitaloceanspaces.com/prod/help/671a5e245d077ea954b2cd4c/1792171152834-mcp-claude-in-chrome-blob-1792171102135-g6g1ts.webp)

## Related

- [Public API (Beta): Generate Tokens and Read or Create Bookings](https://help.cowlendar.com/a/public-api-beta-generate-tokens-and-read-or-create-bookings)
- [Connect Cowlendar to CRM, ERP or App with the Public API and Webhooks](https://help.cowlendar.com/a/connect-cowlendar-to-crm-erp-or-app-public-api-webhooks)
- [Zapier Integration](https://help.cowlendar.com/a/zapier-integration)
