Skip to main content

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)TypeRequiredDescription
date_fromstringYesFirst date of the range (YYYY-MM-DD)
date_tostringNoLast date. Omit to run until a later range starts
days_of_weekarrayNoLowercase weekday names. Omit for every day
timesarrayNoStart times each running day in this range, HH:MM
day_timesobjectNoDays inside this range that run different times
window_opens / window_closes / every_value / every_unitNoAn every-so-often range instead of set times
sessions_end_atstringNoEvery 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_idsarrayNoRun inside these seasons instead of explicit dates
booking_option_idintegerNoRun this range for ONE way of booking. Omit for every option
guide_idsarrayNoStaff who normally run this range. Any ONE of them being free is enough for a session to sell
repeats_yearlybooleanNoAdd 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"]
}'
Ranges may not overlap

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.

This replaces the existing pattern

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.

FieldTypeRequiredDescription
days_of_weekarrayNoLowercase weekday names it runs on. Omit for every day
timesarrayNoSet start times each running day, 24-hour HH:MM. Use this or the window fields below
day_timesobjectNoDays that run different times from times, keyed by lowercase weekday, each a list of HH:MM. List only the days that differ
window_opensstringNoFirst start time of the day, HH:MM
window_closesstringNoLast start time of the day, HH:MM
every_valueintegerNoHow often a session starts inside the window
every_unitstringNoMinutes or Hours
sessions_end_atstringNoEvery 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_fromstringNoFirst date the pattern applies (YYYY-MM-DD). Defaults to today
date_tostringNoLast date it applies. Omit to keep running โ€” a year of sessions is generated at a time
season_idsarrayNoRun only inside these seasons instead of a date window
repeats_yearlybooleanNoAdd 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.

FieldTypeRequiredDescription
timesarrayNo*The exact times this day should run, HH:MM
use_default_timesbooleanNo*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"
}'