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

# Webhooks

> Get notified in real-time when bookings are created or cancelled.

Webhooks let your systems react to booking events in real-time. When a booking is confirmed or cancelled, Slotflow sends an HTTP POST to your registered URL with the event details.

## Supported events

| Event               | When it fires                                        |
| ------------------- | ---------------------------------------------------- |
| `booking.confirmed` | A new booking is created via `POST /v1/bookings`     |
| `booking.cancelled` | A booking is cancelled via `DELETE /v1/bookings/:id` |

## Registering a webhook

```javascript
const res = await fetch("https://api.slotflow.dev/v1/webhooks", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://your-agent.com/webhooks/slotflow",
    events: ["booking.confirmed", "booking.cancelled"],
  }),
});

const webhook = await res.json();
// Save webhook.signing_secret securely — you'll need it to verify payloads
console.log(webhook.signing_secret); // "whsec_k8J2mN4p..."
```

* Each URL can only be registered once per organization (duplicates are rejected)
* You can register for one or both events
* The URL must be reachable from the internet
* The response includes a `signing_secret` (prefixed `whsec_`) — store it securely to verify incoming payloads

### Webhook limits by plan

| Plan    | Max webhooks |
| ------- | ------------ |
| Free    | 2            |
| Starter | 10           |
| Growth  | Unlimited    |

## Payload format

Every webhook delivery is an HTTP POST with a JSON body:

```json
{
  "event": "booking.confirmed",
  "created_at": "2026-03-10T14:05:00.000Z",
  "data": {
    "id": "b9e4f2a1-3c5d-4e6f-8a9b-0c1d2e3f4a5b",
    "human_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "starts_at": "2026-03-10T14:00:00.000Z",
    "ends_at": "2026-03-10T14:30:00.000Z",
    "duration_minutes": 30,
    "attendee_name": "Alex Rivera",
    "attendee_email": "alex@startup.io",
    "status": "confirmed",
    "metadata": {
      "lead_id": "lead_8294",
      "conversation_id": "conv_1847",
      "source": "ai_sdr"
    }
  }
}
```

The `data` object contains the full booking, including the `metadata` your agent passed when creating the booking. This is how you connect webhook events back to your agent's workflow.

## Delivery behavior

* **Method:** HTTP POST
* **Content-Type:** `application/json`
* **Timeout:** 10 seconds — your endpoint must respond within 10 seconds
* **Success:** Any 2xx response code

### Retry schedule

If your endpoint fails (non-2xx response or timeout), Slotflow retries:

| Attempt | Timing           |
| ------- | ---------------- |
| 1st     | Immediate        |
| 2nd     | 5 minutes later  |
| 3rd     | 60 minutes later |

After 3 failed attempts, the delivery is marked as failed. No further retries are attempted.

## Signature verification

Every webhook delivery includes an `X-Slotflow-Signature` header that lets you verify the payload was sent by Slotflow and hasn't been tampered with. The signature uses HMAC-SHA256 with a timestamp to prevent replay attacks.

### Header format

```
X-Slotflow-Signature: t=1710000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

* `t` — Unix timestamp (seconds) when the payload was signed
* `v1` — HMAC-SHA256 hex digest of `{timestamp}.{raw_body}` using your webhook's `signing_secret`

### Verifying in Node.js

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

function verifyWebhookSignature(rawBody, signatureHeader, signingSecret) {
  const [tPart, vPart] = signatureHeader.split(",");
  const timestamp = tPart.split("=")[1];
  const signature = vPart.split("=")[1];

  // Reject timestamps older than 5 minutes (replay protection)
  const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
  if (age > 300) {
    throw new Error("Webhook timestamp too old");
  }

  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );

  if (!isValid) {
    throw new Error("Invalid webhook signature");
  }

  return true;
}
```

### Verifying in Python

```python
import hmac
import hashlib
import time

def verify_webhook_signature(raw_body: str, signature_header: str, signing_secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    timestamp = parts["t"]
    signature = parts["v1"]

    # Reject timestamps older than 5 minutes
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("Webhook timestamp too old")

    expected = hmac.new(
        signing_secret.encode(),
        f"{timestamp}.{raw_body}".encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise ValueError("Invalid webhook signature")

    return True
```

### Important notes

