Race conditions & idempotency
The Connect API isn’t transactional from the consumer’s perspective — calls happen sequentially over HTTP, and state can change between them. A few specific races and how to handle them.
Slot-fill races
Section titled “Slot-fill races”Between fetching availability and creating a booking, the slot can fill (someone else booked it via the form widget, the console, or another Connect call).
[Connect] GET /availability/ → slot S available[Form widget] someone books slot S[Connect] POST /calendar_bookings/ → 400 The selected time slot is no longer availableThe booking endpoint does a transactional capacity recheck inside the DB transaction — if the slot filled in between, you get a clean 400 instead of a corrupted booking.
Recovery: refetch availability, pick a fresh slot, retry. Don’t assume the original start/end is still good.
async function bookWithRetry(input, maxAttempts = 3) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { const response = await fetch('/connect/calendar_bookings/', { method: 'POST', headers, body: JSON.stringify(input) }); if (response.ok) return response.json(); const error = await response.json(); if (!/no longer available/.test(error.message)) throw error; // Slot filled — refetch availability and retry const fresh = await refetchAvailability(); input.startDateTime = fresh.startDateTime; input.endDateTime = fresh.endDateTime; input.resourceSlotId = fresh.resourceSlotId; } throw new Error('Could not book after ' + maxAttempts + ' attempts');}Duplicate creates
Section titled “Duplicate creates”POST /connect/table_entries/ is not idempotent — every POST creates a new lead entry. There’s no built-in dedup key.
If your sender retries on transient failure (network blip, 5xx), you’ll get duplicate leads. Two ways to defend:
- Send-side idempotency — generate a stable key on your side (e.g., hash of email + timestamp bucket) and skip the POST if you’ve already submitted it.
- Let the studio resolve it — a retried create is a second lead, but not a second person: when the name matches and every channel you send is already on file, the same contact is attached to both leads. Studios see the requests grouped under that person in the lead pool, and likely duplicate people are offered for review and merge in the console. See Contacts → Matching.
Reschedules aren’t atomic
Section titled “Reschedules aren’t atomic”There’s no single “reschedule” endpoint. The pattern is delete + recreate:
await fetch(`/connect/calendar_bookings/${oldId}`, { method: 'DELETE', headers });await fetch(`/connect/calendar_bookings/`, { method: 'POST', headers, body: JSON.stringify(newBooking) });Between the two calls:
- The old booking’s
calendar.appointment.deletedevent fires. - A flow listening to deletes might cancel the lead’s “appointment confirmed” SMS.
- Then
calendar.appointment.bookedfires for the new booking and the SMS gets sent again.
If your flows aren’t idempotent on the receiving end, this can cause duplicate notifications. Either:
- Make your flows idempotent (e.g., dedup SMS by recipient + day).
- Order the calls carefully (recreate first if the new slot is guaranteed available, then delete).
- Use a “rescheduling” lead state to suppress notifications during the transition.
Transient errors
Section titled “Transient errors”The Connect API isn’t currently rate-limited, but you should still handle 429 and 5xx defensively:
| Status | Strategy |
|---|---|
429 Too Many Requests | Exponential backoff (base 1s, cap at 60s). Honor Retry-After if present. |
500 Internal Server Error | Retry up to 3× with backoff. If it persists, log and surface to ops. |
502/503/504 | Same as 500. |
4xx (other) | Don’t retry — fix the request or surface to ops. |
Related
Section titled “Related”- Authentication & scopes — error codes.
- Bookings resource — full slot validation flow.
- Workflows — idiomatic patterns that minimize race exposure.