Carepatron

Build spec · Customer.io

Calendly to Customer.io build spec

How appointment reminders and cancellations flow from Calendly into Customer.io, with the real webhook payloads to build against. This is the system of record.

System of record Verified live 2 June 2026 Workspace 199348

What it does

Every Calendly booking across the org creates a throwaway meeting profile in Customer.io. That profile owns its own branded emails (a confirmation, a 24 hour reminder, a 1 hour reminder, and a cancellation acknowledgement) and its own cancellation and reschedule state. It runs as a direct webhook with no middleware.

Live components

ComponentIDWhat it is
Calendly webhook subscription52e14b78…Fires invitee.created and invitee.canceled to the receiver
Receiver campaignCampaign 69Webhook triggered. Builds the meeting profile and fires the normalised events
Reminder sequenceCampaign 71Triggered by calendly_call_booked, filtered to Segment 139
Cancellation handlerCampaign 70Triggered by calendly_call_canceled, filtered to Segment 140
Segment "Meeting — Scheduled"139Meeting profiles in scheduled state. Anchors Campaign 71
Segment "Meeting — Hard Canceled"140Meeting profiles in canceled state. Anchors Campaign 70

How it flows end to end

  1. When someone books

    On invitee.created, the receiver creates the meeting profile (its Customer.io id is the Calendly invitee URI) and fires calendly_call_booked. The profile enters Segment 139, so Campaign 71 runs on it:

    • Confirmation email straight away.
    • If the meeting is more than 24 hours out, a day before reminder.
    • If the meeting is more than 1 hour out, a 1 hour reminder.
    • One hour after the start time, the meeting is marked completed.
  2. When someone cancels

    • If new_invitee is present, this is the cancel half of a reschedule. The profile is marked rescheduled and nothing is sent. The new booking builds a fresh profile and a fresh sequence.
    • If not, it is a hard cancel. The profile is marked canceled, calendly_call_canceled fires, and Campaign 70 sends the cancellation acknowledgement from the meeting host.
  3. How reschedules are detected

    On the new booking Calendly sends rescheduled: false, so the reliable signal is the old_invitee and new_invitee pointers, not the rescheduled flag.

Real Calendly webhook payloads

These are real captured Calendly v2 webhook bodies. Personal values (names, emails, ids) have been replaced with dummy data. Every field name, nesting, value format (ISO 8601 timestamps in UTC with microseconds, URI shapes, UUIDs, and nulls) is exactly as Calendly sends it. Build against these.

Two structural notes worth catching early: the top level event is the discriminator, while payload.event is a URI to the scheduled event. The full scheduled event is also expanded inline at payload.scheduled_event, so the receiver never has to make a second call.

1 · invitee.created (a new booking)

{
  "event": "invitee.created",
  "payload": {
    "cancel_url": "https://calendly.com/cancellations/c4e1d8a6-2b73-4f19-9a0c-6d8e2f4b1c33",
    "created_at": "2026-06-02T14:22:27.922032Z",
    "email": "[email protected]",
    "event": "https://api.calendly.com/scheduled_events/7f3a9c20-1e84-4d6b-b9a2-3c5e0f1a7b22",
    "first_name": null,
    "invitee_scheduled_by": null,
    "last_name": null,
    "name": "Jordan Lee",
    "new_invitee": null,
    "no_show": null,
    "old_invitee": null,
    "payment": null,
    "questions_and_answers": [],
    "reconfirmation": null,
    "reschedule_url": "https://calendly.com/reschedulings/c4e1d8a6-2b73-4f19-9a0c-6d8e2f4b1c33",
    "rescheduled": false,
    "routing_form_submission": null,
    "scheduled_event": {
      "calendar_event": {
        "external_id": "m7k2p9q4r8s1t6u3v0w5x2y8za",
        "kind": "google"
      },
      "created_at": "2026-06-02T14:22:27.898497Z",
      "end_time": "2026-06-10T19:45:00.000000Z",
      "event_guests": [],
      "event_memberships": [
        {
          "buffered_end_time": "2026-06-10T20:00:00.000000Z",
          "buffered_start_time": "2026-06-10T19:00:00.000000Z",
          "user": "https://api.calendly.com/users/9c2e7b14-8a36-4d51-b2e9-5f1a0c8d3e88",
          "user_email": "[email protected]",
          "user_name": "Taylor Brooks"
        }
      ],
      "event_type": "https://api.calendly.com/event_types/f1d3b5a7-2c89-4e64-9a1b-7d0e3c5f2a99",
      "invitees_counter": { "active": 1, "limit": 1, "total": 1 },
      "location": {
        "join_url": "https://calendly.com/events/7f3a9c20-1e84-4d6b-b9a2-3c5e0f1a7b22/google_meet",
        "status": "pushed",
        "type": "google_conference"
      },
      "meeting_notes_html": null,
      "meeting_notes_plain": null,
      "name": "Sales Demonstration",
      "start_time": "2026-06-10T19:00:00.000000Z",
      "status": "active",
      "updated_at": "2026-06-02T14:22:30.226091Z",
      "uri": "https://api.calendly.com/scheduled_events/7f3a9c20-1e84-4d6b-b9a2-3c5e0f1a7b22"
    },
    "scheduling_method": null,
    "status": "active",
    "text_reminder_number": null,
    "timezone": "America/New_York",
    "tracking": {
      "salesforce_uuid": null,
      "utm_campaign": null,
      "utm_content": null,
      "utm_medium": null,
      "utm_source": null,
      "utm_term": null
    },
    "updated_at": "2026-06-02T14:22:27.922032Z",
    "uri": "https://api.calendly.com/scheduled_events/7f3a9c20-1e84-4d6b-b9a2-3c5e0f1a7b22/invitees/c4e1d8a6-2b73-4f19-9a0c-6d8e2f4b1c33"
  }
}

