Recurring Schedule
The repeating pattern an experience runs on โ which weekdays, at which times, over which dates or seasons. Sessions are generated from this pattern, so setting it is how you put a whole season on the calendar in one request instead of adding dates one at a time.
Different times in different date rangesโ
A repeating schedule is a list of date ranges, each with its own days and times โ "July runs 9, 10
and 11am on weekdays; August runs 10, 11 and 12". An experience that runs one pattern all year has a
single range, and GET /recurring-schedule keeps describing it exactly as before.
GET /v1/experiences/{id}/schedule-periods
POST /v1/experiences/{id}/schedule-periods
PUT /v1/experiences/{id}/schedule-periods/{periodId}
DELETE /v1/experiences/{id}/schedule-periods/{periodId}
GET needs experiences:read; the rest need experiences:write.
| Field (POST / PUT) | Type | Required | Description |
|---|---|---|---|
date_from | string | Yes | First date of the range (YYYY-MM-DD) |
date_to | string | No | Last date. Omit to run until a later range starts |
days_of_week | array | No | Lowercase weekday names. Omit for every day |
times | array | No | Start times each running day in this range, HH:MM |
day_times | object | No | Days inside this range that run different times |
window_opens / window_closes / every_value / every_unit | No | An every-so-often range instead of set times | |
sessions_end_at | string | No | Every session in this range ends at this time, HH:MM, whatever time it starts (for example, everyone back by 16:30). Send "" to clear it so sessions end at start + the experience length. Omit to keep the current value |
season_ids | array | No | Run inside these seasons instead of explicit dates |
booking_option_id | integer | No | Run this range for ONE way of booking. Omit for every option |
guide_ids | array | No | Staff who normally run this range. Any ONE of them being free is enough for a session to sell |
repeats_yearly | boolean | No | Add next year's copy of this range by itself. See Repeats every year |
curl -X POST "https://app.equipdash.com/api/v1/experiences/1/schedule-periods" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"date_from": "2026-08-01",
"date_to": "2026-08-31",
"days_of_week": ["monday","tuesday","wednesday","thursday","friday"],
"times": ["10:00", "11:00", "12:00"]
}'
A date can only follow one set of times, so a range covering dates another range already covers is
rejected with 422 naming the range it clashes with. The check runs on the dates a range actually
resolves to, so a season-based range is compared the same way. A range with no date_to is allowed โ
it simply stops the day before the next range starts.
One range for one way of bookingโ
A shop that sells an experience more than one way can put each on its own days and times: private
lessons on Tuesdays and Thursdays, the shared session the rest of the week. Send
booking_option_id on the range and the sessions it generates are reserved for that option.
curl -X POST "https://app.equipdash.com/api/v1/experiences/1/schedule-periods" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"date_from": "2026-08-01",
"date_to": "2026-08-31",
"days_of_week": ["tuesday","thursday"],
"times": ["11:00"],
"booking_option_id": 64
}'
Two ranges for DIFFERENT options may share dates - that is the point, and the overlap rule above
only applies within one option (two ranges both left open to every option count as the same one). An
id that does not belong to this experience is rejected with 422.
Every read of a range returns booking_option_id and booking_option_name; both are null when the
range runs for every option, which is what every range said before this field existed. Omit the field
on a PUT and the range keeps the option it has.
Who runs a rangeโ
Set guide_ids and every session the range creates is run by any one of those staff. It is a
default, not an assignment: no session row is written and nobody is emailed. Guides hear about a
departure when a booking locks them onto it, which is unchanged.
curl -X PUT "https://app.equipdash.com/api/v1/experiences/1/schedule-periods/42" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"date_from": "2026-08-01",
"days_of_week": ["saturday"],
"times": ["09:00"],
"guide_ids": [12, 19]
}'
A session takes its own guides when somebody picked them on that session, and those win. A session
nobody has touched follows its range, so changing guide_ids reaches every future session at once.
Leaving the key out and sending an empty array mean different things. Omit guide_ids and the
range keeps the guides it has, so a client that knows nothing about this field can never wipe a pool.
Send "guide_ids": [] and the range runs with nobody, which on an experience that needs a guide
means its sessions stop selling until somebody is added.
Ids are checked against this shop's staff; anything else is dropped. Every read of a range returns
guide_ids and guides (the names), both empty when the range has none.
Removing a range removes the future sessions only that range produced; sessions with bookings are
always kept, and nothing outside those dates changes. The response reports sessions_removed.
Once an experience has more than one range, PUT /recurring-schedule returns 422 โ it writes "the"
schedule, which no longer means anything. Use these endpoints instead.
Repeats every yearโ
Send "repeats_yearly": true on a range with a date_to and it comes round every year. Each night,
while it is on, next year's copy of the range is added with the same days and times and the switch
still on, and its sessions are generated. The copy lands on the shop's yearly season when the range
follows one, and otherwise on the same dates a year later.
curl -X PUT "https://app.equipdash.com/api/v1/experiences/1/schedule-periods/42" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"date_from": "2026-12-01",
"date_to": "2027-04-05",
"times": ["09:00", "13:00"],
"repeats_yearly": true
}'
It is off unless you turn it on. Leave the key out on a PUT and the range keeps the value it has, so
a client that only moves times never turns it off. It is ignored on a range with no date_to, which
keeps running anyway, and on a range built from season_ids, which follows its seasons. Every read of
a range returns repeats_yearly: GET /schedule-periods, GET /recurring-schedule and the response
to each write.
Read the patternโ
GET /v1/experiences/{id}/recurring-schedule
Requires the experiences:read scope. Returns null for schedule when the experience has no
repeating pattern (its sessions were added date by date).
times are the times every running day uses. times_by_day lists only the days that run something
different โ every day absent from it uses times.
{
"data": {
"experience_id": 1,
"schedule": {
"runs_on_days": ["tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"],
"repeats_by": "set_times",
"times": ["11:00 AM", "2:00 PM"],
"times_by_day": { "friday": ["11:00 AM", "2:00 PM", "9:30 PM"] },
"window_opens": null,
"window_closes": null,
"sessions_end_at": null,
"every": null,
"date_window": { "from": "2026-09-01", "to": "2026-11-30" },
"seasons": [],
"repeats_yearly": false
}
}
}
Set the patternโ
PUT /v1/experiences/{id}/recurring-schedule
Requires the experiences:write scope.
Future sessions the new pattern no longer produces are removed. Sessions that already have bookings are always kept. Read the current pattern first if you only mean to change part of it.
| Field | Type | Required | Description |
|---|---|---|---|
days_of_week | array | No | Lowercase weekday names it runs on. Omit for every day |
times | array | No | Set start times each running day, 24-hour HH:MM. Use this or the window fields below |
day_times | object | No | Days that run different times from times, keyed by lowercase weekday, each a list of HH:MM. List only the days that differ |
window_opens | string | No | First start time of the day, HH:MM |
window_closes | string | No | Last start time of the day, HH:MM |
every_value | integer | No | How often a session starts inside the window |
every_unit | string | No | Minutes or Hours |
sessions_end_at | string | No | Every session in this range ends at this time, HH:MM, whatever time it starts (for example, everyone back by 16:30). Send "" to clear it so sessions end at start + the experience length. Omit to keep the current value |
date_from | string | No | First date the pattern applies (YYYY-MM-DD). Defaults to today |
date_to | string | No | Last date it applies. Omit to keep running โ a year of sessions is generated at a time |
season_ids | array | No | Run only inside these seasons instead of a date window |
repeats_yearly | boolean | No | Add next year's copy by itself. Needs a date_to; ignored with season_ids. Omit to keep the current value. See Repeats every year |
A date-only experience (a multi-day package, or an open window) departs on a day rather than at a time, so the time fields are ignored for it.
curl -X PUT "https://app.equipdash.com/api/v1/experiences/1/recurring-schedule" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"days_of_week": ["tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"],
"times": ["11:00", "14:00"],
"date_from": "2026-09-01",
"date_to": "2026-11-30"
}'
Different times on some daysโ
An experience that runs at 7:00 PM and 8:30 PM every night, with an extra 9:30 PM on Friday and
Saturday. Days left out of day_times use times.
-d '{
"times": ["19:00", "20:30"],
"day_times": {
"friday": ["19:00", "20:30", "21:30"],
"saturday": ["19:00", "20:30", "21:30"]
}
}'
Change one day without re-sending the patternโ
PUT /v1/experiences/{id}/recurring-schedule/days/{day}
Requires the experiences:write scope. {day} is a lowercase weekday. Send times for that day, or
use_default_times: true to drop its own times and go back to the shared times. Everything else
about the schedule โ the dates, the days it runs, the shared times โ is left exactly as it was.
| Field | Type | Required | Description |
|---|---|---|---|
times | array | No* | The exact times this day should run, HH:MM |
use_default_times | boolean | No* | true puts the day back to the shared times |
* Send one or the other.
curl -X PUT "https://app.equipdash.com/api/v1/experiences/1/recurring-schedule/days/friday" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "times": ["19:00", "20:30", "21:30"] }'
The day must already be one of the days the schedule runs, and the schedule must be a set-times one โ
both are rejected with 422 otherwise. As with the full replace, sessions this day no longer produces
are removed unless they have bookings.
day_times applies to set-times patterns only โ an every-so-often pattern, a package and an open
window all run one window per day, so it is ignored for those.
Every-so-often instead of set timesโ
-d '{
"days_of_week": ["saturday", "sunday"],
"window_opens": "09:00",
"window_closes": "16:00",
"every_value": 30,
"every_unit": "Minutes"
}'