Webhooks allow your application to receive real-time notifications when events occur in Bookwell, such as new bookings or cancellations.
Overview
When an event occurs, Bookwell sends an HTTP POST request to your configured endpoint with event data. Your server processes the event and responds.
Bookwell Event → Your Endpoint → Your Application
Setting Up Webhooks
Create a Webhook Endpoint
Access Webhook Settings
Go to Settings > Webhooks in your admin dashboard.
Add Endpoint
Click Add Endpoint and provide:
- URL - Your endpoint URL (must be HTTPS)
- Events - Which events to receive
- Description - Optional description
Get Signing Secret
Copy the signing secret for verifying webhook signatures.
Endpoint Requirements
Your endpoint must:
- Use HTTPS
- Accept POST requests
- Respond within 30 seconds
- Return 2xx status code
Event Types
Appointment Events
| Event | Description |
|---|---|
appointment.created | New appointment booked |
appointment.updated | Appointment modified |
appointment.cancelled | Appointment cancelled |
appointment.completed | Service completed |
appointment.no_show | Customer didn't arrive |
Customer Events
| Event | Description |
|---|---|
customer.created | New customer registered |
customer.updated | Customer info changed |
Payment Events
| Event | Description |
|---|---|
payment.completed | Payment successful |
payment.failed | Payment failed |
payment.refunded | Refund processed |
Gift Card Events
| Event | Description |
|---|---|
gift_card.purchased | Gift card sold |
gift_card.redeemed | Gift card used |
Webhook Payload
Event Structure
All webhooks follow this structure:
{
"id": "evt_abc123",
"type": "appointment.created",
"created_at": "2025-01-15T10:30:00Z",
"data": {
"object": {
"id": "apt_xyz789",
"service_name": "60-Minute Massage",
"customer_email": "jane@example.com",
"start_time": "2025-01-20T14:00:00Z"
}
}
}Example: Appointment Created
{
"id": "evt_abc123",
"type": "appointment.created",
"created_at": "2025-01-15T10:30:00Z",
"data": {
"object": {
"id": "apt_xyz789",
"service_id": "srv_def456",
"service_name": "60-Minute Massage",
"customer_id": "cus_ghi012",
"customer_name": "Jane Smith",
"customer_email": "jane@example.com",
"therapist_id": "thr_jkl345",
"therapist_name": "Sarah Johnson",
"start_time": "2025-01-20T14:00:00Z",
"end_time": "2025-01-20T15:00:00Z",
"status": "confirmed",
"price": 9500,
"currency": "usd"
}
}
}Example: Appointment Cancelled
{
"id": "evt_def456",
"type": "appointment.cancelled",
"created_at": "2025-01-18T09:00:00Z",
"data": {
"object": {
"id": "apt_xyz789",
"status": "cancelled",
"cancellation_reason": "Customer request",
"refund_amount": 9500,
"cancelled_at": "2025-01-18T09:00:00Z"
},
"previous_attributes": {
"status": "confirmed"
}
}
}Verifying Webhooks
Important
Always verify webhook signatures to ensure requests come from Bookwell.
Signature Verification
Each webhook includes a signature in the X-Bookwell-Signature header:
X-Bookwell-Signature: t=1704067200,v1=abc123...
Verification Steps
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const [timestamp, hash] = signature.split(',');
const ts = timestamp.split('=')[1];
const sig = hash.split('=')[1];
const signedPayload = `${ts}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(expected)
);
}Timestamp Validation
Reject events older than 5 minutes to prevent replay attacks:
const eventTime = parseInt(timestamp);
const currentTime = Math.floor(Date.now() / 1000);
const tolerance = 300; // 5 minutes
if (currentTime - eventTime > tolerance) {
throw new Error('Webhook too old');
}Handling Webhooks
Best Practices
- Return quickly - Return 200 immediately, process async
- Be idempotent - Handle duplicate events gracefully
- Verify signatures - Always validate webhook authenticity
- Log events - Keep records for debugging
Example Handler
app.post('/webhooks/bookwell', async (req, res) => {
const signature = req.headers['x-bookwell-signature'];
const payload = req.rawBody;
// Verify signature
if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(400).send('Invalid signature');
}
// Acknowledge receipt immediately
res.status(200).send('Received');
// Process event asynchronously
const event = JSON.parse(payload);
switch (event.type) {
case 'appointment.created':
await handleNewAppointment(event.data.object);
break;
case 'appointment.cancelled':
await handleCancellation(event.data.object);
break;
// ... handle other events
}
});Retry Policy
If your endpoint fails, Bookwell retries:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
| 7 | 24 hours |
After 7 failures, the webhook is disabled. Re-enable in settings.
Testing Webhooks
Webhook Logs
View recent webhook deliveries in Settings > Webhooks > Logs:
- Request payload
- Response status
- Response body
- Retry history
Test Events
Send test events to verify your endpoint:
- Go to webhook settings
- Click Send Test Event
- Select event type
- View the response
Local Development
For local testing, use a tunnel service:
# Using ngrok
ngrok http 3000Use the tunnel URL as your webhook endpoint during development.
Managing Webhooks
Listing Webhooks
Via API:
/v1/webhooksList webhook endpoints
curl https://api.bookwell.app/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY"Disabling Webhooks
Temporarily disable without deleting:
- Go to webhook settings
- Toggle the webhook off
- No events will be sent
Deleting Webhooks
Remove a webhook endpoint:
- Go to webhook settings
- Click delete on the endpoint
- Confirm deletion
Troubleshooting
Not Receiving Webhooks
Check:
- Endpoint URL is correct and accessible
- HTTPS certificate is valid
- Firewall allows Bookwell IPs
- Events are selected
- Webhook is enabled
Invalid Signature Errors
Verify:
- Using correct signing secret
- Raw body (not parsed JSON)
- Signature header is complete
Timeouts
If processing takes too long:
- Return 200 immediately
- Process event asynchronously
- Use a job queue for heavy work