Booking tool → Customer.io · incident writeup

A single reschedule is creating 2–3 bookings, and each one sends its own reminders

A customer got three identical SMS reminders three seconds apart before her demo. The reminder automation did exactly what it was asked to do; it was asked three times. The extra bookings come from the reschedule path on sales.carepatron.com, which emits multiple invitee.created webhooks with fresh invitee IDs for one rescheduled meeting.

Prepared 17 Sep 2026 For Chad (booking tool) From Carlos Source Customer.io workspace 199348, live API reads
In short
  1. What happened: Heather Smith rescheduled her demo from 3:00 to 3:30 pm. Three booking records arrived in Customer.io within one second. She received 12 messages for one meeting, including 3 SMS at 15:00:03, :04 and :06.
  2. Where it comes from: The three records have three different invitee URIs. Customer.io keys everything on the URI the webhook sends and cannot invent one, so three URIs means three inbound webhooks. Genuine Calendly bookings never show this; only sales.carepatron.com reschedules do.
  3. How often: 8 bookings in 17 days (Sep 1–17), every one a reschedule, each duplicated 2× or 3×. Roughly 1 in 130 bookings.
  4. Why it surfaced now: it has been duplicating emails for weeks; the T-30 SMS reminder went live ~7 Sep and made it visible.
  5. Ask: make the reschedule path emit one invitee record (or dedupe before sending), and send the cancel-half for every superseded invitee. I will change the Customer.io side to key on person + start time so we are safe either way.

1. The report

Hyden, 16 Sep: "Had a demo with a client yesterday who mentioned she received 3 back to back sms messages for the demo she had." Customer: [email protected], +18122079338.

2. What Heather actually received

Every timestamp below is read from Customer.io delivery logs. Times are US Eastern (her timezone).

3. The four records behind it

Invitee ID (end of URI)Created (UTC)Meeting startStatusNote
a94e71a3466f4aef8c0f34078a3173df11 Sep 18:50:0314 Sep 15:00 ETrescheduledOriginal booking. Plain 32-char hex ID.
7fdf5006-a539-4f27-aee7-b6effdef781511 Sep 18:52:0714 Sep 15:30 ETcompletedReschedule. Three dashed UUIDs, identical payloads, one second apart. Each ran a full reminder journey.
7a1bc7f4-9d64-4cf5-882e-6788efa6304111 Sep 18:52:0814 Sep 15:30 ETcompleted
9845346f-3970-4e33-9784-263d3e26637311 Sep 18:52:0814 Sep 15:30 ETcompleted

All four share scheduled_events/56eddcbe-2f30-435c-982e-ed76abed03e8. Note that on sales.carepatron.com that segment identifies the event type (the booking page), not the booking; it is shared across different people.

4. Why this is upstream of Customer.io

The receiving automation (Customer.io campaign 69) creates one meeting profile per webhook and sets its identifier from the payload:

recipient: {"field":"id","type":"trigger_attribute","value":"payload.uri"}

It stores payload.uri as meeting_id, then fires calendly_call_booked carrying that same meeting_id. The reminder automation (campaign 92) only starts a journey when the profile's meeting_id equals the event's meeting_id, so each journey is correctly bound to exactly one record.

Customer.io has no way to generate an invitee URI. Three distinct URIs can only come from three distinct inbound POSTs. Campaign 69 has not been edited since 9 Jul 2026, and campaign 92's last change (7 Sep) added the SMS step and did not touch entry logic.

5. The same signature in every case

I found four reschedule duplications with full profile history. All four follow the same pattern.

a94e71a3466f4aef8c0f34078a3173df7fdf5006-a539-… 7a1bc7f4-9d64-… 9845346f-3970-…  heather.smith.91075@ · 11 Sep · 3 copies in 1 s
b7eacfd47ae9491d8691f1e9358f10ae515015a0-f38a-… 12071de9-c788-… 9086c063-01af-…  [email protected] · 29 Aug · 3 copies in 1 s
bb667e174434407ea39903fb5c82ecef518d1186-2296-… 4b9cf556-674b-… 2574694f-00d7-…  [email protected] · 1 Sep · 3 copies, same second
8f26d4e1500a43cba03ce02d8e3fb195697a1c1d-7cc8-… 42a961f4-21f1-…  [email protected] · 17 Sep · 2 copies, 65 s apart
original booking: 32-char hex, no dashes, always exactly one reschedule: dashed UUIDs, 2 or 3 of them, identical start time

Two things stand out for whoever looks at the code:

