Webhooks

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

EventWhen it fires
booking.confirmedA new booking is created via POST /v1/bookings
booking.cancelledA booking is cancelled via DELETE /v1/bookings/:id

Registering a webhook

1const res = await fetch("https://api.slotflow.dev/v1/webhooks", {
2 method: "POST",
3 headers: {
4 "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
5 "Content-Type": "application/json",
6 },
7 body: JSON.stringify({
8 url: "https://your-agent.com/webhooks/slotflow",
9 events: ["booking.confirmed", "booking.cancelled"],
10 }),
11});
12
13const webhook = await res.json();
14// Save webhook.signing_secret securely — you'll need it to verify payloads
15console.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

PlanMax webhooks
Free2
Starter10
GrowthUnlimited

Payload format

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

1{
2 "event": "booking.confirmed",
3 "created_at": "2026-03-10T14:05:00.000Z",
4 "data": {
5 "id": "b9e4f2a1-3c5d-4e6f-8a9b-0c1d2e3f4a5b",
6 "human_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
7 "starts_at": "2026-03-10T14:00:00.000Z",
8 "ends_at": "2026-03-10T14:30:00.000Z",
9 "duration_minutes": 30,
10 "attendee_name": "Alex Rivera",
11 "attendee_email": "alex@startup.io",
12 "status": "confirmed",
13 "metadata": {
14 "lead_id": "lead_8294",
15 "conversation_id": "conv_1847",
16 "source": "ai_sdr"
17 }
18 }
19}

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:

AttemptTiming
1stImmediate
2nd5 minutes later
3rd60 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

1const crypto = require("crypto");
2
3function verifyWebhookSignature(rawBody, signatureHeader, signingSecret) {
4 const [tPart, vPart] = signatureHeader.split(",");
5 const timestamp = tPart.split("=")[1];
6 const signature = vPart.split("=")[1];
7
8 // Reject timestamps older than 5 minutes (replay protection)
9 const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
10 if (age > 300) {
11 throw new Error("Webhook timestamp too old");
12 }
13
14 const expected = crypto
15 .createHmac("sha256", signingSecret)
16 .update(`${timestamp}.${rawBody}`)
17 .digest("hex");
18
19 const isValid = crypto.timingSafeEqual(
20 Buffer.from(signature),
21 Buffer.from(expected)
22 );
23
24 if (!isValid) {
25 throw new Error("Invalid webhook signature");
26 }
27
28 return true;
29}

Verifying in Python

1import hmac
2import hashlib
3import time
4
5def verify_webhook_signature(raw_body: str, signature_header: str, signing_secret: str) -> bool:
6 parts = dict(p.split("=", 1) for p in signature_header.split(","))
7 timestamp = parts["t"]
8 signature = parts["v1"]
9
10 # Reject timestamps older than 5 minutes
11 if abs(time.time() - int(timestamp)) > 300:
12 raise ValueError("Webhook timestamp too old")
13
14 expected = hmac.new(
15 signing_secret.encode(),
16 f"{timestamp}.{raw_body}".encode(),
17 hashlib.sha256
18 ).hexdigest()
19
20 if not hmac.compare_digest(signature, expected):
21 raise ValueError("Invalid webhook signature")
22
23 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

1const express = require("express");
2const crypto = require("crypto");
3const app = express();
4
5// Use raw body for signature verification
6app.post("/webhooks/slotflow", express.raw({ type: "application/json" }), (req, res) => {
7 const signature = req.headers["x-slotflow-signature"];
8 const rawBody = req.body.toString();
9
10 // Verify the signature
11 try {
12 verifyWebhookSignature(rawBody, signature, process.env.SLOTFLOW_WEBHOOK_SECRET);
13 } catch (err) {
14 console.error("Webhook verification failed:", err.message);
15 return res.sendStatus(401);
16 }
17
18 const { event, data } = JSON.parse(rawBody);
19
20 switch (event) {
21 case "booking.confirmed":
22 console.log(`Booking confirmed: ${data.attendee_name} at ${data.starts_at}`);
23 handleBookingConfirmed(data);
24 break;
25
26 case "booking.cancelled":
27 console.log(`Booking cancelled: ${data.id}`);
28 handleBookingCancelled(data);
29 break;
30 }
31
32 // Always respond 200 quickly — do heavy processing asynchronously
33 res.sendStatus(200);
34});

Python (Flask) example

1from flask import Flask, request
2
3app = Flask(__name__)
4
5@app.route("/webhooks/slotflow", methods=["POST"])
6def slotflow_webhook():
7 signature = request.headers.get("X-Slotflow-Signature")
8 raw_body = request.get_data(as_text=True)
9
10 # Verify the signature
11 try:
12 verify_webhook_signature(raw_body, signature, os.environ["SLOTFLOW_WEBHOOK_SECRET"])
13 except ValueError as e:
14 return str(e), 401
15
16 payload = request.get_json()
17 event = payload["event"]
18 data = payload["data"]
19
20 if event == "booking.confirmed":
21 handle_booking_confirmed(data)
22 elif event == "booking.cancelled":
23 handle_booking_cancelled(data)
24
25 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:

1// AI Sales Agent
2metadata: {
3 lead_id: "lead_8294",
4 conversation_id: "conv_1847",
5 source: "ai_sdr",
6 qualified_at: "2026-03-10T13:45:00Z",
7}
8
9// Customer Support
10metadata: {
11 ticket_id: "ticket_12345",
12 customer_id: "cust_6789",
13 issue_type: "billing",
14 priority: "high",
15}
16
17// Recruiting
18metadata: {
19 application_id: "app_456",
20 job_id: "job_789",
21 stage: "technical_interview",
22}

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

1app.post("/webhooks/slotflow", (req, res) => {
2 const { event, data } = req.body;
3
4 if (event === "booking.confirmed") {
5 const { source } = data.metadata;
6
7 if (source === "ai_sdr") {
8 crm.updateLead(data.metadata.lead_id, { status: "demo_booked" });
9 } else if (data.metadata.ticket_id) {
10 supportSystem.updateTicket(data.metadata.ticket_id, { status: "callback_scheduled" });
11 }
12 }
13
14 res.sendStatus(200);
15});

Managing webhooks

List webhooks

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

Delete a webhook

$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.