* Always use the **raw request body** (not parsed JSON) when computing the signature
* Use **timing-safe comparison** (`crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python) to prevent timing attacks
* Reject timestamps older than **5 minutes** to prevent replay attacks
* Store your `signing_secret` securely (environment variable, secrets manager)

## Building a webhook handler

### Express.js example

```javascript
const express = require("express");
const crypto = require("crypto");
const app = express();

// Use raw body for signature verification
app.post("/webhooks/slotflow", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-slotflow-signature"];
  const rawBody = req.body.toString();

  // Verify the signature
  try {
    verifyWebhookSignature(rawBody, signature, process.env.SLOTFLOW_WEBHOOK_SECRET);
  } catch (err) {
    console.error("Webhook verification failed:", err.message);
    return res.sendStatus(401);
  }

  const { event, data } = JSON.parse(rawBody);

  switch (event) {
    case "booking.confirmed":
      console.log(`Booking confirmed: ${data.attendee_name} at ${data.starts_at}`);
      handleBookingConfirmed(data);
      break;

    case "booking.cancelled":
      console.log(`Booking cancelled: ${data.id}`);
      handleBookingCancelled(data);
      break;
  }

  // Always respond 200 quickly — do heavy processing asynchronously
  res.sendStatus(200);
});
```

### Python (Flask) example

```python
from flask import Flask, request

app = Flask(__name__)

@app.route("/webhooks/slotflow", methods=["POST"])
def slotflow_webhook():
    signature = request.headers.get("X-Slotflow-Signature")
    raw_body = request.get_data(as_text=True)

    # Verify the signature
    try:
        verify_webhook_signature(raw_body, signature, os.environ["SLOTFLOW_WEBHOOK_SECRET"])
    except ValueError as e:
        return str(e), 401

    payload = request.get_json()
    event = payload["event"]
    data = payload["data"]

    if event == "booking.confirmed":
        handle_booking_confirmed(data)
    elif event == "booking.cancelled":
        handle_booking_cancelled(data)

    return "", 200  # Always respond 200 quickly
```

## Using metadata

Metadata is the bridge between your agent's workflow and webhook events. Whatever JSON you pass in the booking's `metadata` field appears in the webhook payload.

**Common metadata patterns:**

```javascript
// AI Sales Agent
metadata: {
  lead_id: "lead_8294",
  conversation_id: "conv_1847",
  source: "ai_sdr",
  qualified_at: "2026-03-10T13:45:00Z",
}

// Customer Support
metadata: {
  ticket_id: "ticket_12345",
  customer_id: "cust_6789",
  issue_type: "billing",
  priority: "high",
}

// Recruiting
metadata: {
  application_id: "app_456",
  job_id: "job_789",
  stage: "technical_interview",
}
```

Your webhook handler reads `data.metadata` to route the event to the right system:

```javascript
app.post("/webhooks/slotflow", (req, res) => {
  const { event, data } = req.body;

  if (event === "booking.confirmed") {
    const { source } = data.metadata;

    if (source === "ai_sdr") {
      crm.updateLead(data.metadata.lead_id, { status: "demo_booked" });
    } else if (data.metadata.ticket_id) {
      supportSystem.updateTicket(data.metadata.ticket_id, { status: "callback_scheduled" });
    }
  }

  res.sendStatus(200);
});
```

## Managing webhooks

### List webhooks

```bash
curl https://api.slotflow.dev/v1/webhooks \
  -H "Authorization: Bearer sk_live_your_api_key"
```

### Delete a webhook

```bash
curl -X DELETE https://api.slotflow.dev/v1/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer sk_live_your_api_key"
```

## Best practices

1. **Verify signatures** — always verify the `X-Slotflow-Signature` header to ensure payloads are authentic. Reject any request with an invalid or missing signature.

2. **Respond 200 immediately** — do heavy processing asynchronously. Slotflow times out after 10 seconds and will retry, which could cause duplicate processing.

3. **Make handlers idempotent** — due to retries, you may receive the same event twice. Use `data.id` (the booking ID) to deduplicate.

4. **Log webhook payloads** — store the raw payload for debugging. If something goes wrong, you'll want to see exactly what was delivered.

5. **Use HTTPS** — your webhook URL should use HTTPS in production to protect the payload in transit.

6. **Monitor delivery** — check the Slotflow dashboard for failed deliveries. Common causes: endpoint down, slow response (>10s timeout), non-2xx response codes.