2 · invitee.canceled (a hard cancel)

Same shape as the booking above, with a cancellation object added and the status fields flipped to canceled. rescheduled stays false.

{
  "event": "invitee.canceled",
  "payload": {
    "cancel_url": "https://calendly.com/cancellations/c4e1d8a6-2b73-4f19-9a0c-6d8e2f4b1c33",
    "cancellation": {
      "canceled_by": "Taylor Brooks",
      "canceler_type": "host",
      "created_at": "2026-06-02T14:28:43.443288Z",
      "reason": "Something came up\n"
    },
    "created_at": "2026-06-02T14:22:27.922032Z",
    "email": "[email protected]",
    "event": "https://api.calendly.com/scheduled_events/7f3a9c20-1e84-4d6b-b9a2-3c5e0f1a7b22",
    "first_name": null,
    "invitee_scheduled_by": null,
    "last_name": null,
    "name": "Jordan Lee",
    "new_invitee": null,
    "no_show": null,
    "old_invitee": null,
    "payment": null,
    "questions_and_answers": [],
    "reconfirmation": null,
    "reschedule_url": "https://calendly.com/reschedulings/c4e1d8a6-2b73-4f19-9a0c-6d8e2f4b1c33",
    "rescheduled": false,
    "routing_form_submission": null,
    "scheduled_event": {
      "...": "identical to invitee.created above, with status now \"canceled\"",
      "name": "Sales Demonstration",
      "start_time": "2026-06-10T19:00:00.000000Z",
      "status": "canceled",
      "uri": "https://api.calendly.com/scheduled_events/7f3a9c20-1e84-4d6b-b9a2-3c5e0f1a7b22"
    },
    "scheduling_method": null,
    "status": "canceled",
    "text_reminder_number": null,
    "timezone": "America/New_York",
    "tracking": { "salesforce_uuid": null, "utm_campaign": null, "utm_content": null, "utm_medium": null, "utm_source": null, "utm_term": null },
    "updated_at": "2026-06-02T14:28:43.446299Z",
    "uri": "https://api.calendly.com/scheduled_events/7f3a9c20-1e84-4d6b-b9a2-3c5e0f1a7b22/invitees/c4e1d8a6-2b73-4f19-9a0c-6d8e2f4b1c33"
  }
}

3 · invitee.canceled (the cancel half of a reschedule)

Same body as the hard cancel above, except for the fields below. The discriminator is rescheduled: true with new_invitee pointing at the replacement booking. The cancellation object is still present here, so do not use its presence to tell a reschedule apart from a hard cancel.

{
  "event": "invitee.canceled",
  "payload": {
    "rescheduled": true,
    "status": "canceled",
    "new_invitee": "https://api.calendly.com/scheduled_events/e6f2a1b8-3c47-4d2a-9b85-0a7c1e3f5d66/invitees/4a8d2c91-6e15-4b73-8f0a-2c9d5e1b7a77",
    "old_invitee": null,
    "cancellation": {
      "canceled_by": "Jordan Lee",
      "canceler_type": "invitee",
      "created_at": "2026-06-02T14:33:53.302916Z",
      "reason": "Something came up\n"
    },
    "uri": "https://api.calendly.com/scheduled_events/2d9b4e71-5c3a-4a8e-b1f7-9e0c3a5d2b44/invitees/8b1c6f30-7d24-4e95-a3c2-1f6b8d0e4a55"
  }
}

