Calendly does one thing and exposes it cleanly. Two webhook events — an invitee booking, and an invitee cancelling — cover the large majority of what a business needs to send around an appointment.
The two events
| Event | Send | Why |
|---|---|---|
| invitee.created | Immediate confirmation: what, when, where, who | Calendly's own email confirmation is easy to miss; a message is not |
| invitee.canceled | Acknowledgement plus a rebooking link | A cancellation is the highest-intent moment to rebook, and almost nobody uses it |
The second row is the underused one. Someone who just cancelled has demonstrated they still want the thing — they just cannot make that slot. A short message with a link back to your booking page recovers a meaningful share of them.
What Calendly does not give you
There is no "day before" webhook
Calendly tells you when a booking is made or cancelled, not when a booking is imminent. The reminder — the single highest-value message around any appointment — has to come from your own scheduled job reading the appointment time. That is a cron and a query, not a webhook.
// Runs hourly. Sends a reminder for appointments 14-15 hours out, so an
// evening-before message lands at a civil time regardless of booking time.
export async function sendReminders() {
const due = await db.appointments.findMany({
where: {
startsAt: { gte: hoursFromNow(14), lt: hoursFromNow(15) },
reminderSentAt: null,
canceledAt: null,
},
});
for (const appointment of due) {
if (await isSuppressed(appointment.phone)) continue;
if (!withinQuietHours(appointment.timezone)) continue;
await sendMessage({
to: appointment.phone,
body: reminderText(appointment),
idempotencyKey: `${appointment.id}:reminder`,
});
await db.appointments.update({
where: { id: appointment.id },
data: { reminderSentAt: new Date() },
});
}
}The reminder job. Runs hourly, sends for appointments in a target window, and records what it sent so a re-run cannot double-send.
The reminderSentAt column is what makes the job safe to re-run. Without it, a retry or an overlapping run sends the same reminder twice, which is the most common self-inflicted wound in appointment messaging.
Getting the phone number at all
Calendly will not have a phone number unless you ask for one. Add a required phone question to the booking form — and, in the same place, the consent language, so the permission and the number arrive together. That single form change is what makes the whole integration legal as well as possible. What consent has to look like.
The afternoon build
- Phone number as a required booking question, with consent language beside it.
- Webhook endpoint verifying Calendly's signature over the raw body.
- Confirmation on invitee.created.
- Rebooking nudge on invitee.canceled.
- Hourly reminder job with a sent-marker column.
- Quiet hours in the invitee's timezone — Calendly gives you it, so use it.