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 │ │
│◀────────────────────│ │
- Voice AI sends request to DialStack
- DialStack relays to your platform's webhook URL
- Your platform processes and responds
- 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 signedv1: HMAC-SHA256 signature (hex-encoded)
Verification Algorithm
The signature is computed as:
signature = HMAC-SHA256(webhook_secret, timestamp + "." + request_body)
Implementation Examples
- Node.js
- Python
- Go
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...
}
);
import hmac
import hashlib
import time
def verify_signature(payload: bytes, signature_header: str, secret: str) -> bool:
# Parse the signature header
parts = signature_header.split(',')
timestamp = parts[0].replace('t=', '')
signature = parts[1].replace('v1=', '')
# Check timestamp (reject if older than 5 minutes)
now = int(time.time())
if now - int(timestamp) > 300:
raise ValueError('Signature timestamp too old')
# Compute expected signature
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
expected = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Compare signatures (timing-safe)
if not hmac.compare_digest(signature, expected):
raise ValueError('Invalid signature')
return True
# Flask example
@app.route('/dialstack/availability/search', methods=['POST'])
def availability_search():
try:
verify_signature(
request.data,
request.headers.get('X-DialStack-Signature'),
os.environ['DIALSTACK_WEBHOOK_SECRET']
)
except ValueError as e:
return jsonify({'error': {'code': 'invalid_signature', 'message': str(e)}}), 401
# Process the request...
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"strconv"
"strings"
"time"
)
func verifySignature(payload []byte, signatureHeader, secret string) error {
// Parse the signature header
parts := strings.Split(signatureHeader, ",")
timestamp := strings.TrimPrefix(parts[0], "t=")
signature := strings.TrimPrefix(parts[1], "v1=")
// Check timestamp (reject if older than 5 minutes)
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Now().Unix()-ts > 300 {
return errors.New("signature timestamp too old")
}
// Compute expected signature
signedPayload := timestamp + "." + string(payload)
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(signedPayload))
expected := hex.EncodeToString(h.Sum(nil))
// Compare signatures (timing-safe)
if !hmac.Equal([]byte(signature), []byte(expected)) {
return errors.New("invalid signature")
}
return nil
}
Error Handling
Return appropriate HTTP status codes and error responses:
| Status | When to Use |
|---|---|
| 200 | Request processed successfully |
| 400 | Invalid request format |
| 409 | Slot unavailable (for bookings) |
| 500 | Internal 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:
- Store the idempotency key when processing a booking
- If the same key is received again, return the original booking response
- 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}"