Skip to main content

Webhooks

Registering a webhook

Visit the Webhooks page in the SavvyCal Dashboard and follow the prompts to register a new webhook.

For platforms, register webhooks on the platform settings page under the "Platform webhooks" section.

Listening for events

When an event occurs that triggers a webhook, we will send an HTTP POST to the URL you specified, with a JSON-encoded body of this shape:

POST /my-webhook-receiver HTTP/1.1

host: https://myapp.com
user-agent: SavvyCal Webhooks (https://savvycal.app)
x-savvycal-signature: sha256=6CAA4DEF5C3463B785E885FF19B8987B348E19399D2C5FB291274EDFA7128105
x-savvycal-webhook-id: wh_XXXXXXXXXX
content-type: application/json

{
"id": "evt_d025a96ac0c6",
"account_id": "acct_d025a96ac0c6",
"version": "1.0",
"created_at": "2025-03-12T12:34:55Z",
"data": {
"type": "appointment.created",
"object": {...}
}
}

Verifying signatures

Each webhook delivery includes an x-savvycal-signature header containing an HMAC-SHA256 signature of the raw request body. You can use this to verify that the payload was sent by SavvyCal and has not been tampered with.

To find your webhook's signing secret, open the webhook in the Webhooks page in the SavvyCal Dashboard. The signing secret is displayed in the edit panel.

To verify a signature:

  1. Read the raw request body (before any JSON parsing).
  2. Compute the HMAC-SHA256 digest of the body using your webhook's signing secret as the key.
  3. Hex-encode the digest in uppercase and prepend sha256=.
  4. Compare the result to the x-savvycal-signature header value using a constant-time comparison function.

JavaScript

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, secret, signatureHeader) {
const digest = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
.toUpperCase();

const expected = `sha256=${digest}`;

return (
expected.length === signatureHeader.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader))
);
}

Ruby

require 'openssl'

def verify_webhook_signature(raw_body, secret, signature_header)
digest = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body).upcase
expected = "sha256=#{digest}"

Rack::Utils.secure_compare(expected, signature_header)
end

Event types

The following event types are available. The Schema definitions referenced in the table below represent the shape of the data field in the webhook payload.

Event TypeDescription
account_user.createdEmitted when a new account user is created. (Schema)
account_user.deletedEmitted when an account user is removed. (Schema)
account_user.updatedEmitted when an existing account user is updated. (Schema)
appointment.createdEmitted when a new appointment is created. (Schema)
appointment.rescheduledEmitted when an existing appointment is rescheduled. (Schema)
appointment.canceledEmitted when an existing appointment is canceled. (Schema)
appointment.confirmedEmitted when an existing appointment is confirmed. (Schema)
appointment.deletedEmitted when an existing appointment is deleted. (Schema)
appointment.meeting.canceledEmitted after a meeting is canceled with the conferencing provider. (Schema)
appointment.meeting.createdEmitted after a meeting is created with the conferencing provider. (Schema)
appointment.meeting.failedEmitted when a meeting creation or update terminally fails. (Schema)
appointment.meeting.updatedEmitted after a meeting is updated with the conferencing provider. (Schema)
booking_intent.abandonedEmitted when a booking intent is abandoned. (Schema)
booking_intent.completedEmitted when a booking intent is completed. (Schema)
booking_intent.createdEmitted when a new booking intent is created. (Schema)
booking_intent.updatedEmitted when an existing booking intent is updated. (Schema)
block.createdEmitted when a new block is created. (Schema)
block.updatedEmitted when an existing block is updated. (Schema)
block.deletedEmitted when an existing block is deleted. (Schema)
client.createdEmitted when a new client is created. (Schema)
client.deletedEmitted when a client is deleted. (Schema)
client.updatedEmitted when an existing client is updated. (Schema)
connected_account.createdEmitted when a new calendar integration is connected. (Schema)
connected_account.deletedEmitted when a calendar integration is disconnected. (Schema)
connected_account.refresh_failedEmitted when an OAuth token refresh permanently fails. (Schema)
connected_account.reconnectedEmitted when a previously failed integration is reconnected. (Schema)
provider.createdEmitted when a new provider is created. (Schema)
provider.deactivatedEmitted when a provider is deactivated. (Schema)
provider.reactivatedEmitted when a previously deactivated provider is reactivated. (Schema)
provider.updatedEmitted when an existing provider is updated. (Schema)
provider_schedule.createdEmitted when a new provider schedule is created. (Schema)
provider_schedule.deletedEmitted when a provider schedule is deleted. (Schema)
provider_schedule.updatedEmitted when an existing provider schedule is updated. (Schema)
service.createdEmitted when a new service is created. (Schema)
service.deletedEmitted when a service is deleted. (Schema)
service.updatedEmitted when an existing service is updated. (Schema)
service_provider.createdEmitted when a provider is added to a service. (Schema)
service_provider.deletedEmitted when a provider is removed from a service. (Schema)

Sensitive fields

Webhook payloads err on the side of caution when it comes to personally identifiable information. Fields like first_name, last_name, email, and phone on client objects will be null in webhook payloads.

If you need access to these fields, fetch the full record via the API after receiving the webhook. For example, after receiving a client.created event, you can call GET /v1/clients/:id to retrieve the complete client data.