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.
- Receive-side dedup — automate a “find duplicate by email/phone” step in your workflow that merges or skips duplicates after the fact.
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.