A recurring booking creates a series of bookings at a fixed frequency. Each occurrence is a separate booking record tracked under a booking_series parent object.
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/bookings | Create a booking — include a recurrence object to create a series |
| GET | /v1/booking-series | List all series |
| GET | /v1/booking-series/:id | Get series + all occurrences |
| GET | /v1/bookings?series_id= | Filter individual bookings by series |
Include a recurrence object in the standard booking creation request:
curl -X POST https://api.vennio.app/v1/bookings \
-H "Authorization: Bearer $VENNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"business_id": "your-business-id",
"customer_email": "customer@example.com",
"customer_name": "Jane Doe",
"start_time": "2026-04-07T10:00:00Z",
"end_time": "2026-04-07T10:30:00Z",
"recurrence": {
"frequency": "weekly",
"count": 8,
"timezone": "America/New_York"
}
}'
| Field | Type | Description |
|---|---|---|
frequency |
weekly | biweekly | monthly |
How often the booking repeats |
count |
integer | Number of occurrences to create |
until_date |
ISO date | Create occurrences up to (and including) this date |
timezone |
IANA timezone | Wall-clock timezone for recurrence expansion |
Provide count or until_date — not both.
The response includes the booking_series object alongside the created bookings:
{
"booking_series": {
"id": "series_abc123",
"frequency": "weekly",
"created_count": 8,
"skipped_count": 0
},
"bookings": [...]
}
created_count — number of occurrences successfully createdskipped_count — number of occurrences skipped due to conflictsCancelling a series cancels all future confirmed occurrences and fires a booking_series.cancelled webhook. Past occurrences are unaffected.
The timezone in the recurrence object is used to expand occurrences. Wall-clock time stays consistent — a 10:00 AM weekly meeting stays at 10:00 AM even when clocks change.
If the start date falls on a day that doesn't exist in a target month (e.g. 31 January → February), the occurrence is clamped to the last valid day of that month (28 or 29 February).
created_count and skipped_count in the series response show what was created versus skipped due to calendar conflicts.