Double-booking is a concurrency problem, not an availability problem. You can show a slot as open, validate it, and still hand the same 2:00 PM block to two people who tapped Book within the same second. Checking free time tells you what was open a moment ago. It says nothing about who else is mid-checkout for that exact slot right now. This recipe covers the race itself: the managed path, where the booking flow guards the slot for you, and the do-it-yourself path with the Calendar API, where the guard is your job.
Which path prevents double-booking for me?
Section titled “Which path prevents double-booking for me?”The managed Scheduler path enforces no-double-booking for you, so a slot already taken returns an error on the second request. The do-it-yourself Calendar API path gives you raw create-event control but no built-in guard, so you add your own lock or database constraint to win the race.
The split comes down to who owns the slot guard. With the Scheduler booking flow, the platform tracks which slots are claimed and rejects a second claim on the same one, usually within a few hundred milliseconds of the first. You write almost no concurrency code. With the Calendar API, you call POST /v3/calendars/availability, pick a slot, and create the event yourself. Nothing stops two of your own requests from creating two events in the same window. That gap is where double-booking lives, and closing it is the bulk of this guide. Most teams should start on the managed path and only drop down to the Calendar API when they need a fully custom flow across all 6 providers.
How does the managed Scheduler path stop a double-booking?
Section titled “How does the managed Scheduler path stop a double-booking?”The Scheduler booking flow validates the slot against the Configuration and the live calendar at write time, then claims it. Two requests for the same 30-minute slot resolve to one success and one rejection, so you never persist two conflicting events for that block.
A booking request goes to POST /v3/scheduling/bookings with a start_time and end_time that must match a slot the Scheduling Availability endpoint returned for the same Configuration. Nylas re-validates that window before it writes the event, so a slot that filled in the last 30 seconds gets rejected rather than booked. You do not re-check availability yourself and you do not hold a lock. The flow does both. The 2 things you supply are a valid session or Configuration reference and a slot that was open when the form loaded. The request below books a slot through the managed flow.
curl --compressed --request POST \ --url 'https://api.us.nylas.com/v3/scheduling/bookings' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <SCHEDULER_SESSION_ID>' \ --header 'Content-Type: application/json' \ --data '{ "start_time": 1709643600, "end_time": 1709645400, "participants": [], "guest": { "name": "Jane Doe", "email": "[email protected]" } }'from nylas import Client
nylas = Client( "<NYLAS_API_KEY>", "<NYLAS_API_URI>",)
booking = nylas.scheduler.bookings.create( request_body={ "start_time": 1763119800, "end_time": 1763121600, "guest": { "name": "Jane Doe", }, }, query_params={ "configuration_id": "<SCHEDULER_CONFIG_ID>", },)
print("Created booking:", booking)When the slot is gone, the call returns a non-200 status. Catch it, refresh the slot list, and ask the user to pick again. See the book an event reference for the full request and response shape.
How do I prevent double-booking with the Calendar API directly?
Section titled “How do I prevent double-booking with the Calendar API directly?”The Calendar API has no atomic conditional create, so you guard the slot in your own application. Re-check availability immediately before writing, hold an app-side lock or a database unique constraint on the slot key, then create the event only while you own that lock.
Be honest with yourself here: there is no “create this event only if the slot is still free” call. The check and the write are 2 separate requests, and any gap between them, even 50 ms, is a window where a competitor can slip in. So the lock carries the guarantee, not the availability check. The safe order is lock the slot key, re-run POST /v3/calendars/availability for that exact window, create the event with POST /v3/grants/{grant_id}/events?calendar_id=<CALENDAR_ID> (the calendar_id query parameter is required), then release. A database unique constraint on (calendar_id, start_time) is the most durable lock because it survives process restarts and works across every server in your fleet. The re-check first appears in check availability.
curl --request POST \ --url 'https://api.us.nylas.com/v3/calendars/availability' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "start_time": 1700000000, "end_time": 1700003600, "duration_minutes": 30, "interval_minutes": 30, "participants": [ { "email": "[email protected]", "calendar_ids": ["primary"] } ] }'Only after the re-check passes and you hold the lock do you write the event.
How do I create the event once I hold the lock?
Section titled “How do I create the event once I hold the lock?”With the slot key locked and the re-check passing, write the event with POST /v3/grants/{grant_id}/events. Pass the calendar_id as a query parameter and the slot times in the when object. Release the lock after the response, whether it succeeds or fails.
This is the write half of the do-it-yourself path. The event lands on the host calendar, the participants array adds the booker, and the when object carries the same Unix-second start_time and end_time you validated 1 step earlier. Keep the locked window short: hold the lock only around the re-check and this create call, typically under 500 ms, so other bookers are not blocked longer than they must be. If you store a row in your own bookings table keyed on (calendar_id, start_time) with a unique constraint, the insert is what actually serializes competing requests. The Nylas event create is then a follow-on action you take only after that insert wins.
curl --compressed --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "title": "Annual Philosophy Club Meeting", "busy": true, "conferencing": { "provider": "Zoom Meeting", "autocreate": { "conf_grant_id": "<NYLAS_GRANT_ID>", "conf_settings": { "settings": { "join_before_host": true, "waiting_room": false, "mute_upon_entry": false, "auto_recording": "none" } } } }, "participants": [ { "name": "Leyah Miller", "email": "[email protected]" }, { "name": "Nyla", "email": "[email protected]" } ], "resources": [{ "name": "Conference room", "email": "[email protected]" }], "description": "Come ready to talk philosophy!", "when": { "start_time": 1674604800, "end_time": 1722382420, "start_timezone": "America/New_York", "end_timezone": "America/New_York" }, "location": "New York Public Library, Cave room", "recurrence": [ "RRULE:FREQ=WEEKLY;BYDAY=MO", "EXDATE:20211011T000000Z" ],}'from nylas import Client
nylas = Client( "<NYLAS_API_KEY>", "<NYLAS_API_URI>")
grant_id = "<NYLAS_GRANT_ID>"
events = nylas.events.create( grant_id, request_body={ "title": 'Build With Nylas', "when": { "start_time": 1609372800, "end_time": 1609376400 }, }, query_params={ "calendar_id": "<CALENDAR_ID>" })
print(events)The response returns the created event with its id. Store that id against your bookings row so a later cancel or reschedule can find it. The create an event reference lists every field.
What happens when a conflict slips through anyway?
Section titled “What happens when a conflict slips through anyway?”A conflict that escapes the lock shows up as 2 events in the same window. Detect it by listing events for the slot after writing, then resolve it by canceling the later booking, notifying that booker, and offering the next open slot. Treat detection as a routine background check, not an edge case.
No lock is perfect. A retry storm, a half-released lock, or a clock skew of a few seconds can leave 2 events on one calendar. So build a reconciliation step that runs after each write or on a short timer: list events for the affected window and, if more than 1 booking maps to the same slot key, keep the earliest and cancel the rest. The booker whose event you cancel should get an email with a fresh slot list, never a silent failure. Log every collision with the slot key and both event IDs so you can tune lock duration later. A tight lock makes collisions rare, but the reconciliation pass is what turns the occasional stray conflict into a recoverable hiccup instead of an angry support ticket.
What’s next
Section titled “What’s next”- Check availability for the re-check request and response fields
- Customize the booking flow to configure the form, buffers, and notice windows
- Embed a scheduling page to drop the managed flow into your app
- Book an event reference for the managed booking endpoint
- Create an event reference for the do-it-yourself event fields