4 · invitee.created (the new half of a reschedule)

Same body as a normal booking, except for the fields below. Note rescheduled is false here. The signal is old_invitee being set. It cross references the cancel half above: this old_invitee equals the cancel half's uri, and the cancel half's new_invitee equals this uri.

{
  "event": "invitee.created",
  "payload": {
    "rescheduled": false,
    "status": "active",
    "new_invitee": null,
    "old_invitee": "https://api.calendly.com/scheduled_events/2d9b4e71-5c3a-4a8e-b1f7-9e0c3a5d2b44/invitees/8b1c6f30-7d24-4e95-a3c2-1f6b8d0e4a55",
    "uri": "https://api.calendly.com/scheduled_events/e6f2a1b8-3c47-4d2a-9b85-0a7c1e3f5d66/invitees/4a8d2c91-6e15-4b73-8f0a-2c9d5e1b7a77"
  }
}

Field reference

The fields the receiver reads, with exact names and formats. The scheduler must populate these.

Field pathFormat and exampleNotes
eventString: invitee.created or invitee.canceledTop level. The router branches on this.
payload.uri…/scheduled_events/{uuid}/invitees/{uuid}Unique and stable per booking. Becomes the meeting profile id. The same value must return on that booking's cancel or reschedule.
payload.email[email protected]Used to match the person who gets messaged.
payload.nameJordan LeeOne field. first_name and last_name are always null.
payload.timezoneAmerica/New_YorkIANA timezone string.
payload.text_reminder_numberE.164 string or nullMaps to the profile phone.
payload.rescheduledBooleanTrue only on the cancel half of a reschedule. False on the new booking.
payload.new_invitee / payload.old_inviteeInvitee URI or nullThe real reschedule signal. new_invitee set on the cancel half, old_invitee set on the new half.
payload.reschedule_url / payload.cancel_urlhttps://calendly.com/reschedulings/{uuid}Used in email CTAs.
payload.cancellation.reasonString, may end with \nAlso canceled_by and canceler_type (host or invitee).
payload.scheduled_event.start_time / end_time2026-06-10T19:00:00.000000ZISO 8601, UTC, microsecond precision. Converted to unix for the reminder timers.
payload.scheduled_event.nameSales DemonstrationThe meeting type name shown in emails.
payload.scheduled_event.event_type…/event_types/{uuid}Stable id per meeting type. Use this if a demo filter is added later.
…event_memberships[0].user_name / user_emailTaylor Brooks · [email protected]The host. Emails send from this address.
…location.join_url / typehttps://calendly.com/events/{uuid}/google_meettype example google_conference. location.location holds in person addresses.
payload.questions_and_answersArray, empty [] when noneWhen intake questions exist, each item is { question, answer, position }.
payload.tracking.*All null in the sampleutm_source/medium/campaign/content/term and salesforce_uuid. Carry a real Carepatron id here, or as a first class field.

The events the receiver fires

The receiver normalises every booking into two Customer.io events on the matching profiles.

calendly_call_booked

meeting_type_name · meeting_start_time (unix) · meeting_host_name · meeting_id

calendly_call_canceled

meeting_type_name · meeting_id · cancellation_reason

Richer personalisation in the emails comes from the meeting profile attributes below, not from these event fields.

Meeting profile attributes

Set on the throwaway profile when the booking comes in. Its id is the Calendly invitee URI.

profile_type email name timezone phone meeting_id meeting_start_time meeting_end_time meeting_type_name meeting_host_name meeting_host_email meeting_join_url meeting_location_type meeting_location meeting_reschedule_url meeting_cancel_url meeting_status created_at

meeting_status moves through scheduled, then rescheduled, canceled, or completed.

Good to know

Replacing Calendly with an internal scheduler

To reuse this pipeline with no Customer.io or template changes, send the payload shapes above to the receiver webhook URL. Minimum the receiver depends on:

Upgrades that do not touch templates

Carepatron
Verified live from the Customer.io API on 2 June 2026. Campaigns 69, 70, and 71. Segments 139 and 140. Payloads are real captured Calendly v2 webhook bodies with personal data replaced by dummy values, format preserved exactly.