Bookings that arrive with api.calendly.com URIs show no duplication over the same period.

6. How often

1,070
confirmation emails sent, 1–17 Sep
8
duplicated bookings (all reschedules)
~1 in 130
bookings affected
2 of 180
SMS recipients got an instant burst, 10–17 Sep

Method: grouped campaign-92 deliveries by recipient and template; counted groups with 2+ sends of the same confirmation inside 180 seconds; excluded pairs whose subjects showed two genuinely different meetings. The rate among reschedules is much higher than 1 in 130, but I do not have a reschedule count to divide by.

7. The second-order problem: reminders for a meeting that moved

When a customer with duplicate records reschedules again, only one record gets the invitee.canceled update. The others stay scheduled and keep sending for the old slot.

[email protected] (times UTC)

8. What I'd like checked in the booking tool

Search the send logs for these invitee IDs and see how many outbound webhook POSTs each reschedule produced, and what triggered each one:

11 Sep 18:52 UTC · 7fdf5006-a539-4f27-aee7-b6effdef7815 · 7a1bc7f4-9d64-4cf5-882e-6788efa63041 · 9845346f-3970-4e33-9784-263d3e266373
29 Aug 07:02 UTC · 515015a0-f38a-4074-a676-0dd1d04bd356 · 12071de9-c788-4e38-85c6-ce79cd7786dd · 9086c063-01af-48bc-a130-1261397e3031
01 Sep 06:01 UTC · 518d1186-2296-483d-802f-8d0b66c9f3df · 4b9cf556-674b-4014-b4cd-66055641b8b9 · 2574694f-00d7-4533-9b3e-ffc228de54b5
17 Sep 12:52 UTC · 697a1c1d-7cc8-45f6-9607-89b318cad7c7 · 42a961f4-21f1-4989-8700-17297b213120

Candidate causes, most to least likely from the outside:

  1. Retry with a fresh ID. The webhook send is retried after a slow or failed response, and each attempt mints a new invitee UUID instead of reusing the first. Fits the sub-second timing and the varying count (2 vs 3).
  2. The create step runs once per something. Per host, per calendar, or an event handler bound more than once. Would need to be a bug given the count varies.
  3. Double-submit on the reschedule confirmation. Fits the 65-second case, not the one-second ones.

9. Fixes

Booking tool

Make reschedule idempotent

  1. One invitee record per reschedule. Reuse the invitee ID across retries, or dedupe on (event type, invitee email, start time) before emitting invitee.created.
  2. Cancel every superseded invitee. On a later reschedule or cancel, send invitee.canceled for each record that is being replaced, not just one, so nothing is left sending for the old time.
Customer.io

Key meetings on person + start time

  1. Profile ID becomes email|start_time instead of the invitee URI. Multiple webhooks for the same meeting then update one record. URI kept as an attribute for tracing.
  2. Reminder re-entry set to a 30-minute minimum interval, so repeated booked events within seconds start one journey, not three.
  3. Cancel and reschedule look up by the same key, so a second reschedule updates the one record and there are no orphans.

Will be built on a draft copy and tested with a live booking, reschedule and cancel before going live. Removes the dependency on the booking-tool fix, but does not replace it.

Already done (17 Sep): the one pending duplicate, Latoya Waddle's 22 Sep walkthrough, had two live records. I marked one completed so she receives a single set of reminders. Swept all other duplicates from 1–17 Sep; none are still pending. Frequency caps were considered and rejected: Customer.io applies them per person, and each duplicate record is a separate person.

Appendix: webhook contract the receiver depends on

Payload pathUsed for
eventinvitee.created or invitee.canceled
payload.uriMeeting profile identifier. Must be unique and stable per booking, and the same URI must return on its cancel or reschedule.
payload.email, payload.name, payload.timezone, payload.text_reminder_numberRecipient details; the event is fanned out to every profile whose email matches.
payload.new_inviteePresent on the cancel-half of a reschedule. Distinguishes reschedule from hard cancel.
payload.scheduled_event.start_time / end_timeISO 8601. Drives the day-before, 1-hour and T-30 waits.
payload.scheduled_event.name, .event_memberships[0].user_name / user_email, .location.join_urlPersonalisation in the reminders.
payload.reschedule_url, payload.cancel_urlLinks in the emails.
All facts read from the Customer.io API on 17 Sep 2026 (profiles, deliveries, campaign 69 and 92 configuration). Where I am inferring rather than observing, the text says so. Questions: Carlos.