Webhooks are HTTP POST requests that yAppointment sends to your backend in real time when events occur — booking confirmations, cancellations, customer updates, and more. This guide shows how to securely receive, verify, and process webhooks.
https://myapp.com/webhooks/yapp)booking.created — customer booked an appointmentbooking.cancelled — booking was cancelledbooking.rescheduled — customer rescheduledcustomer.created — new customer profile createdcustomer.updated — customer info changedEvery webhook is a POST request with this structure:
{
"id": "evt_1234567890",
"timestamp": "2025-04-19T14:30:00Z",
"event": "booking.created",
"data": {
"id": "bkg_987654321",
"customerId": "cust_123",
"serviceId": "srv_456",
"customerName": "John Doe",
"customerEmail": "john@example.com",
"serviceName": "Hair Consultation",
"start": "2025-04-20T10:00:00Z",
"end": "2025-04-20T11:00:00Z",
"timezone": "America/New_York",
"status": "confirmed"
}
}
All timestamps are UTC (ISO 8601 format). Event IDs are unique per webhook delivery and can be used for deduplication.
Every webhook includes an X-Webhook-Signature header containing an HMAC-SHA256
signature. Verify this signature to ensure the webhook came from yAppointment.
import crypto from 'crypto'
import express from 'express'
const app = express()
const WEBHOOK_SECRET = process.env.YAPP_WEBHOOK_SECRET // Store in env vars!
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const computed = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computed)
)
}
app.post('/webhooks/yapp', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'] as string
const payload = req.body.toString()
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' })
}
const event = JSON.parse(payload)
// Process the webhook
console.log(`Received ${event.event} webhook:`, event)
// Always respond with 200 OK immediately, then process asynchronously
res.status(200).json({ received: true })
// Handle the event in the background
handleWebhookAsync(event).catch(err => console.error('Webhook error:', err))
})
async function handleWebhookAsync(event: any) {
switch (event.event) {
case 'booking.created':
// Update your database, send confirmation email, notify customer
await db.bookings.insert({
id: event.data.id,
customerId: event.data.customerId,
confirmedAt: new Date()
})
break
case 'booking.cancelled':
// Update your database, notify customer
await db.bookings.update(
{ id: event.data.id },
{ status: 'cancelled', cancelledAt: new Date() }
)
break
case 'customer.updated':
// Sync customer data
await db.customers.update(
{ id: event.data.customerId },
{ name: event.data.customerName, email: event.data.customerEmail }
)
break
}
}
app.listen(3000)
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['YAPP_WEBHOOK_SECRET']
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
computed = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, computed)
@app.route('/webhooks/yapp', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
payload = request.get_data()
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
event = json.loads(payload)
# Respond immediately
response = jsonify({'received': True})
# Handle asynchronously in a background worker
handle_event_async.delay(event)
return response, 200
def handle_event_async(event):
if event['event'] == 'booking.created':
db.bookings.insert({
'id': event['data']['id'],
'customerId': event['data']['customerId'],
'confirmedAt': datetime.now()
})
elif event['event'] == 'booking.cancelled':
db.bookings.update(
{'id': event['data']['id']},
{'status': 'cancelled', 'cancelledAt': datetime.now()}
)
if __name__ == '__main__':
app.run(port=5000)
If your endpoint returns a non-2xx status or times out, yAppointment retries:
After the 5th failure, the webhook is marked as failed and moved to a dead-letter queue.
Best practice: Always respond with 200 OK within 30 seconds, even if you haven't finished processing. Handle the actual business logic asynchronously in a background job queue (Redis, Bull, Celery, etc.).
Failed webhooks are stored for manual inspection:
Webhooks remain in the dead-letter queue for 30 days, then are automatically deleted.
From your webhook configuration page:
booking.created)A test webhook will be posted to your endpoint with sample data. This is identical to production webhooks except the data is fake. Use it to verify your endpoint is reachable and your signature verification works.
If a webhook fails, check these things in order:
Endpoint reachability: Can yAppointment's servers reach your domain? Check your firewall, DNS, and HTTPS certificate.
Status code: Your endpoint must return 2xx (200, 201, 202, etc.). Redirects (3xx) and client errors (4xx) are treated as failures.
Response time: If your endpoint takes >30 seconds, yAppointment times out and retries. Always respond quickly and handle the logic asynchronously.
Signature mismatch: Verify you're using the correct webhook secret. Copy it from your admin panel again if you're unsure.
Logs: Enable verbose logging in your webhook handler to see the exact request and response yAppointment received.
app.post('/webhooks/yapp', (req, res) => {
const signature = req.headers['x-webhook-signature'] as string
const payload = JSON.stringify(req.body)
console.log('[Webhook] Received:', {
event: req.body.event,
signature: signature?.substring(0, 20) + '...',
payloadSize: payload.length
})
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
console.log('[Webhook] Signature verification failed')
return res.status(401).json({ error: 'Invalid signature' })
}
console.log('[Webhook] Processing event:', req.body.event)
res.status(200).json({ received: true })
})
Webhooks are delivered in roughly the order they occur, but strict ordering is not
guaranteed. Use the id field for deduplication and the timestamp field to order
events if you need exact sequencing.
async function handleWebhookAsync(event: any) {
// Prevent duplicate processing
const exists = await db.webhookLogs.findOne({ eventId: event.id })
if (exists) {
console.log(`Already processed event ${event.id}`)
return
}
// Process the event
await processEvent(event)
// Record it
await db.webhookLogs.insert({
eventId: event.id,
event: event.event,
processedAt: new Date()
})
}
This ensures that if a webhook is delivered twice (due to our retry logic), you only process it once.