Skip to main content

Appointment Webhooks

Receive appointment availability searches and booking requests from DialStack.

Overview

When DialStack's voice AI searches for availability or creates a booking, your platform can receive webhook notifications to handle these requests. Webhooks are optional — if not configured, the voice AI will receive an error.

How It Works

┌─────────────┐ ┌───────────┐ ┌──────────────┐
│ DialStack │ │ DialStack │ │ Your Platform│
│ Voice AI │ │ API │ │ │
└──────┬──────┘ └─────┬─────┘ └──────┬───────┘
│ │ │
│ 1. Search/Book │ │
│────────────────────▶│ │
│ │ │
│ │ 2. Webhook POST │
│ │────────────────────▶│
│ │ │
│ │ 3. JSON Response │
│ │◀────────────────────│
│ │ │
│ 4. Pass-through │ │
│◀────────────────────│ │
  1. Voice AI sends request to DialStack
  2. DialStack relays to your platform's webhook URL
  3. Your platform processes and responds
  4. DialStack passes the response back to the voice AI

Configuration

Configure webhooks on your platform record:

  • webhook_url: Base URL for webhook endpoints (e.g., https://api.yourplatform.com/dialstack)
  • webhook_secret: Shared secret for signature verification

DialStack will append the endpoint path to your base URL:

  • {webhook_url}/availability/search
  • {webhook_url}/bookings

Webhook Endpoints

Search Availability

Receive availability search requests.

Endpoint: POST {webhook_url}/availability/search

Request Headers:

Content-Type: application/json
X-DialStack-Signature: t=1697634600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-DialStack-Account-Id: acct_01h2xcejqtf2nbrexx3vqjhp41

Request Body:

{
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"query": {
"filter": {
"start_at_range": {
"start_at": "2024-01-15T09:00:00Z",
"end_at": "2024-01-15T17:00:00Z"
}
}
}
}

Expected Response (200 OK):

{
"availabilities": [
{
"start_at": "2024-01-15T10:00:00Z",
"duration_minutes": 30
}
]
}

Create Booking

Receive booking creation requests.

Endpoint: POST {webhook_url}/bookings

Request Headers:

Content-Type: application/json
X-DialStack-Signature: t=1697634600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-DialStack-Account-Id: acct_01h2xcejqtf2nbrexx3vqjhp41

Request Body:

{
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"idempotency_key": "booking-req-123456",
"booking": {
"start_at": "2024-01-15T10:00:00Z",
"duration_minutes": 30,
"customer": {
"phone": "+15551234567",
"name": "John Doe",
"email": "john@example.com"
},
"notes": "Initial consultation - referred by AI assistant"
}
}

Expected Response (200 OK):

{
"booking": {
"id": "bkg_01h2xcejqtf2nbrexx3vqjhp41",
"status": "confirmed",
"start_at": "2024-01-15T10:00:00Z",
"end_at": "2024-01-15T10:30:00Z",
"location": {
"name": "Main Office",
"address": "123 Main St, City, ST 12345"
}
}
}

Signature Verification

All webhook requests include a signature header for verification. Always verify signatures to ensure requests are from DialStack.

Header Format

X-DialStack-Signature: t=<timestamp>,v1=<signature>
  • t: Unix timestamp (seconds) when the request was signed
  • v1: HMAC-SHA256 signature (hex-encoded)

Verification Algorithm

The signature is computed as:

signature = HMAC-SHA256(webhook_secret, timestamp + "." + request_body)

Implementation Examples

import crypto from 'crypto';

function verifySignature(payload, signatureHeader, secret) {
// Parse the signature header
const parts = signatureHeader.split(',');
const timestamp = parts[0].replace('t=', '');
const signature = parts[1].replace('v1=', '');

// Check timestamp (reject if older than 5 minutes)
const now = Math.floor(Date.now() / 1000);
if (now - parseInt(timestamp) > 300) {
throw new Error('Signature timestamp too old');
}

// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');

// Compare signatures (timing-safe)
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid signature');
}

return true;
}

// Express.js example
app.post(
'/dialstack/availability/search',
express.raw({ type: 'application/json' }),
(req, res) => {
try {
verifySignature(
req.body.toString(),
req.headers['x-dialstack-signature'],
process.env.DIALSTACK_WEBHOOK_SECRET
);
} catch (error) {
return res.status(401).json({ error: { code: 'invalid_signature', message: error.message } });
}

// Process the request...
}
);

Error Handling

Return appropriate HTTP status codes and error responses:

StatusWhen to Use
200Request processed successfully
400Invalid request format
409Slot unavailable (for bookings)
500Internal server error

Error Response Format:

{
"error": {
"code": "slot_unavailable",
"message": "The requested time slot is no longer available"
}
}

Idempotency

Booking requests include an idempotency_key field. Use this to prevent duplicate bookings:

  1. Store the idempotency key when processing a booking
  2. If the same key is received again, return the original booking response
  3. Keys can be safely expired after 24 hours

Timeouts

  • DialStack waits up to 30 seconds for your webhook response
  • If your webhook times out, the voice AI receives a 504 Gateway Timeout error
  • Design your endpoints to respond quickly; defer heavy processing if needed

Testing

Local Development

Use a tool like ngrok to expose your local server:

ngrok http 3000

Then configure your platform's webhook URL to the ngrok URL:

https://abc123.ngrok.io/dialstack

Manual Testing

Test your webhook endpoint with cURL:

# Generate a test signature
TIMESTAMP=$(date +%s)
PAYLOAD='{"account_id":"acct_test","query":{"filter":{}}}'
SECRET="your_webhook_secret"
SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "${SECRET}" | cut -d' ' -f2)

# Send test request
curl -X POST http://localhost:3000/dialstack/availability/search \
-H "Content-Type: application/json" \
-H "X-DialStack-Signature: t=${TIMESTAMP},v1=${SIGNATURE}" \
-H "X-DialStack-Account-Id: acct_test" \
-d "${PAYLOAD}"