Webhook Events

VardaCal Platform API can send real-time webhook notifications to your application when events occur in your account. Set up webhooks to receive instant updates about bookings, event types, and more.

Pro Tip: Use webhooks to keep your system synchronized with VardaCal without polling the API. This saves API calls and provides real-time updates.

Setting Up Webhooks

Create webhook endpoints using the Platform API to start receiving event notifications.

1. Create a Webhook Endpoint

Use the Platform API to register your webhook URL:

curl -X POST https://api.vardacal.com/api/v1/platform/webhooks \
  -H "X-API-Key: vca_live_sk_..." \
  -H "X-API-Secret: secret_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/vardacal",
    "events": ["booking.created", "booking.cancelled"],
    "enabled": true
  }'

Response:

{
  "id": 123,
  "url": "https://your-app.com/webhooks/vardacal",
  "events": ["booking.created", "booking.cancelled"],
  "enabled": true,
  "secret": "whsec_abc123...",
  "created_at": "2025-10-28T12:00:00Z"
}

Important: Save the secret value - you'll need it to verify webhook signatures.

2. Handle Webhook Requests

Create an endpoint in your application to receive webhook POST requests:

// Express.js webhook handler
app.post('/webhooks/vardacal', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-vardacal-signature'];
  const payload = req.body;

  // Verify signature (see Security section below)
  if (!verifySignature(payload, signature)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);

  // Handle different event types
  switch(event.type) {
    case 'booking.created':
      console.log('New booking:', event.data);
      // Add to your database, send notifications, etc.
      break;
    case 'booking.cancelled':
      console.log('Cancelled booking:', event.data);
      // Update your records
      break;
  }

  // Always respond with 200 to acknowledge receipt
  res.status(200).send('Webhook received');
});
# Flask webhook handler
@app.route('/webhooks/vardacal', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-VardaCal-Signature')
    payload = request.get_data()

    # Verify signature
    if not verify_signature(payload, signature):
        return 'Invalid signature', 401

    event = request.get_json()

    # Handle different event types
    if event['type'] == 'booking.created':
        print(f"New booking: {event['data']}")
        # Add to database, send notifications, etc.
    elif event['type'] == 'booking.cancelled':
        print(f"Cancelled booking: {event['data']}")
        # Update records

    # Always respond with 200
    return 'Webhook received', 200
# Rails webhook handler
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def vardacal
    signature = request.headers['X-VardaCal-Signature']
    payload = request.raw_post

    # Verify signature
    unless verify_signature(payload, signature)
      return head :unauthorized
    end

    event = JSON.parse(payload)

    # Handle different event types
    case event['type']
    when 'booking.created'
      Rails.logger.info "New booking: #{event['data']}"
      # Add to database, send notifications, etc.
    when 'booking.cancelled'
      Rails.logger.info "Cancelled booking: #{event['data']}"
      # Update records
    end

    # Always respond with 200
    head :ok
  end
end

Available Events

Booking Events

  • booking.created Triggered when a new booking is created
  • booking.cancelled Triggered when a booking is cancelled
  • booking.rescheduled Triggered when a booking is rescheduled
  • booking.confirmed Triggered when a booking is confirmed by the host
  • booking.reminder.sent Triggered when a reminder is sent for a booking

Event Type Events

  • event_type.created Triggered when a new event type is created
  • event_type.updated Triggered when an event type is updated
  • event_type.deleted Triggered when an event type is deleted

Payload Examples

Here's what webhook payloads look like for different event types:

booking.created

{
  "id": "evt_abc123",
  "type": "booking.created",
  "created_at": "2025-10-28T14:30:00Z",
  "data": {
    "id": 456,
    "event_type_id": 123,
    "start_time": "2025-11-01T10:00:00Z",
    "end_time": "2025-11-01T10:30:00Z",
    "status": "confirmed",
    "attendee": {
      "name": "John Doe",
      "email": "john@example.com",
      "phone": "+1234567890"
    },
    "meeting_url": "https://meet.vardacal.com/abc123",
    "created_at": "2025-10-28T14:30:00Z"
  }
}

booking.cancelled

{
  "id": "evt_def456",
  "type": "booking.cancelled",
  "created_at": "2025-10-28T15:00:00Z",
  "data": {
    "id": 456,
    "event_type_id": 123,
    "start_time": "2025-11-01T10:00:00Z",
    "status": "cancelled",
    "cancellation_reason": "Attendee requested cancellation",
    "cancelled_at": "2025-10-28T15:00:00Z"
  }
}

Webhook Security

Every webhook request includes an HMAC signature in the X-VardaCal-Signature header. Always verify this signature to ensure the webhook came from VardaCal and hasn't been tampered with.

Security Warning: Never process webhooks without verifying the signature. This prevents attackers from sending fake webhook requests to your endpoint.

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  // Compute expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Usage in Express handler
app.post('/webhooks/vardacal', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-vardacal-signature'];
  const secret = process.env.WEBHOOK_SECRET; // From webhook creation response

  if (!verifyWebhookSignature(req.body.toString(), signature, secret)) {
    return res.status(401).send('Invalid signature');
  }

  // Signature is valid, process the webhook
  const event = JSON.parse(req.body);
  // ... handle event
});
import hmac
import hashlib

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    """Verify webhook signature using HMAC-SHA256"""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    # Use constant-time comparison to prevent timing attacks
    return hmac.compare_digest(signature, expected_signature)

# Usage in Flask handler
@app.route('/webhooks/vardacal', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-VardaCal-Signature')
    secret = os.environ.get('WEBHOOK_SECRET')
    payload = request.get_data()

    if not verify_webhook_signature(payload, signature, secret):
        return 'Invalid signature', 401

    # Signature is valid, process the webhook
    event = request.get_json()
    # ... handle event
require 'openssl'

def verify_webhook_signature(payload, signature, secret)
  # Compute expected signature
  expected_signature = OpenSSL::HMAC.hexdigest(
    OpenSSL::Digest.new('sha256'),
    secret,
    payload
  )

  # Use constant-time comparison to prevent timing attacks
  ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
end

# Usage in Rails controller
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def vardacal
    signature = request.headers['X-VardaCal-Signature']
    secret = ENV['WEBHOOK_SECRET']
    payload = request.raw_post

    unless verify_webhook_signature(payload, signature, secret)
      return head :unauthorized
    end

    # Signature is valid, process the webhook
    event = JSON.parse(payload)
    # ... handle event
  end
end

Delivery & Retries

Automatic Retries

If your endpoint doesn't respond with a 2xx status code, VardaCal will automatically retry the webhook with exponential backoff:

  • Attempt 1: Immediate
  • Attempt 2: After 1 minute
  • Attempt 3: After 5 minutes
  • Attempt 4: After 15 minutes
  • Attempt 5: After 1 hour

Best Practice: Respond with a 200 status code as quickly as possible. Process the webhook data asynchronously using a background job queue to avoid timeouts.

Testing Webhooks

Test your webhook endpoint without waiting for real events:

curl -X POST https://api.vardacal.com/api/v1/platform/webhooks/123/test \
  -H "X-API-Key: vca_live_sk_..." \
  -H "X-API-Secret: secret_..."

This sends a test webhook.test event to your endpoint.