An event series is a run of dates from one floor plan: a play every night for three weeks, a matinee and an evening each Saturday, a cinema’s showtimes. Each date is a normal event with its own key. You sell it, report on it and check it in exactly like any other event, through the Events API.
If the dates must be bought together, use a Performance Group instead. If buyers hold a seat for a whole season and renew it, use Seasons.
Every route on this page takes a matching sk_test_… or sk_live_… secret
key. A test key makes test events; a live key makes live events, which use
credits like any other live event.
A key can’t make a date public. The paste-in run tag lists public dates that sell through SeatLayer checkout, so make the dates public from the dashboard first.
How the dates are planned
You send a schedule, not a list of timestamps. SeatLayer plans the dates with the same engine the dashboard’s New event sheet uses, so a series made by API and one made in the dashboard have the same dates.
| Field | Meaning |
|---|---|
rule |
none (one day), daily, weekly, biweekly, or dates (the days in dates) |
from |
The first day, YYYY-MM-DD. Not used by dates |
until or count |
The last day (inclusive), or how many days. With neither, one day |
times |
One to six start times, HH:MM. Each day gets one date per time |
skip |
Moments to leave out, YYYY-MM-DDTHH:MM: “Sunday has no evening show” |
dates |
For rule: dates: the days, YYYY-MM-DD |
Times are read in the series’ timezone (an IANA name such as
Europe/London). A start time stays the same on the clock across a daylight
saving change: a 19:30 show is at 19:30 in October and in November. Without a
timezone, the usual zone of this floor plan’s events is used, else UTC.
Every date carries the series name as its name, Hamlet, with nothing added.
The dates are told apart by startsAt and linked by seriesKey, so a ticket
page can show one title with its times. For that reason a series name cannot
hold {n}, {date} or {time}.
A series holds at most 366 dates. One call adds at most 100.
Preview, then create
/v1/event-seriesSecret keySend dryRun: true to see the dates without creating anything:
curl https://api.seatlayer.io/v1/event-series \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Hamlet",
"chartKey": "c_41b2…",
"timezone": "Europe/London",
"schedule": { "rule": "daily", "from": "2027-03-01", "until": "2027-03-21", "times": ["14:30", "19:30"] },
"dryRun": true
}'{
"timezone": "Europe/London",
"dates": [
{ "moment": "2027-03-01T14:30", "startsAt": 1803911400000, "name": "Hamlet" },
{ "moment": "2027-03-01T19:30", "startsAt": 1803929400000, "name": "Hamlet" }
]
}Without dryRun, the series is created and a job makes its dates. The answer is
202 straight away; send an Idempotency-Key so a retry gets the same series:
{
"series": { "seriesKey": "ser_7c1d…", "name": "Hamlet", "state": "creating", "dateCount": 0, "job": { "jobId": "ser_7c1d…:create", "state": "running", "total": 42, "done": 0, "failed": 0 } },
"jobId": "ser_7c1d…:create"
}The first date is made from the published floor plan, as
POST /v1/events makes an event. Every later date is a copy of the first:
the same seats, the same event-owned prices, releases, booking form, online
codes and blocked seats. Optional fields: venue, and durationMinutes to set
each date’s end.
When the job ends, SeatLayer sends series.created
with the event keys it made and any date it could not make. Each date also
sends event.created as it is made.
| Error | When |
|---|---|
404 chart_not_found |
No such floor plan in this organization |
422 chart_not_published |
Publish the floor plan first |
422 invalid_schedule |
The schedule is not valid; detail names the field |
422 too_many_dates |
The schedule makes more than 366 dates |
422 invalid_name |
Empty, over 120 characters, or holds {n}, {date} or {time} |
From an event you already set up
Send fromEventKey instead of name and chartKey to turn one event into a
run. The event becomes the first date, under its own key, and every other date
is a copy of it: its floor plan, prices, checkout, payment account, poster,
booking form, online codes and its own blocked seats. A copy carries none of the
event’s orders, holds, sold seats, reservations or tickets, and gets its own
key. This is how the dashboard’s New event sheet makes several dates: it
sets up the first one, then asks for the rest.
{ "fromEventKey": "lighthouse-keeper", "schedule": { "rule": "daily", "from": "2026-11-06", "until": "2026-11-19", "times": ["14:00", "17:30", "20:15"] } }The series takes the event’s name, zone and floor plan. A moment in the
schedule equal to the event’s own start is the event itself, so it is not made
again. name and timezone may be sent only when they match the event;
chartKey, venue and durationMinutes come from the event and are refused.
A dryRun lists every date the series would hold, the event included.
series.created lists the event first.
| Error | When |
|---|---|
404 event_not_found |
No such event in this organization and mode |
422 invalid_request |
A field the event already sets was sent with another value |
422 event_has_no_date |
The event has no start time yet |
422 event_in_season |
The event is a season date |
422 event_in_performance_group |
The event is in an active performance run |
422 event_in_series |
The event is already a date of a series, named by seriesKey (also while that series is still being made); add dates to it instead |
422 invalid_schedule |
The schedule adds no date to the event |
503 series_not_started |
The series did not answer, so whether it started is not known. The event stays with it: send the same request again, with the same Idempotency-Key, to carry on. A request with a new key finds that series, or makes a new one if the first never started |
409 create_abandoned |
This request’s series never started and was given up when another request came for the event. Its key answers this every time: send the request again with a new Idempotency-Key |
Follow the job
/v1/event-series/:keySecret keyAnswers the series, every date made so far in start order (each as
retrieveEvent describes it), and the latest job:
{
"series": { "seriesKey": "ser_7c1d…", "state": "ready", "dateCount": 42, "firstEventKey": "ev_3a9e…" },
"events": [{ "key": "ev_3a9e…", "name": "Hamlet", "startsAt": 1803911400000, "seriesKey": "ser_7c1d…" }],
"job": { "jobId": "ser_7c1d…:create", "state": "completed", "total": 42, "done": 42, "failed": 0, "failedDates": [] }
}state is creating while a job runs, then ready, or partial when some
dates could not be made. job.failedDates names each one and why, for example
insufficient_credits: every date of the series not made, whichever job it
failed in, so a later job does not hide it. Add a failed date again, or send
{ "retryFailed": true } to add dates, to make it again. The job is durable: if SeatLayer restarts part way, it
carries on where it stopped, and no date is made twice.
To list the dates as events, use GET /v1/events?seriesKey=ser_7c1d…. Every
event of a series carries seriesKey.
/v1/event-seriesSecret keyLists the series in the key’s mode, newest first (limit up to 100, cursor
for the next page).
Add dates
/v1/event-series/:key/datesSecret keySend either a schedule (as above) or exact moments:
{ "dates": ["2027-03-28T19:30", "2027-04-04T19:30"] }Each new date is a copy of the series’ first date, so a price you changed on it
is the price the new dates get. A moment the series already holds is left out;
one that failed to be made is made again. { "retryFailed": true } takes
every date that failed (up to 100). The answer is 202 {jobId, added}, and
series.dates_added says when the job ends.
The same Idempotency-Key answers the same job, even when its first answer was
lost after the job started. The 366-date cap holds when two calls add dates
at once.
| Error | When |
|---|---|
404 series_not_found |
No such series for this key and mode |
409 series_busy |
A change to every date is running; add the dates once it ends |
422 too_many_dates |
More than 100 new dates, or the series would pass 366 |
422 invalid_schedule |
Not valid, or every date is already in the series |
422 nothing_to_retry |
retryFailed, and no date of the series failed |
Change all dates
/v1/event-series/:keySecret keyChange every upcoming date at once. Send any of these:
| Field | What every upcoming date gets |
|---|---|
name |
The new series name: every upcoming date named Hamlet becomes Hamlet Live |
prices |
{categories}: only the categories, or a tiered category’s tiers, whose price changes, each shaped as PUT /v1/events/:key/pricing takes it. Every other price stays each date’s own |
sales |
on opens sales, off pauses them |
{ "name": "Hamlet Live", "prices": { "categories": [{ "key": "std", "price": 42 }] }, "sales": "off" }Dates already over are left alone. New prices apply to new orders; tickets
already sold keep the price they were sold at. A price you leave out keeps
each date’s own value, so a run whose weekend dates cost more than its
weekdays keeps the difference. A date you renamed on its own
keeps its name, but still takes the prices and sales. Opening sales runs each
date’s own go-live checks: a date that cannot open is listed in
job.failedDates with sales_not_ready, and the rest open. Anything else
stays per date, through the Events API.
A job makes the change, one date at a time. The answer is 200 when it ended
within the call, else 202; follow it with GET /v1/event-series/:key:
{
"series": { "seriesKey": "ser_7c1d…", "name": "Hamlet Live", "state": "ready" },
"jobId": "sju_5e0a…",
"job": { "jobId": "sju_5e0a…", "state": "running", "total": 41, "done": 12, "skipped": 0, "failed": 0 }
}The job is durable: a change that SeatLayer had to restart part way carries on,
and no date is changed twice. Send an Idempotency-Key, so a retry answers the
same job.
| Error | When |
|---|---|
409 series_busy |
Dates are still being made, or another change is running |
422 nothing_to_change |
No name, prices or sales was sent |
422 invalid_name |
Empty, over 120 characters, or holds {n}, {date} or {time} |
422 invalid_sales |
sales is not on or off |
422 invalid_event_pricing |
No category was sent, one is not on the floor plan or is sent twice, a tier is unknown, a tiered category is given one price, or a price is not valid |