Skip to content
iMessage APIs
Operations6 min read

Connecting iMessage to Calendly

If your bookings come through Calendly, this is the easiest useful integration on this site. Two webhook events, two messages, done in an afternoon.

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

EventSendWhy
invitee.createdImmediate confirmation: what, when, where, whoCalendly's own email confirmation is easy to miss; a message is not
invitee.canceledAcknowledgement plus a rebooking linkA 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.

typescript
// 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.
Calendlyschedulingintegration