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
- 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.
- 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.
- How often: 8 bookings in 17 days (Sep 1–17), every one a reschedule, each duplicated 2× or 3×. Roughly 1 in 130 bookings.
- Why it surfaced now: it has been duplicating emails for weeks; the T-30 SMS reminder went live ~7 Sep and made it visible.
- 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).
Thu 11 Sep
14:44
Signs up for CarepatronSpeech-language pathology, solo, Indiana
14:50:03
Books "Carepatron Walkthrough" with Hyden for Mon 14 Sep, 3:00 pmOne invitee record created: …/invitees/a94e71a3466f4aef8c0f34078a3173df
14:52:07
14:52:08
14:52:08
Reschedules to 3:30 pm. Three new invitee records arrive in one second ×3The 3:00 record is correctly marked rescheduled. The three new ones are identical apart from their IDs.
14:57:10
3 × email "Confirmed: Carepatron Walkthrough" ×3
Sat 13 Sep
15:30:03
3 × email "Tomorrow: Carepatron Walkthrough" ×3
Mon 14 Sep
14:30:05
3 × email "In 1 hour: Carepatron Walkthrough" ×3
15:00:03
15:00:04
15:00:06
3 × SMS, T-30 reminder ×3This is what she noticed. All three delivered.
15:30
Demo with Hyden
16:05
Subscribes to Plus (annual)No commercial harm in this instance.
3. The four records behind it
| Invitee ID (end of URI) | Created (UTC) | Meeting start | Status | Note |
| a94e71a3466f4aef8c0f34078a3173df | 11 Sep 18:50:03 | 14 Sep 15:00 ET | rescheduled | Original booking. Plain 32-char hex ID. |
| 7fdf5006-a539-4f27-aee7-b6effdef7815 | 11 Sep 18:52:07 | 14 Sep 15:30 ET | completed | Reschedule. Three dashed UUIDs, identical payloads, one second apart. Each ran a full reminder journey. |
| 7a1bc7f4-9d64-4cf5-882e-6788efa63041 | 11 Sep 18:52:08 | 14 Sep 15:30 ET | completed |
| 9845346f-3970-4e33-9784-263d3e266373 | 11 Sep 18:52:08 | 14 Sep 15:30 ET | completed |
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.
a94e71a3466f4aef8c0f34078a3173df →
7fdf5006-a539-… 7a1bc7f4-9d64-… 9845346f-3970-… heather.smith.91075@ · 11 Sep · 3 copies in 1 s
b7eacfd47ae9491d8691f1e9358f10ae →
515015a0-f38a-… 12071de9-c788-… 9086c063-01af-… [email protected] · 29 Aug · 3 copies in 1 s
bb667e174434407ea39903fb5c82ecef →
518d1186-2296-… 4b9cf556-674b-… 2574694f-00d7-… [email protected] · 1 Sep · 3 copies, same second
8f26d4e1500a43cba03ce02d8e3fb195 →
697a1c1d-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:
- The ID format changes between a new booking and a reschedule, so the reschedule runs through a different code path.
- Copies land within one second in three of four cases, which points at a retry or a handler firing more than once rather than a user clicking repeatedly. Latoya's 65-second gap could be either.
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)
29 Aug
07:02:03
Reschedule creates 3 records for 17 Sep 09:15 ×3
16 Sep
09:15:05
3 × "Tomorrow: Onboarding with Ammaarah" ×3
17 Sep
06:00:06
She reschedules to 29 Sep. One of the three records (9086c063…) is flipped to rescheduled. The other two are untouched.
06:05:14
Correct confirmation for 29 Sep
08:15:03
2 × "In 1 hour: Onboarding with Ammaarah" for the 09:15 slot she moved two hours earlier
×2The flipped record stayed silent, which confirms the status change does halt a journey. The two orphans did not know.
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:
- 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).
- 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.
- Double-submit on the reschedule confirmation. Fits the 65-second case, not the one-second ones.
9. Fixes
Booking tool
Make reschedule idempotent
- One invitee record per reschedule. Reuse the invitee ID across retries, or dedupe on (event type, invitee email, start time) before emitting
invitee.created.
- 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
- 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.
- Reminder re-entry set to a 30-minute minimum interval, so repeated booked events within seconds start one journey, not three.
- 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 path | Used for |
| event | invitee.created or invitee.canceled |
| payload.uri | Meeting 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_number | Recipient details; the event is fanned out to every profile whose email matches. |
| payload.new_invitee | Present on the cancel-half of a reschedule. Distinguishes reschedule from hard cancel. |
| payload.scheduled_event.start_time / end_time | ISO 8601. Drives the day-before, 1-hour and T-30 waits. |
| payload.scheduled_event.name, .event_memberships[0].user_name / user_email, .location.join_url | Personalisation in the reminders. |
| payload.reschedule_url, payload.cancel_url | Links in the emails. |