You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a recurring booking is created through the API v2 endpoint (POST /v2/bookings) using the recurrenceCount field, any booking limits configured on the event type — such as "Max 1 booking per day" or "Max 3 bookings per week" — are only evaluated against the first occurrence. All subsequent occurrences in the same recurring request are inserted without re-checking the limit. This allows an attendee to silently bypass daily, weekly, monthly, or yearly booking caps in a single API call.
This is a correctness and fairness bug affecting any host who has booking limits configured and whose event types are booked through the v2 API — including platform customers, Zapier / Make / n8n automation users, and custom booking UIs built on the Cal.com Platform product.
Steps to Reproduce
Log in to cal.com (or a self-hosted instance on main).
Go to Event Types → create or edit an event type with the following settings:
Duration: 30 minutes
Booking limits: Max 1 booking per day
Recurring bookings: enabled
Obtain a valid API v2 access token (via OAuth or personal access token) for the event owner.
Check the HTTP response — all 5 bookings return 201 Created.
Call GET /v2/bookings and observe that 5 booking records now exist for the same host, all on the same day.
Actual Results
All 5 recurring occurrences are created unconditionally, ignoring the "Max 1 booking per day" limit.
The checkBookingLimits function inside packages/lib/server/checkBookingLimits.ts is invoked only for the first occurrence. The remaining 4 are inserted without the check being re-run against the accumulating count.
No error is returned — the caller receives 201 Created for every occurrence.
The host's calendar ends up with 5 bookings on the same day, far exceeding the configured limit.
Expected Results
Booking limits should be evaluated for every individual occurrence before any of them are persisted.
If any single occurrence would push the cumulative total over the configured limit for that period (day / week / month / year), the entire recurring request should be rejected with a 400 Bad Request, for example:
{
"status": "error",
"error": {
"message": "Booking limit of 1 per day exceeded for one or more of the requested recurring dates."
}
}
The same booking limits enforced on the UI booking page should behave identically when bookings are created through the API v2.
Technical Details
Affected endpoint:POST /v2/bookings (API v2)
Relevant source files to investigate:
apps/api/v2/src/modules/bookings/services/bookings.service.ts — iterates over recurring occurrences and calls the booking handler per date
packages/lib/server/checkBookingLimits.ts — performs the per-period count check
packages/features/bookings/lib/handleNewBooking/index.ts — core booking handler invoked for each occurrence
Root cause: The recurring occurrence loop calls handleNewBooking sequentially per date. Each call re-queries the DB for the current booking count from scratch, so earlier iterations that were inserted in the same in-flight request are either not yet visible (transaction isolation) or not accumulated in memory — meaning the limit counter never increments across the loop, and every iteration passes the same stale check.
Node.js version: Reproducible on Node.js 18.x and 20.x.
Reproduced on a self-hosted main branch instance (Docker Compose). Created an event type with "Max 1 booking per day" enabled. Sent POST /v2/bookings with recurrenceCount: 5. The API returned 201 Created for all 5 occurrences and GET /v2/bookings confirmed 5 records on the same day. Then attempted to book a second individual 30-minute slot on the same day via the UI booking page — that correctly returned the "Booking limit reached" error, confirming the limit logic works on the UI path but is bypassed via the API v2 recurring path.
Issue Summary
When a recurring booking is created through the API v2 endpoint (
POST /v2/bookings) using therecurrenceCountfield, any booking limits configured on the event type — such as "Max 1 booking per day" or "Max 3 bookings per week" — are only evaluated against the first occurrence. All subsequent occurrences in the same recurring request are inserted without re-checking the limit. This allows an attendee to silently bypass daily, weekly, monthly, or yearly booking caps in a single API call.This is a correctness and fairness bug affecting any host who has booking limits configured and whose event types are booked through the v2 API — including platform customers, Zapier / Make / n8n automation users, and custom booking UIs built on the Cal.com Platform product.
Steps to Reproduce
main).201 Created.GET /v2/bookingsand observe that 5 booking records now exist for the same host, all on the same day.Actual Results
checkBookingLimitsfunction insidepackages/lib/server/checkBookingLimits.tsis invoked only for the first occurrence. The remaining 4 are inserted without the check being re-run against the accumulating count.201 Createdfor every occurrence.Expected Results
400 Bad Request, for example:{ "status": "error", "error": { "message": "Booking limit of 1 per day exceeded for one or more of the requested recurring dates." } }Technical Details
POST /v2/bookings(API v2)apps/api/v2/src/modules/bookings/services/bookings.service.ts— iterates over recurring occurrences and calls the booking handler per datepackages/lib/server/checkBookingLimits.ts— performs the per-period count checkpackages/features/bookings/lib/handleNewBooking/index.ts— core booking handler invoked for each occurrencehandleNewBookingsequentially per date. Each call re-queries the DB for the current booking count from scratch, so earlier iterations that were inserted in the same in-flight request are either not yet visible (transaction isolation) or not accumulated in memory — meaning the limit counter never increments across the loop, and every iteration passes the same stale check.lengthInMinutesis ignored for recurring bookings via API v2), which confirms the recurring path in the v2 API is broadly under-validated.Evidence
Reproduced on a self-hosted
mainbranch instance (Docker Compose). Created an event type with "Max 1 booking per day" enabled. SentPOST /v2/bookingswithrecurrenceCount: 5. The API returned201 Createdfor all 5 occurrences andGET /v2/bookingsconfirmed 5 records on the same day. Then attempted to book a second individual 30-minute slot on the same day via the UI booking page — that correctly returned the "Booking limit reached" error, confirming the limit logic works on the UI path but is bypassed via the API v2 recurring path.