HTTP API
HTTP API
Most of what the site and the /admin pages do is also available as JSON over HTTP, under
/api/v1. Use it to script event setup — events, days, locations, guests, sessions — or to connect
another tool to sessions, proposals, RSVPs and votes. The v1 in the path is the API’s version.
The OpenAPI description
Every endpoint, with its parameters, request bodies and responses, is described in an OpenAPI 3.1 document. The site serves the document of the version it runs:
/api/v1/docsis an interactive reference. Read the endpoints and send requests from the browser; they carry your login cookies, so they act as you. Each endpoint shows which cookie it needs; log in at/adminfirst for the/api/v1/admin/ones./api/v1/openapi.jsonis the document itself, for another OpenAPI viewer or a client generator.
Both need the same login as the rest of /api/v1 (see below). To stop serving them, set
SB_API_DOCS=false (Configuration). The document is also in the repository, at
packages/contracts/openapi.json in the tag of each release.
Conventions
/admin/is about privilege. An endpoint under/api/v1/admin/does something the plain one, where one exists, does not (any phase, on behalf of hosts), not only for a different caller. Today it needs a site admin; the role shows in the endpoint’s description, never in its path, so a later organizer role fits without a change.POSTwith a verb is a command that is not a plain create, update or removal of one resource (such asaccept,decline,cancel,move,assign,removeandread). Removing one resource isDELETE.- Events:
{id}or{slug}. Resources use the event’sid; the schedule (snapshot and feed) uses theslug, the address people see. Neither changes after the event is created. /attendeesand/guests.GET /api/v1/attendeeslists every guest of the site;GET /api/v1/guests?eventId=…lists one event’s guests.Authorizationis reserved for API tokens of the formBearer sb_…. The server ignores the header today and never readsBasic, so a reverse proxy with basic authentication may forward it.- Cookie names and values are not part of the API. Use a cookie jar.
- Finding an event’s ID. Endpoints that take an
eventIdneed it.GET /api/v1/eventslists the events andGET /api/v1/events/{id}reads one with its days; neither needs a site admin.
Signing in
A request carries the same cookies the site sets in a browser: the site password’s and the login session’s. There are no API tokens yet; they are planned.
- Site password. When
SITE_PASSWORDis set, every/api/v1endpoint outside/api/v1/admin/needs the site cookie.DELETE /api/v1/auth/siteclears it and ends the login session. - Site admin.
/api/v1/admin/needs the login session of a site admin, and nothing else: the site password is not asked for there. Without a session these endpoints answer401, and403for a guest who is not a site admin.
Scripts sign in as a site admin with a username and password (the user’s page in the admin UI sets them), then send the cookies on each request:
SITE=https://sessions.example.org
curl -c cookies.txt -H 'content-type: application/json' \
-d '{"password": "your site password"}' \
"$SITE/api/v1/auth/site"
curl -b cookies.txt -c cookies.txt -H 'content-type: application/json' \
-d '{"identifier": "your-username", "credential": "your password"}' \
"$SITE/api/v1/auth/login"
curl -b cookies.txt "$SITE/api/v1/admin/events"
Endpoints that act as an attendee — RSVPs, votes, your own profile and notifications — act as the name chosen in the browser, and are meant for the site itself until API tokens arrive. The cookies are only sent over HTTPS in production.
Admin endpoints refuse a request that a browser sends from another website; scripts, which send no
Origin header, are not affected.
Compatibility
These rules bind /api/v1, the snapshot and the feed from release 5.0.0.
Not breaking, in any release: a new endpoint, method, query parameter or optional request field;
a new response field; a new value of an enum a response already has; a new problem code; a new
feed change type; a new error status for a new condition; wider access; a changed rate limit; a
different detail text; an ordering that was not documented.
Breaking: removing or renaming an endpoint, field, enum value or problem code; making an
optional field required or a nullable one non-null; changing a type, an id format or what a field
means; narrowing access; changing the meaning of an existing status; ending a documented behaviour.
A break comes as a new major path, /api/v2, served beside /api/v1 for at least one more major
release.
Deprecation. An endpoint or field to be removed is marked deprecated in openapi.json, named
in the changelog, answered with Deprecation and Sunset headers, and kept for at least one major
release. SITE_PASSWORD follows the same rule: once it is deprecated it logs a warning and keeps
working until a major release removes it.
Clients must tolerate unknown response fields; unknown enum values (treat them as “other”);
unknown problem codes (fall back to the status); unknown feed types (ignore them, and read the
snapshot again on the next seq gap); a reference to a guest that no longer exists; and nullable
fields becoming set. Do not parse detail. The server never adds a feed type whose data an existing
type must now also carry.
Self-hosters. Environment variables keep their names and meaning within a major release; a new
one is prefixed SB_, and the existing unprefixed names stay. /api/health is not versioned and
never changes meaning. The data paths (DATABASE_URL, /data/uploads) and the URLs people hold,
such as /join/{token}, /auth/reset and event addresses, keep working for the life of the data.
So do /api/v1/docs, /api/v1/openapi.json, /legacy/… (old links keep redirecting), and
/media/… and /locations/… file addresses.
Errors
A refused request answers with a problem details body (RFC 9457, application/problem+json):
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"code": "session.full",
"detail": "This session is full"
}
Branch on code. Once released, a code keeps its meaning; title and detail are English text for
people and may change. A request that does not match the endpoint’s description gets 400 with
request.invalid and an errors list naming each field and what is wrong with it; a few endpoints
attach the same list to their own codes, such as guest.invalid.
| Status | Means |
|---|---|
400 |
The request is invalid (request.invalid, or a field’s code) |
401 |
Not signed in (site.unauthenticated, admin.unauthenticated) |
403 |
Not allowed (admin.required, guest.protected, …) |
404 |
Not found |
409 |
Conflicts with the current state (session.full, …) |
422 |
An Idempotency-Key reused for a different request |
429 |
Too many requests (rate.limited); see Rate limits |
500 |
An unexpected error on the server (server.error) |
503 |
A service the site relies on failed, such as the mail server |
Rate limits
Each caller may send 300 requests at once, then 5 a second: a site admin counts as one caller, each
attendee signed in to a protected name as another, and everyone else by their address (see
Behind a reverse proxy). A request past the limit gets
429 with rate.limited and a Retry-After header saying how many seconds to wait before
retrying. /api/v1/health is never limited.
Retrying safely: Idempotency-Key
A request that changes something (POST, PUT, PATCH, DELETE) may carry an Idempotency-Key
header. Send a new random value, such as a UUID, for each change you mean to make, and the same
value again when you retry it — after a timeout or a dropped connection, for example. The change is
then made once.
- A retry after the first request finished gets the first response again, without repeating the change.
- A retry while the first is still running gets
409withidempotency.inProgress; wait and retry. If the first never finishes, for instance because the server restarted, a retry after a minute runs it again. - The same key with a different method, path or body gets
422withidempotency.keyReused. - An answer of
500or above is not kept, so a retry runs the request again.
Keys are kept for 24 hours and are separate for each signed-in user, so two scripts cannot collide on a key. A key must be 1 to 255 characters. A request with neither a site admin nor a selected attendee has its key ignored.
Caching
Every response is sent with cache-control: no-store: browsers and proxies never keep it, so a read
always shows the current state.
Editing what someone else may be editing
Sessions, proposals, events, days, locations, meeting points and guests carry a version, which
goes up with every change. An edit (PUT) sends the version it was made from as expectedVersion,
and so does moving a location in the order; a photo upload sends it as a form field, a photo removal
as a query parameter. An event’s settings, phase dates and 1-on-1 settings share the event’s
version. A guest has two: the profile’s (its fields, photo and name) and the admin view’s (name,
email, protection and events). A location’s version and a guest’s admin version also go up when it
joins or leaves an event, and a location’s when it changes places. If someone changed the item
since, the edit is refused with 409 and <item>.versionConflict (session, proposal, event,
day, location, meetingPoint, guest, or profile for your own profile), and the body’s
current member holds the item as it is now: redo the edit on it and send again with its version.
Nothing is overwritten unseen.
Syncing from a script
A script that keeps an event in step with an outside plan, such as a list of shifts, reads what is there first and then changes only what differs. As a site admin it can read every endpoint it needs:
- Check times before booking.
GET /api/v1/events/{id}answers the event’s settings and its days, so the script can check session times first. - Read the whole schedule at once.
GET /api/v1/events/{slug}/snapshotanswers the event, its days, rooms, guests and sessions in one response, with RSVP counts by session and, for a guest, their own RSVPs and meetings. - Follow changes as they happen.
GET /api/v1/events/{slug}/feed?since=…streams the changes after the snapshot’sseqas server-sent events, limited to what the guest cookie may see. It ends after 10 minutes; reconnect with the lastidasLast-Event-ID. Anevent: resetmessage means the position is too old: read the snapshot again. - Find a location before adding it.
GET /api/v1/admin/locationslists every location, hidden ones included. Location names need not be unique, so the script decides what counts as the same location, then creates the missing ones withPOST /api/v1/admin/locations. Upload a location’s image withPUT /api/v1/admin/locations/{id}/image(a form withimageandexpectedVersion) and remove it withDELETE /api/v1/admin/locations/{id}/image?expectedVersion=…. - Find the existing sessions.
GET /api/v1/sessions?eventId=…lists the event’s sessions, with their rooms, hosts (id and name only) and capacity, one page at a time (see Long lists); sort them by start yourself. - Update a session.
PUT /api/v1/admin/sessions/{id}replaces the session: send every field, taking the ones you do not change from the read, and itsversionasexpectedVersion. A field left out gets400, so nothing is reset by accident. - Warn before deleting. Each session in the list has
numRsvps; show it beforeDELETE /api/v1/admin/sessions/{id}.
Send an Idempotency-Key with each change, so retrying one after a dropped connection does not
create a second location or session.
Long lists
Lists that grow with an event (sessions, proposals, guests, comments and your notifications) come in
pages. Ask for up to limit items (1 to 1000, default 100); the answer is
{ "items": […], "nextCursor": "…" }. Pass nextCursor as cursor to get the next page, until it
is null. Each list keeps a fixed order, given in the reference. An item deleted between two pages
moves nothing; one changed meanwhile may show up twice or not at all, so read the whole list again
when that matters. A cursor that does not come from the server is refused with 400 and
cursor.invalid, and so is one from another list, such as another event’s sessions.
The TypeScript client
The repository holds a typed client, @schellingboard/api-client in packages/api-client: types
generated from openapi.json and a small wrapper around
openapi-fetch. It is not published to npm; use it from a
checkout of the release you run, or generate a client in any language from openapi.json.
import { createApiClient } from "@schellingboard/api-client";
const api = createApiClient({
baseUrl: "https://sessions.example.org",
headers: { cookie: adminCookie },
});
const { data, error } = await api.GET("/api/v1/events");
if (error) console.error(error.code);