Events lifecycle¶
Events are scheduled, live learning — workshops, all-hands, office hours. Unlike on-demand content, people don't get assigned to an event; they enroll. This guide walks the full arc: define a reusable series, schedule a concrete event, enroll attendees, and check them in on the day.
How the pieces fit¶
graph LR
ES[Event series<br/>template] -->|defaults flow into| EV[Event<br/>scheduled occurrence]
EV -->|has one or more| TS[Timeslots]
U[User] -->|enrolls in| EV
EN[Enrollment] -->|check in on the day| CI[checkin_datetime]
- An event series is an optional template carrying default settings — location, capacity, in-person/online modes. Create it once, reuse it for every occurrence of a recurring event.
- An event is a concrete scheduled occurrence with one or more timeslots (the actual sessions). An event can belong to a series (inheriting its defaults) or stand alone.
- An enrollment links a user to an event, with an attendance method and check-in state.
1. (Optional) Create an event series¶
If this is a one-off, skip to step 2. For anything recurring, define the template first so every event shares the same defaults:
curl -X POST https://acme.plusplus.app/api/v2/event-series/ \
-H "Authorization: Bearer pp_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Weekly Engineering Sync",
"duration": 60,
"location_id": 42,
"is_local": true,
"is_online": true,
"enrollment_limit": 50,
"online_enrollment_limit": 200
}'
{
"public_id": "6ba7b810-…",
"name": "Weekly Engineering Sync",
"is_local": true,
"is_online": true,
"enrollment_limit": 50,
"online_enrollment_limit": 200,
"location": { "id": 42, "name": "San Francisco HQ", "timezone": "America/Los_Angeles" }
}
location_id is the numeric id of a location (the one place in the API you reference something by a numeric id rather than a public_id). Capacity is per occurrence: enrollment_limit / wait_list_limit govern in-person seats, and the online_* variants govern online seats. 0 means unlimited.
2. Schedule an event¶
An event needs at least one timeslot. Link it to the series with event_series_id (or omit it for a standalone event):
curl -X POST https://acme.plusplus.app/api/v2/events/ \
-H "Authorization: Bearer pp_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Weekly Engineering Sync — Mar 18",
"event_series_id": "6ba7b810-…",
"timeslots": [
{
"starts_at": "2026-03-18T17:00:00Z",
"duration": "PT1H",
"rooms_info": "Room 3A, Building 2",
"watch_link": "https://meet.acme.com/eng-sync"
}
]
}'
{
"public_id": "c8a7f1e2-…",
"name": "Weekly Engineering Sync — Mar 18",
"starts_at": "2026-03-18T17:00:00Z",
"ends_at": "2026-03-18T18:00:00Z",
"event_series": { "public_id": "6ba7b810-…", "name": "Weekly Engineering Sync" },
"timeslots": [
{
"id": 42,
"starts_at": "2026-03-18T17:00:00Z",
"ends_at": "2026-03-18T18:00:00Z",
"duration": "PT1H",
"rooms_info": "Room 3A, Building 2",
"watch_link": "https://meet.acme.com/eng-sync"
}
]
}
A few timeslot details:
durationis an ISO-8601 duration string:PT1H,PT1H30M,PT90M.starts_at— send the local wall-clock time the session begins, labeled as UTC (Z). The event's location supplies the real timezone. See the events module conventions in the API reference for the full rationale.- The event's top-level
starts_at/ends_atare computed from its timeslots — you don't set them directly.
What the event inherits from its series¶
Everything you leave out of the create request is taken from the series, so the request above needs only a name and a timeslot to produce an event that matches one scheduled from the series in the web app:
Fields you can override by sending them:
| Inherited | Notes |
|---|---|
body_markdown |
|
tags |
Send "tags": [] to create the event without any. |
location_id |
Send "location_id": null to create the event without a location. |
enrollment_limit, wait_list_limit, online_enrollment_limit, online_wait_list_limit |
|
is_local, is_online, is_hidden |
|
external_link |
|
The series' duration |
Applied to any timeslot that doesn't set its own duration. |
Sending any of these overrides the series — including sending null, which is read as a decision rather than an omission.
Inherited with no way to override, because this endpoint has no field for them:
| Inherited | Notes |
|---|---|
cover image |
The series is the only way to give an event a cover image. |
| Facilitators | Organizer, co-organizers and presenters are copied onto the event. Adjust afterwards via the facilitators sub-resource. |
| Channel | The event lands in the series' channel, which also governs who can see it. |
| Survey relationships | Copied as new rows; their schedule is recomputed from the event's own timeslots. |
external_link_description |
Inheritance happens once, at creation: later edits to the series don't reach events already scheduled from it.
:::note
Inheritance only applies to POST /events/. PATCH /events/{id}/ changes exactly the fields you send, so moving an existing event into a series does not backfill anything.
:::
To list the events generated from a series, filter by it:
curl "https://acme.plusplus.app/api/v2/events/?event_series_id=6ba7b810-…&ordering=created" \
-H "Authorization: Bearer pp_your_token_here"
ordering supports name, created, and modified (prefix - for descending). To present events chronologically, sort by each event's computed starts_at client-side.
3. Enroll attendees¶
Enroll a user by referencing the event and an attendance method:
curl -X POST https://acme.plusplus.app/api/v2/enrollments/ \
-H "Authorization: Bearer pp_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"user_id": "8f3a…",
"event_id": "c8a7f1e2-…",
"attendance_method": "in_person",
"accept_waitlist": true
}'
{
"public_id": "d4e5f6a7-…",
"user": { "public_id": "8f3a…", "name": "Jane Doe", "email": "jane.doe@acme.com" },
"event": { "public_id": "c8a7f1e2-…", "name": "Weekly Engineering Sync — Mar 18" },
"status": "enrolled",
"attendance_method": "in_person",
"checkin_datetime": null
}
Behavior worth knowing:
attendance_methodisin_personoronline; capacity is tracked separately for each.accept_waitlist— when the chosen method is full, set thistrueto take a waitlist spot (status: "waitlisted") instead of getting a409.- Create-or-get:
201for a new enrollment,200if an identical active enrollment already exists. A409means the method is full (and you didn't accept the waitlist), or an active enrollment exists with a different method.
To enroll a whole group at once, use the bulk endpoint — see Bulk operations & async jobs.
4. Check in on the day¶
When the attendee shows up, record it:
curl -X POST https://acme.plusplus.app/api/v2/enrollments/d4e5f6a7-…/checkin/ \
-H "Authorization: Bearer pp_your_token_here"
Check-in is idempotent — checking in someone already checked in returns 200, not an error — so it's safe to retry or wire to a "scan badge" button. It returns 409 only if the enrollment was dropped.
5. Handle drops and cancellations¶
curl -X POST https://acme.plusplus.app/api/v2/enrollments/d4e5f6a7-…/drop/ \
-H "Authorization: Bearer pp_your_token_here"
drop sets status: "dropped". It returns 409 if the attendee has already been checked in — you can't un-attend someone. When a waitlisted spot opens up because of a drop, promote a waitlisted enrollment as your business rules require.
6. After the event: recordings¶
Captured the session? Attach the recording to the series so it plays back for everyone, via the Mux upload flow. That's its own walkthrough — see Uploading recordings.
Common pitfalls¶
- Trying to assign an event. Events use enrollments, not assignments. There's no assignment endpoint for events.
- Setting
starts_aton the event. It's derived from timeslots — schedule the timeslot and the event's start/end follow. - Forgetting a timeslot. An event create requires at least one timeslot; an event with no session can't be scheduled.
- Mismatched online/in-person limits. In-person and online seats are independent. A full room doesn't block online enrollment, and vice versa.
- Deleting an event referenced by a track.
DELETEreturns409if a track references the event. Remove it from the track first.