/v1/series
A recurring series books one repeating pattern, weekly or monthly, as a run of ordinary meetings. This page covers reading, creating, and managing the series itself: pausing it, resuming it, ending it, editing its pattern, and moving it to a new host.
The occurrences a series creates are ordinary bookings, not a separate
resource. List them with GET /v1/bookings and a
series_id filter, and read or act on any one of them with the regular
booking endpoints, uid for uid.
A series' UID is its UUID, and every endpoint that takes :uid rejects a
malformed or unknown one with the same 404 series_not_found, so the
response never confirms which case it was.
GET /v1/series
List recurring series, narrowed to what you may see.
| Method | GET |
| URL | https://api.42min.us/v1/series |
| Scope | series:read |
| Auth | Required |
Query parameters
| Param | Type | Notes |
|---|---|---|
limit | integer | Default 20, max 100. |
cursor | string | Opaque cursor from a prior meta.next_cursor. |
status | string | Comma-separated. Allowed values: active, paused, ended. |
host_user_id | UUID | Filter to one host. |
event_type_id | UUID | Filter to one event type. |
What you see depends on your role: your own series as a user, the series of
the people you manage as a manager, and the whole organization as an admin
or owner. A host_user_id outside that set comes back as an empty page,
not an error, so it never confirms whether the host exists.
Response
{
"data": [
{
"uid": "01H…",
"version": 1,
"event_type": { "id": "01HXXX…", "slug": "weekly-lesson", "name": "Weekly lesson" },
"host": { "user_id": "01H…", "username": "ada", "email": "ada@example.com" },
"attendee": { "name": "Bob Builder", "email": "bob@example.com", "phone": null },
"frequency": "weekly",
"interval_weeks": 1,
"weekdays": ["mon", "wed"],
"time": "18:30",
"timezone": "Europe/Lisbon",
"starts_on": "2026-09-14",
"count": 24,
"status": "active",
"location_url": null,
"notify_invitee": true,
"source": "api",
"created_at": "2026-09-10T09:00:00.000Z",
"updated_at": "2026-09-10T09:00:00.000Z",
"ended_at": null
}
],
"meta": { "request_id": "req_…", "next_cursor": null, "has_more": false }
}
Field notes:
versionis the series' revision, bumped by every action on it. It's what you send back asIf-MatchonPATCH.sourcesays where the series came from:public(an invitee, from the booking page),internal(a host or admin, from the dashboard),api, ormcp.- Occurrences aren't included here; see
GET /v1/bookings?series_id={uid}.
curl
curl -H "Authorization: Bearer $TOKEN" \
"https://api.42min.us/v1/series?status=active&limit=50"
GET /v1/series/:uid
Return one series. Sets ETag for the revision.
| Method | GET |
| URL | https://api.42min.us/v1/series/{uid} |
| Scope | series:read |
| Auth | Required |
Headers (response)
ETag: "3"
Response
Same shape as a list item.
curl
curl -i -H "Authorization: Bearer $TOKEN" \
https://api.42min.us/v1/series/01H…
Common errors
404 series_not_found: unknown UID, or the UID is not a UUID (collapsed into the same 404 to avoid leaking format details).
POST /v1/series
Create a recurring series: books up to count occurrences of one repeating
pattern in a single call.
| Method | POST |
| URL | https://api.42min.us/v1/series |
| Scope | series:write |
| Auth | Required |
Requires an Idempotency-Key. This sends real
invitee email and writes to a real calendar, so retry a failed attempt with
the same key rather than reissuing the call.
Headers
Authorization: Bearer <token>
Content-Type: application/json
Idempotency-Key: <unique-per-operation>
Body
Either event_type_id or (username + event_slug) is required.
{
"host_user_id": "8b7c1e6f-1111-4222-8333-444455556666",
"event_type_id": "1c2d3e4f-5555-4666-8777-888899990000",
"attendee": {
"email": "ada@example.com",
"name": "Ada Lovelace"
},
"frequency": "weekly",
"interval_weeks": 1,
"weekdays": ["mon", "wed"],
"time": "18:30",
"timezone": "Europe/Lisbon",
"starts_on": "2026-09-14",
"count": 24
}
Field reference:
| Field | Type | Notes |
|---|---|---|
host_user_id | UUID | Required. Whose calendar the series is booked on; must be someone you may act for. |
event_type_id | UUID | One of event_type_id or usernameevent_slug required. |
username / event_slug | string | Alternative to event_type_id. |
attendee.email | string | Required. |
attendee.name | string | If omitted, built from first_name + last_name, else falls back to email. |
attendee.first_name / last_name | string | Optional. |
attendee.phone | string | Optional. |
frequency | string | Required. weekly or monthly_same_day. |
interval_weeks | integer | 1 to 8, default 1. Weekly patterns only. |
weekdays | array | Required for a weekly pattern: up to 7 of sun…sat. Ignored for monthly_same_day. |
time | string | Required. HH:MM, 24-hour, the wall clock in timezone. |
timezone | IANA tz | Optional. Defaults to the host's own zone. |
starts_on | date | Required. YYYY-MM-DD, the first civil day the pattern is walked from. Past instants are never booked. |
count | integer | Required, 1 to 42. Dates the host isn't free on are skipped, not shifted, so a series can come back with fewer occurrences than requested. |
location_url | string | Optional. A fixed http(s) link every occurrence carries, instead of a generated conference link. |
notify_invitee | boolean | Optional, default true. false means no mail reaches the attendee for this series. |
Response
201 Created:
{
"data": {
"series": { "uid": "01H…", "version": 1, "…": "…" },
"occurrences": [
{ "uid": "01H…", "start_at": "2026-09-14T17:30:00.000Z", "end_at": "2026-09-14T18:00:00.000Z", "status": "confirmed" }
],
"skipped": []
},
"meta": { "request_id": "req_…" }
}
Field notes:
occurrenceslists what was actually booked, in order. Eachuidis an ordinary booking UID, usable on every/v1/bookingsroute.skippedlists pattern dates that weren't free (reason: "conflict"); the series is still created with the rest. Compare its length againstcountto know whether every occurrence was booked.
curl
curl -X POST https://api.42min.us/v1/series \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"host_user_id": "8b7c1e6f-1111-4222-8333-444455556666",
"event_type_id": "1c2d3e4f-5555-4666-8777-888899990000",
"attendee": {"email": "ada@example.com", "name": "Ada Lovelace"},
"frequency": "weekly",
"weekdays": ["mon", "wed"],
"time": "18:30",
"starts_on": "2026-09-14",
"count": 24
}'
Common errors
400 validation_error: required field missing or malformed.400 attendee_email_invalid: bad email format.400 missing_idempotency_key: header missing.403 host_not_allowed:host_user_idis outside who you may act for.404 event_type_not_found: bad id/slug.409 recurring_disabled: the event type's recurring mode isoff.409 no_dates_free: no date in the pattern was free, so nothing was created.422 count_out_of_range:countis outside the allowed range.503 availability_unavailable: the host's conflict calendar couldn't be read, so nothing was booked on a guess. Retry with the SAMEIdempotency-Key, honoringRetry-After.
POST /v1/series/:uid/pause
Cancel every future occurrence and record each as owed, so a later resume
books a replacement for it. Occurrences the invitee already moved on their
own are left alone. Nothing is emailed to the attendee.
| Method | POST |
| URL | https://api.42min.us/v1/series/{uid}/pause |
| Scope | series:write |
| Auth | Required |
No body. Safe to repeat: a re-run holds whatever the first run didn't reach.
Response
200 OK:
{
"data": { "held": 6, "superseded": false },
"meta": { "request_id": "req_…" }
}
held is how many occurrences this call canceled and recorded as owed. See
Superseded and busy responses below for
what superseded: true means.
curl
curl -X POST https://api.42min.us/v1/series/01H…/pause \
-H "Authorization: Bearer $TOKEN"
Common errors
404 series_not_found409 series_busy: another action holds the series right now (for example, its confirmation email is still sending). Read it again and retry.409 series_ended: terminal, nothing can change it.
POST /v1/series/:uid/resume
Book one replacement for each occurrence a pause is holding, and reactivate the series.
| Method | POST |
| URL | https://api.42min.us/v1/series/{uid}/resume |
| Scope | series:write |
| Auth | Required |
Requires an Idempotency-Key. If-Match is
optional here: send the ETag from a prior GET to refuse a resume that
another change overtook, or omit it to resume whatever the series is now. A
value that doesn't parse as a valid revision answers 400 invalid_if_match.
Response
200 OK:
{
"data": { "created": 4, "skipped": [], "owed": 0, "superseded": false },
"meta": { "request_id": "req_…" }
}
Field notes:
createdis how many replacement occurrences this call booked.skippedlists dates that weren't free (reason: "conflict"); those stay owed.owedis how many occurrences are still waiting for a replacement. Above zero means the series is stillpaused, and callingresumeagain with the current revision converges on the rest.
curl
curl -X POST https://api.42min.us/v1/series/01H…/resume \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)"
Common errors
400 invalid_if_match:If-Matchwas sent but isn't a valid revision.400 missing_idempotency_key404 series_not_found409 series_busy/409 series_ended503 availability_unavailable: retry with the SAMEIdempotency-Key, honoringRetry-After.
POST /v1/series/:uid/end
Cancel every future occurrence for good and send the attendee one notice listing them. Nothing is owed afterward, and the series can never be resumed. Past occurrences are untouched.
| Method | POST |
| URL | https://api.42min.us/v1/series/{uid}/end |
| Scope | series:write |
| Auth | Required |
Requires an Idempotency-Key. Safe to repeat: a
re-run cancels whatever the first run didn't reach.
Response
200 OK:
{
"data": { "canceled": 5, "superseded": false },
"meta": { "request_id": "req_…" }
}
curl
curl -X POST https://api.42min.us/v1/series/01H…/end \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)"
Common errors
400 missing_idempotency_key404 series_not_found409 series_busy/409 series_ended
POST /v1/series/:uid/host
Move every future occurrence, then the series itself, to another host.
| Method | POST |
| URL | https://api.42min.us/v1/series/{uid}/host |
| Scope | series:write |
| Auth | Required |
Body
{ "host_user_id": "9c8d2e7a-2222-4333-9444-555566667777" }
| Field | Type | Notes |
|---|---|---|
host_user_id | UUID | Required. The new host; must be someone you may act for. |
Response
200 OK:
{
"data": { "moved": 5, "unchanged": 0, "stopped_by": null, "superseded": false },
"meta": { "request_id": "req_…" }
}
A transfer that can't complete (no writable calendar on the new host, or
another action taking the series over) stops the run: moved says how many
occurrences did move, and stopped_by names the reason. stopped_by: null
means every occurrence moved and the series' own host changed too; anything
else means it didn't, and the call can be repeated once the cause is fixed.
curl
curl -X POST https://api.42min.us/v1/series/01H…/host \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"host_user_id":"9c8d2e7a-2222-4333-9444-555566667777"}'
Common errors
400 validation_error403 host_not_allowed: the new host is outside who you may act for.404 series_not_found409 series_busy/409 series_ended
PATCH /v1/series/:uid
Edit a series' pattern, its fixed link, or whether the attendee is notified. Requires optimistic locking.
| Method | PATCH |
| URL | https://api.42min.us/v1/series/{uid} |
| Scope | series:write |
| Auth | Required |
If-Match is required: the ETag from a prior GET of this series.
Omitting it returns 428 missing_if_match and nothing is changed. Unlike
PATCH /v1/bookings/:uid, this route does not also require an
Idempotency-Key.
Headers
Authorization: Bearer <token>
Content-Type: application/json
If-Match: "3"
Body
Send only what changes.
{ "weekdays": ["tue", "thu"], "time": "09:00" }
| Field | Type | Notes |
|---|---|---|
frequency | string | weekly or monthly_same_day. |
interval_weeks | integer | 1 to 8. |
weekdays | array | Up to 7 of sun…sat. |
time | string | HH:MM, 24-hour. |
location_url | string | Replaces the series' existing fixed link. A series created without one can't gain one here, and sending null to clear it is refused as a validation error rather than treated as "remove it". |
notify_invitee | boolean | Changes whether future sends reach the attendee. |
starts_on, count, the attendee, and the event type are fixed for the
life of a series and can't be patched.
Changing frequency, interval_weeks, weekdays, or time holds every
future occurrence the new pattern no longer generates and books a
replacement for each, reported under resumed. Changing only
location_url or notify_invitee restamps the existing occurrences in
place and leaves every date alone, so resumed is null.
Response
200 OK, with headers including ETag: "<new-version>":
{
"data": {
"series": { "uid": "01H…", "version": 2, "…": "…" },
"resumed": { "created": 3, "skipped": [], "owed": 0, "superseded": false }
},
"meta": { "request_id": "req_…" }
}
curl
curl -X PATCH https://api.42min.us/v1/series/01H… \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "If-Match: \"3\"" \
-d '{"weekdays":["tue","thu"],"time":"09:00"}'
Common errors
400 validation_error: malformed field, such as a badtimeformat.404 series_not_found409 series_busy/409 series_ended409 version_conflict:If-Matchdoesn't match the currentversion. Re-GETand retry.422: a well-formed but semantically rejected change, such as sendinglocation_url: nullto clear an existing link.428 missing_if_match: header missing.
Superseded and busy responses
Every action above can be overtaken by another one running on the same
series at the same time: two requests pausing a series at once, or an
end landing while a resume is still booking replacements. Two different
things can happen:
409 series_busymeans the call couldn't get started at all, because something else currently holds the series (mid-send confirmation email, or another action's very first write). Nothing changed. Read the series again and retry.superseded: truein a200response means the call did start, and its own work up to that point is real and already committed, but another action took the series over partway through, so it stopped early. The counts in the response (held,created,canceled,moved) are what this call actually finished, not what it was asked to do. Read the series again and repeat the same call; it converges on whatever is left.
change_host reports the same situation through stopped_by instead of
superseded, since it already has a field for "the run didn't finish."
This matters most for resume and a pattern-changing PATCH, both of which
can need several occurrence inserts to finish one call.
Last updated September 10, 2026.