Webhook Integration

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.

Register a webhook endpoint

  1. Log in to your yAppointment admin panel
  2. Go to Integrations > Webhooks
  3. Click Add Webhook
  4. Enter your endpoint URL (e.g., https://myapp.com/webhooks/yapp)
  5. Select the events you want to receive:
  6. Click Save
  7. You'll receive a Webhook Secret — keep it safe

Event payload structure

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

Signature verification

Every webhook includes an X-Webhook-Signature header containing an HMAC-SHA256 signature. Verify this signature to ensure the webhook came from yAppointment.

Node.js / Express implementation

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)

Python / Flask implementation

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)

Retry schedule

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

Dead-letter handling

Failed webhooks are stored for manual inspection:

  1. Log in to your admin panel
  2. Go to Integrations > Webhooks > Failed Deliveries
  3. View the error, response status, and retries
  4. Click Retry to send again

Webhooks remain in the dead-letter queue for 30 days, then are automatically deleted.

Testing with the "Send test event" button

From your webhook configuration page:

  1. Click Send Test Event
  2. Select an event type (e.g., booking.created)
  3. Click Send

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.

Debugging failed deliveries

If a webhook fails, check these things in order:

  1. Endpoint reachability: Can yAppointment's servers reach your domain? Check your firewall, DNS, and HTTPS certificate.

  2. Status code: Your endpoint must return 2xx (200, 201, 202, etc.). Redirects (3xx) and client errors (4xx) are treated as failures.

  3. Response time: If your endpoint takes >30 seconds, yAppointment times out and retries. Always respond quickly and handle the logic asynchronously.

  4. Signature mismatch: Verify you're using the correct webhook secret. Copy it from your admin panel again if you're unsure.

  5. 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 })
})

Event ordering guarantees

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.