> ## Documentation Index
> Fetch the complete documentation index at: https://apidocs.sessionboard.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> What's new in the Sessionboard Public API

Release notes for the Sessionboard Public API. Entries are grouped by month (newest first). Breaking changes are called out explicitly.

> [!NOTE]
>
> API tokens created before scoped tokens were introduced continue to work with full access when no scopes are set. New tokens should request only the scopes your integration needs — see [Authentication](https://apidocs.sessionboard.com/authentication.md) and [OAuth](https://apidocs.sessionboard.com/oauth.md).

***

## October 2026

### October 21 — Session files: approval status, and held files have no URL

**Documented — `approval_status` and `embargo_until` on session files.** On events using Approvals (Early Access), the Session Files endpoints (`GET/POST/PATCH /v1/event/{eventId}/sessions/{sessionId}/files…`) return `approval_status` (`not_required`, `pending`, `approved`, `denied`, `superseded` or `embargoed`) and `embargo_until` on each file. A file Approvals holds back is still listed but has `url: null` until it is approved and released. Publish only files whose `approval_status` is `approved` or `not_required`, and remove a file you already published when it moves to any other status. See [Session files](https://apidocs.sessionboard.com/api-reference/session-files.md).

**Changed — simple upload checks the real file type against the upload slot.** `POST /v1/event/{eventId}/sessions/{sessionId}/files/upload` with a `content_slot_id` now reads the file's bytes, as the presigned `…/complete` route already did. A file whose content does not match the slot's accepted types (for example a PNG renamed `.pdf` into a PDF-only slot) returns `400` naming the slot's types and the file's real type, and nothing is stored.

### October 21 — Invalid URLs in URL fields return 400

**Changed — `200` becomes `400` for a URL the field cannot store.** Writing a URL custom field (for example on `PUT /v1/event/{eventId}/contacts/{contactId}`) with a value that is not an `http(s)` URL, or an `http://` URL on a field set to require `https`, used to answer `200` and store `null`, wiping the previous value. It now returns `400 TYPE_MISMATCH` and leaves the stored value unchanged. Handle `TYPE_MISMATCH` on these writes: correct the URL, or omit the field, before retrying.

### October 21 — Too-long and malformed values return 400

**Changed — `500` becomes `400` for values the database rejects.** Create, update and bulk write endpoints used to answer `500 INTERNAL_ERROR` when a value could not be stored. They now return `400`: `VALUE_TOO_LONG` (with `details.max_length`) for text longer than its field allows, and `VALIDATION_ERROR` for a malformed id or an out-of-range number. Bulk requests report the same codes per operation. `GET /v1/logs/audit-trail` with an unknown `subject_type` now returns `400 VALIDATION_ERROR` naming the allowed values instead of `500`. Requests that used to succeed are unaffected.

> [!NOTE]
>
> This arrives on the production hosts (`public-api.sessionboard.com`, `public-api-eu.sessionboard.com`) with the next Public API release.

### October 20 — Ask a question about an event (rolling out)

Additive. Nothing existing changes shape.

**Added — `POST /v1/event/{eventId}/ask`.** Ask a plain-language question about one event and get an answer written for the organizer, grounded in the event's program, session transcripts, knowledge and pinned answers and the Help Center. The response carries `answer` (Markdown with `[n]` markers), `citations` (one card per source record), `actions` (always empty — the endpoint is read-only) and a `conversation_id` to send back for a follow-up. Questions are up to 500 characters. An answer can take up to 90 seconds, so set your client timeout accordingly. Errors use `{ error, message, details }`: `422 VALIDATION_ERROR`, `429 RATE_LIMITED` (with `Retry-After`), `429 QUOTA_EXCEEDED`, `403 FORBIDDEN` when Ask or AI features are off. The API request log records these calls with the question redacted. See [Ask a question about an event](https://apidocs.sessionboard.com/api-reference/ask/ask-a-question-about-an-event.md).

**Added — `read:ask` scope.** Required for Ask, and granted only explicitly: legacy tokens with no scopes get `403 SCOPE_REQUIRED`. The token's event allow-list and the organization's AI features switch apply. See [Authentication](https://apidocs.sessionboard.com/authentication.md) and [OAuth](https://apidocs.sessionboard.com/oauth.md).

**Added — `ask_event` MCP tool.** The same Ask from Claude, ChatGPT and other assistants, with its sources listed and the `conversation_id` to continue. Needs `read:ask`; reconnect an assistant connected earlier to grant it. See [MCP Server](https://apidocs.sessionboard.com/insights/mcp-server.md#asking-about-an-event).

> [!NOTE]
>
> This is rolling out. It arrives on the production hosts (`public-api.sessionboard.com`, `public-api-eu.sessionboard.com`) and the MCP server with the next Public API release.

### October 20 — Documentation: date-only custom fields and log export reasons

Documentation only. Nothing changes shape.

* **Date-only custom field format documented.** `date` custom field values serialize as `M-DD-YYYY` (for example `9-22-2026`, `3-04-2026`), not ISO 8601. The format is unchanged; it is now stated on `CustomFieldValue` and on `GET /v1/events?expand=custom_fields`.
* **Log export authentication reasons corrected.** `GET /v1/logs/api-requests` only serves authentication failures that can be tied to your organization. `missing_token` and `invalid_token` are never exported; `owner_inactive` and `event_not_allowed` were missing from the list.

### October 10 — When a participant's acceptance invitation was sent

Additive. Nothing existing changes shape.

* **`acceptance_invite_sent_at` on session `participants`.** On events with Participant Acceptance turned on, each entry in a session's `participants` now says when that role's acceptance invitation was last sent, or `null` if it hasn't been sent. It sits next to `acceptance_status`, which is now documented too. The legacy `speakers`, `chairpersons` and `moderators` arrays don't carry it.

> [!NOTE]
>
> This arrives on the production hosts (`public-api.sessionboard.com`, `public-api-eu.sessionboard.com`) with the next Public API release.

### October 10 — Contact writes accept about, salutation and address

Additive. Nothing existing changes shape.

**Added — more standard fields on contact writes.** `PUT /v1/event/{eventId}/contacts/{contactId}`, `POST /v1/event/{eventId}/contacts/create` and bulk contact writes now save `about`, `salutation`, `honorific`, `address_line_1`, `address_line_2`, `address_city`, `address_state`, `address_postal_code` and `address_country`, using the names GET returns. These keys were previously dropped without an error. `address_country` accepts the numeric code GET returns, an alpha-2/alpha-3 code or the English name; an unknown value returns `400`. Write responses now echo the stored values, including `title`.

**Added — `reason` on the log export `403`.** A log export token calling any non-log endpoint gets `"reason": "log_export_only"` in the `403` body.

> [!NOTE]
>
> This arrives on the production hosts (`public-api.sessionboard.com`, `public-api-eu.sessionboard.com`) with the next Public API release.

### October 10 — API tokens stop working when their creator leaves the organization

**Changed — a token follows its creator's membership.** Requests with an API token (`x-access-token`) whose creator has been removed from the organization or set to Inactive now return `401`, and appear in `/v1/logs/api-requests` with the reason `owner_inactive`. Reactivating or re-adding the person restores their tokens. OAuth sessions already ended this way. If an integration runs on a departing colleague's token, replace it with one created under an account that stays.

> [!NOTE]
>
> This arrives on the production hosts (`public-api.sessionboard.com`, `public-api-eu.sessionboard.com`) with the next Public API release.

### October 10 — Log export: the `stream` values

Documentation only. Nothing about the API changed.

**Corrected — `stream` on a log event.** It is `api_request` on `/v1/logs/api-requests`, `security_event` on `/v1/logs/security-events` and `audit` on `/v1/logs/audit-trail`. The [SIEM guide](https://apidocs.sessionboard.com/guides/exporting-logs-to-a-siem.md) example and the `LogEvent` schema showed the URL segment (`audit-trail`) instead, which a SIEM filter would never match.

### October 8 — Session files can be uploaded into an upload slot

Additive. Nothing existing changes shape.

* **`content_slot_id` on `POST /v1/event/{eventId}/sessions/{sessionId}/files` and `.../files/upload`.** Name the [upload slot](https://apidocs.sessionboard.com/api-reference/session-files/list-upload-slots.md) a file answers (Poster, Presentation, Handout…). The slot must be active, apply to the session, accept the file's declared type and have room under its `max_files`; otherwise `400` names the slot. The file's `type` comes from the slot. Optional — API callers act as the organization, so no slot deadline applies and a file may still be attached without a slot. Ignored on events that use a single file request.
* **`session_id` filter on `GET /v1/event/{eventId}/content-slots`.** Returns only the active slots that apply to that session, in speaker order — the list to pick a `content_slot_id` from. `404` if the session is not on the event.

### October 7 — Awards: programs, categories, rounds, promo codes, submissions and reviews

Additive. Nothing existing changes shape.

**New — manage Awards through the API.** Awards are organization-level, so every path starts at `/v1/organization/{orgId}/awards`:

* **Programs** — list, get, create, update, delete. Creating a program sets up its submission form and first round, as the admin app does, and counts against your plan's program limit.
* **Categories and subcategories**, **rounds**, and **promo codes** under `/programs/{programId}/...` — list, get, create, update, delete.
* **Submissions** — list (filter by `status`, `category_id`, `current_round_id`), get with form answers, update answers, delete. Submissions are still created by submitters in the program's portal.
* **Reviews** — list and get each reviewer's assignment and scores on a submission, and remove a reviewer. Scores are still entered by reviewers in the review portal.

`PUT` and `PATCH` both update only the fields you send. Deletes are soft: the record goes to Trash and can be restored in the admin app. Where the admin app asks you to confirm — a program with submissions, a round with reviewer assignments, a promo code already used, a category with submissions — the API answers `409` until you send `force=true`. A category with subcategories also needs `cascade=true`. The default round can't be deleted (`422`), promo codes must be unique within a program (`409`), and a percentage code can't exceed 100 (`400`).

**New — the `read:awards` and `write:awards` scopes.** A token whose scopes don't include them gets `403`; a legacy token with no scopes can read but not write. Both scopes are also offered over OAuth. Another organization's id anywhere in the path returns `404`. See [Authentication](https://apidocs.sessionboard.com/authentication.md).

> [!NOTE]
>
> These endpoints and scopes arrive on the production hosts (`public-api.sessionboard.com`, `public-api-eu.sessionboard.com`) with the next Public API release; until then they answer `404`.

### October 7 — MCP connection: OAuth, not a token switch

Documentation only. Nothing about the API changed.

**Corrected — assistants sign in with OAuth.** Claude, ChatGPT, Copilot, and Gemini connect to the hosted MCP server by signing in. There is no MCP switch on the organization or on an API token, and an API token pasted into the assistant does not authenticate. The person signing in needs AI features on for the organization and the **Connect AI assistants** permission.

**Corrected — `read:insights` is a scope, not a toggle.** A script calling `/v1/insights/...` with `x-access-token` needs `read:insights` in the token's scopes. A token with an empty scope list is the legacy full-access case. The [MCP Server](https://apidocs.sessionboard.com/insights/mcp-server.md) page, [Reports & Dashboards](https://apidocs.sessionboard.com/insights/overview.md), and [OAuth](https://apidocs.sessionboard.com/oauth.md) now say this. OAuth also advertises the write scopes (`write:sessions`, `write:contacts`, and the rest of the authorization-server list); the old note that OAuth was read-only was wrong.

The Help Center walkthrough is at [Connect AI assistants](https://learn.sessionboard.com/integrations/connect-ai-assistants).

### October 6 — Log export for SIEMs: `GET /v1/logs/{stream}`

Additive. Nothing existing changes shape.

**New — pull your logs into Splunk, Microsoft Sentinel or Elastic.** Three endpoints return your organization's logs as pages of events in one shared shape: [`/v1/logs/api-requests`](https://apidocs.sessionboard.com/api-reference/log-export/list-api-request-logs) (every request made with your API tokens to this API, the MCP server and Insights, kept 365 days), [`/v1/logs/security-events`](https://apidocs.sessionboard.com/api-reference/log-export/list-security-events) (sign-ins, failed sign-ins, two-factor and passkey events, password resets) and [`/v1/logs/audit-trail`](https://apidocs.sessionboard.com/api-reference/log-export/list-audit-trail) (every recorded change to your data). Each page returns a `next_cursor`; send it back on the next call and no event is missed or repeated, even one recorded late. 60 requests per minute per token. See [Exporting logs to a SIEM](https://apidocs.sessionboard.com/guides/exporting-logs-to-a-siem.md) for Splunk, Sentinel and Elastic setups.

**New — the `read:audit_logs` scope, on a dedicated token.** Log export needs an API token holding `read:audit_logs` and no other scope. Only users who can manage users can create one; it always covers the whole organization, and it can't call any other endpoint (`403`). OAuth tokens can't use log export.

**Changed — failed authentication is logged.** Requests rejected for a missing, invalid, revoked or expired key, an archived organization, an inactive member, disabled AI features, or an event belonging to another organization are now recorded, with the reason, and appear in `/v1/logs/api-requests` as `outcome: "failure"`. Responses to those requests are unchanged.

> [!NOTE]
>
> These endpoints and the scope arrive on the production hosts (`public-api.sessionboard.com`, `public-api-eu.sessionboard.com`) with the next Public API release; until then they answer `404`.

### October 5 — OAuth tokens stop working when the user leaves the organization

**Changed — an OAuth (MCP / AI assistant) token now ends with its user's membership.** When the user who authorized the connection is deactivated in or removed from the organization, requests with their Bearer token return `401` with "User is no longer an active member of this organization", and refreshing the token fails with `invalid_grant`. Previously the token kept working until it expired. Event-only users with AI Access are unaffected while they remain active. API tokens sent with `x-access-token` are organization credentials and keep working unchanged. See [OAuth → Permission Model](https://apidocs.sessionboard.com/oauth.md#permission-model).

> [!NOTE]
>
> This takes effect on the production hosts with the next Public API release.

### October 2 — Upload slots: `GET /v1/event/{eventId}/content-slots` and `content_slot_id` on session files

Additive. Nothing existing changes shape.

**New — list the files an event asks its speakers for.** Organizers can now define *upload slots* under **Program → Settings → Files** — one per ask (Poster, Presentation, Handout, …), each with its own `due_date`, `due_date_extended`, `allowed_file_types`, `min_files` / `max_files`, `required` flag and `order`. `GET /v1/event/{eventId}/content-slots` returns the active slots in the order speakers see them; `?include_archived=true` adds retired ones (`is_active: false`). Requires `read:sessions`. Which sessions a slot applies to, and which roles may upload, are organizer-side configuration and are not exposed. See [List upload slots](https://apidocs.sessionboard.com/api-reference/session-files/list-upload-slots.md).

**New — `content_slot_id` on every session file.** `GET /v1/event/{eventId}/sessions/{sessionId}/files` now includes `content_slot_id`, the slot the file answers, so a poster is distinguishable from a deck without inspecting filenames or MIME types. It is `null` for files uploaded before slots existed or outside any slot.

**Opt-in per event.** Slots are on for events created or cloned from October 2 onwards; events created earlier stay on their single file request until an organizer turns the switch on. For those events the slot list is empty and `content_slot_id` is `null` on every file — treat an empty list as "this event does not use slots", not as an error. Uploads through this API do not yet take a slot id.

> [!NOTE]
>
> This endpoint is live on the DEV host. The production hosts (`public-api.sessionboard.com`, `public-api-eu.sessionboard.com`) pick it up with the next Public API release; until then they answer `404` for the route and omit `content_slot_id`.

### October 2 — MCP write tools are flagged destructive

**Changed — every `manage_*` tool on the [MCP server](https://apidocs.sessionboard.com/insights/mcp-server.md) now declares `destructiveHint: true`.** Previously only tools with a `delete` action carried the flag; `manage_session`, `manage_contact`, `manage_exhibitor`, `manage_sponsor` and `manage_metadata` reported `destructiveHint: false` because they cannot delete. Each of them can overwrite an existing record through `update`, which is what the hint is meant to signal, so they now report `true`. Tool names, parameters, actions and responses are unchanged. In practice an AI client may ask you to confirm before one of these tools writes, where before it may have run without a prompt; read tools (`list_*`, `read_*`, `get_*`, `query_data`, `execute_sbql`, `search`, `fetch`) are unaffected.

***

## September 2026

### September 30 — Contact updates through the API trigger automations

**Changed — `PUT /v1/event/{eventId}/contacts/{contactId}` now fires field-change automations.** A value written through this endpoint was stored but never announced: Sessionboard automations watching that field (**When → A field changes**) did not run, and the change did not appear in the contact's activity feed. The write now publishes the same field-change event an admin edit does, with the source recorded as `public_api`, so an automation that emails a speaker when **Status** becomes **Confirmed** fires whether the status changed in the admin or through your integration. Request and response shapes are unchanged. Because the same event feeds [webhooks](https://apidocs.sessionboard.com/webhooks.md), a `contact.updated` delivery now follows an API update too; a request that changes both standard and custom fields in one call may deliver two, one per group of values, so keep webhook handling idempotent. If you have an automation that should *not* react to your own writes, add an **Only enroll when** filter on the record rather than relying on the previous silence.

### September 28 — One `404` message per single-record `GET`

**Changed — a missing record reads the same however it is missing.** `GET /v1/event/{eventId}/sessions/{sessionId}` answered an unknown id with `Session could not be found` and a soft-deleted one with `Session could not be found for this event`. Both now answer `Session not found`, the message this reference documents. The speaker, participant and contact `GET`s by id move to their documented messages the same way: `Speaker not found`, `Participant not found on this event`, and `Contact not found` (event and organization). Status `404` and `name: "NotFoundError"` are unchanged; if you match on the message text, match on these.

### September 28 — Bulk `TYPE_MISMATCH` lists dropdown options as an array

**Changed — a rejected dropdown value names each valid option unambiguously.** When a sessions, exhibitors or sponsors bulk operation rejects a value that is not one of a `dropdown` field's options, the `TYPE_MISMATCH` error listed the options joined with commas, so an option whose own label contains a comma (`Banana, Chocolate`) read as two options. The `message` now quotes each option (`Valid options: ["Banana, Chocolate", "10"]`), and the `error` object gains `valid_options`, an array with one entry per option. `code` and the rest of the error are unchanged, and `valid_options` appears on no other error. See [Error codes on `400`](https://apidocs.sessionboard.com/api-reference/overview#error-codes-on-400).

### September 27 — Filter session files by type; `type` and `label` in file responses

`GET /v1/event/{eventId}/sessions/{sessionId}/files` accepts `?type=` — one value, a comma-separated list, or the parameter repeated — and returns only files of those types. The organizer-facing `type` (`presentation`, `handout`, `poster`, `holding_slide`, …) and display `label` are now included on every session file response, so a client can tell a generated **Holding Slide** from a speaker's upload without inspecting the filename. A malformed `type` value answers `400`; an unfiltered call is unchanged. See [Session files](https://apidocs.sessionboard.com/api-reference/session-files.md).

### September 26 — Linked composition sources report as editable

**Changed — `composition_status.is_read_only` is now always `false`.** Sessions linked into another record (`role: "source"`) could already be updated through the API, but the flag still said `true`. It now matches the behavior. `role`, `is_linked` and `target` are unchanged. Deleting, withdrawing, duplicating or re-composing a linked source is still refused until it is unlinked. See [Sessions & composition](https://apidocs.sessionboard.com/api-reference/sessions-composition.md).

### September 25 — Participants: every session role in one search, filter by role, look up by email

Nothing about existing endpoints changed. `POST /v1/event/{eventId}/speakers` and `GET .../speakers/{contactId}` return exactly what they did yesterday; everything here is new.

**Added — `POST /v1/event/{eventId}/participants` returns every contact holding a session role.** Speaker search matches a session's speakers only. Chairpersons, moderators and custom roles (Panelist, Coach, Author, …) were reachable only by walking `POST .../sessions` and merging four arrays per session. Participant search returns all of them in one paginated list, each contact **once**, with a `roles` array listing every role it holds on the event and the session ids where it holds each — `{ roleId, roleName, slug, coreRole, isCustom, sessionIds }`. Every other field is the same `Contact` shape speaker search returns, in the same order, with `roles` appended last. All speaker-search filters, `sort`, `expand` and pagination work identically, including top-level-session matching for categorization filters and the `400` for an empty filter array. See [Search participants](https://apidocs.sessionboard.com/api-reference/participants/search-participants.md).

**Added — filter by role.** `filters.roleIds` narrows the search to participants holding at least one of the given roles, standard or custom; `filters.roleSlugs` does the same by slug (`speaker`, `moderator`, a custom slug) and also works on older events whose roles have `roleId: null`. `GET /v1/event/{eventId}/participant-roles` lists the event's roles with their ids, names, `core_role` and `is_custom`, in the order the Sessions settings page shows them. See [List participant roles](https://apidocs.sessionboard.com/api-reference/participants/list-participant-roles.md).

**Added — look up by email.** Most external systems key on email, not on a Sessionboard contact id. `filters.email` on the search is an exact, case-insensitive match that returns an empty result for an unknown address. `GET /v1/event/{eventId}/participants?email=` is the single-record form: it returns the participant, or `404`. Search on this API is a `POST`, so the `GET` on the collection path is unambiguous — there is no `/by-email` sub-path to remember. See [Get a participant by email](https://apidocs.sessionboard.com/api-reference/participants/get-a-participant-by-email.md).

**Added — `GET /v1/event/{eventId}/participants/{contactId}`** returns one participant with its `roles`, and `404` when the contact is not on the event or holds no session role.

> [!NOTE]
>
> Roles are stored per event, and an event that has never been opened in the Sessions editor may not have its standard roles created yet. Its participants are still returned; a standard role on such an event has `roleId: null` (with `roleName`, `slug` and `coreRole` filled in) and `GET .../participant-roles` returns an empty list until the roles exist.

### September 25 — Subsessions carry the admin-defined order

Event admins can now drag subsessions into a custom order in Sessionboard. The public API reflects that order and tells you what it is. This is an additive change — no existing field changes type or disappears.

**Added — `subsession_order` on every subsession.** Each item in `parent.subsessions[]` — the minimal `Subsession` shape, the `expand=subsession_details` shape, the `SubsessionStatus` items on `POST /v1/event/{eventId}/sessions/status`, and the top-level body when you `GET /v1/event/{eventId}/sessions/{sessionId}` with a subsession id — now includes `subsession_order` (integer, nullable). It is the position the admin set among that parent's subsessions, or `null` for subsessions of a parent that has never been reordered.

**Changed — `subsessions[]` is returned in that order.** Session search, `GET` by id and the status search all sort children by `subsession_order`, then subsession number, then start time, then creation time — the same order the Sessionboard admin, portal and agenda embeds use. Previously the session endpoints ordered children by start time alone and the status search by subsession number, so a reorder in the admin was not visible through the API and the two searches could disagree. For a parent whose subsessions have never been reordered, the array is unchanged in practice, since the backfilled order matches the previous subsession-number-then-start-time sort. If your integration relies on a specific child order, sort on `subsession_order` (or `starts_at`) client-side rather than on array position.

### September 19 — `custom_fields` works on `sessions/bulk`, and creating a status needs `status`

**Fixed — `POST /v1/event/{eventId}/sessions/bulk` writes `custom_fields` instead of discarding them.** An operation carrying `custom_fields` was reported as `"status": "success"` and nothing was stored, on create and on update alike — the update path built its write from a fixed list of standard columns and never read the property at all. Values are now written on both actions. This was the last bulk endpoint with the gap: `contacts/bulk`, `exhibitors/bulk` and `sponsors/bulk` have always honored it. As everywhere else, an unknown `internal_name`, a value of the wrong type, or a field owned by an integration fails **that operation only** and rolls it back; the operations around it still commit.

**Clarified — draft sessions do not take `custom_fields`.** `POST /v1/event/{eventId}/agenda-drafts/{draftId}/sessions/bulk` and its single-record sibling are the one exception to "`data` takes the same fields as the matching single-record endpoint". A draft session is a scheduling placement of an existing session inside a draft agenda, not a record of its own, so there are no custom fields on it. Set them on the underlying session through `/v1/event/{eventId}/sessions`.

**Breaking — `POST /v1/event/{eventId}/statuses/create` requires `color` and `status`.** This reference documented `name` as the only required field, which has never worked: both of the others are stored without defaults, so a minimal payload always failed. It failed as a `500`, which is what made this hard to diagnose. Sending `{ name }` alone now answers `400` naming `color`, and `{ name, color }` answers `400` naming `status`. Nothing that succeeded before stops succeeding — a payload that omitted either field was already failing.

`status` is the underlying approval state a custom label maps onto, and several labels may share one. It must be `pending`, `accepted`, `declined`, `accept_queue` or `decline_queue`; any other value is now a `400` naming the field rather than a `500`. The same enum applies to `PUT /v1/event/{eventId}/statuses/{id}`. The create response code is `201`, not `200` as documented. The other event-settings creates — tags, tracks, levels, formats, rooms, languages and affiliations — need only `name` and are unchanged. See [Create a session status](https://apidocs.sessionboard.com/api-reference/metadata-writes/create-a-session-status.md).

### September 18 — Breaking: missing records answer `404`, and GDPR pagination is validated

> [!WARNING]
>
> **Four behavior changes here can break an existing integration.** Three of them turn a `200` or a `403` into a `404`, and one turns a silently-accepted request into a `400`. If your client treats a non-2xx as a fatal error, or reads a `200` body without checking it for `null`, read this section before your next run.

**Breaking — a session the search cannot see is a `404`, not `200` with a body of `null`.** `GET /v1/event/{eventId}/sessions/{sessionId}` resolved the id against a lookup that sees soft-deleted sessions, then answered from a search that does not, and returned HTTP `200` with a body of literally `null`. No operation in this reference has ever declared a null payload; every one of them declares `404`. A soft-deleted session, and a subsession the search cannot return, now answer `404` with `Session could not be found for this event`. If you were checking the body for `null`, check the status instead.

**Breaking — an id that belongs to a different resource is a `404`.** The same pattern ran on three more single-record GETs: `GET /v1/event/{eventId}/contacts/{contactId}`, `GET /v1/event/{eventId}/speakers/{speakerId}`, and `GET /v1/organization/{orgId}/contacts/{contactId}`. Asking for a sponsor or exhibitor group id as a contact, or a contact id as a speaker, returned `200` with `null`; it now returns `404`. The sponsor and exhibitor GETs already behaved this way.

**Breaking — a record that is not on the event answers `404` instead of `403`.** The group GETs (sponsors, exhibitors, speakers) rejected a record with no association to the event with `403`, even though the same end state answered `404` when the association had been removed through this API's own `DELETE`. It is now `404` in both cases, matching the single not-found response these operations document. If your integration reads `403` as "my token is wrong" and disables itself, this change stops that false alarm. (`403` still means what it says: an authorization failure.)

**Breaking — `GET /v1/gdpr/requests` validates its query parameters.** `?page=0`, `?pageSize=500` and `?pageSize=-1` were accepted with a `200` and silently clamped. They now return `400`, as does any query parameter the operation does not declare. `page` is 1–999 and `pageSize` is 1–100; the maximum is what keeps the endpoint usable on organizations holding millions of rows, so it is a contract rather than a clamp. The spec now lists the `400` alongside `200` and `401`. See [GDPR requests](https://apidocs.sessionboard.com/api-reference/gdpr/list-gdpr-requests.md).

**Fixed — `pageSize` works on the session, content, and transcription lists.** The shared `PageSize` parameter and every POST search are spelled `pageSize`, but these GET lists read only `page_size`, so the documented camelCase spelling was ignored and every response came back at the default 25. Both spellings are now accepted, and `pageSize` above 100 is capped at 100 on these list endpoints.

### September 17 — Reference corrections: group deletes, bulk totals, and empty filter arrays

Documentation only. Nothing about the API changed — these are four places where this reference described behavior the API has never had, each of which could make a generated client or a schema validator reject a valid exchange.

**Corrected — deleting a sponsor or exhibitor returns `204`, not `200`.** `DELETE /v1/event/{eventId}/sponsors/{sponsorId}` and `.../exhibitors/{exhibitorId}` answer `204` with an empty body. This page documented `200 "Sponsor deleted"`, so a client asserting on `200` treated every successful delete as unexpected. A repeated delete of an already-deleted record is a `404`.

**Corrected — a group delete removes the group, not one of its roles.** The path names the role you call under, but the record that is soft-deleted is the underlying group. A group holding both roles loses both, and a group linked to several events is removed from all of them — it stops being returned by any event's sponsor or exhibitor search across your organization. This matches the admin UI, where the unit of deletion is the group; there is no role-scoped or event-scoped delete. Restore reverses it with the same reach, and now documents the `400` you get for a record that is not deleted. See [Soft-delete a sponsor](https://apidocs.sessionboard.com/api-reference/sponsor-writes/soft-delete-a-sponsor.md) and [Soft-delete an exhibitor](https://apidocs.sessionboard.com/api-reference/exhibitor-writes/soft-delete-an-exhibitor.md).

**Corrected — bulk responses carry the totals as `summary`.** Every bulk endpoint returns `{ batch_id, results, summary }`; this page called the totals object `stats`. `summary.succeeded` counts every row whose `status` is not `error`.

**Corrected — the categorization filter arrays need at least one ID.** `trackIds`, `levelIds`, `formatIds`, `languageIds`, `tagIds` and `sessionIds` are rejected with a `400` naming the field when sent as `[]`, on session search, session status search and speaker search alike. The schemas now declare `minItems: 1`, so a validator catches it locally. To apply no filter, omit the property — `filters: {}` and an absent `filters` both return the full unfiltered set.

**Advisory — fetch the spec files unconditionally.** `/api-reference/openapi.json` and `/api-reference/openapi.yaml` are served by this documentation site with the same `ETag` and `Last-Modified`, so a conditional request for one carrying the other's validator returns `304` and a cache can hand back the wrong format. If you regenerate a client in CI, send no `If-None-Match` or `If-Modified-Since` on these two URLs. An unconditional fetch is always correct. See [Fetching the OpenAPI spec](https://apidocs.sessionboard.com/introduction.md#fetching-the-openapi-spec).

### September 17 — GDPR request outcomes, bulk contacts takes the documented body, and `pageSize` works on event settings

**Changed — `POST /v1/gdpr/requests` records the request and returns the erasure outcome.** The call processes the erasure against your organization's data and returns the stored request, including the `id` to quote as evidence and a `response` object listing what was matched, scrubbed, or retained. Read `status`: `complete` means at least one record matched and was processed; `no_records_found` means nothing in your organization matched that email and nothing was erased — escalate rather than treating it as fulfilled. A `502` means the erasure did not run and nothing was recorded; retry. The documented success code is `200`, not `201`.

**Changed — `GET /v1/gdpr/requests` returns request history**, newest first. It is a bare JSON array (the reference previously showed a `results` envelope) and is paginated with `page` and `pageSize` (default 25, maximum 100). See [GDPR requests](https://apidocs.sessionboard.com/api-reference/gdpr/list-gdpr-requests.md).

**Fixed — `POST /v1/event/{eventId}/contacts/bulk` accepts the documented `{ action, id, data }` body.** Every operation previously failed with `email is required`, including update and delete, because the endpoint read contact fields off the operation itself and never looked at `data`. The documented envelope now works for all three actions. The flat form (`{ "email": …, "first_name": … }` with no `action`) is still accepted as a create for existing integrations, and is deprecated.

**Fixed — a failed bulk contact operation leaves nothing behind.** Operations previously shared one transaction and ran ten at a time, so a row reported as `error` could still have committed part of its writes, duplicate-email detection was unreliable inside a batch, and `results` did not follow the request order. Each operation now runs in its own transaction, in request order, so `results[i]` always describes `operations[i]` and a rejected operation is rolled back whole.

**Fixed — bulk endpoints write `translated_fields` instead of discarding them.** `contacts/bulk`, `sessions/bulk`, `exhibitors/bulk` and `sponsors/bulk` accepted `translated_fields` in an operation, reported success, and stored nothing. Variants are now written, and a variant for a language the event has not enabled fails that operation and rolls it back — the same validation the single-record endpoints have had since 13 September.

**Fixed — event-settings lists honor `pageSize`.** `GET` on tags, tracks, levels, formats, rooms, languages, and statuses read only the undocumented `page_size`, so a request sending the documented `pageSize` silently got 25 results. Both spellings now work and `pageSize` wins when both are sent; a value outside 1–100 is clamped on these endpoints rather than rejected. Results also page stably now — rows created in the same write share a timestamp, and without a tiebreaker a row could repeat on one page and be missing from the next.

**Fixed — reference: metadata and field deletes return `204`.** `DELETE` on tags, tracks, levels, formats, rooms, languages, statuses and custom fields was documented as `200`. All return `204 No Content` with no body, and always have.

**Fixed — reference: the event-settings lists are paginated.** `GET` on languages, formats, tracks, levels, rooms and statuses was documented as returning every record as a bare array. Each returns `{ "data": [...], "pagination": {...} }` with snake\_case pagination keys (`current_page`, `page_size`, `total_pages`, `total_results`) — unchanged behavior, now described correctly.

### September 16 — Group roles, integration-owned fields, and a reverted checkbox change

> [!WARNING]
>
> **Reverted — checkbox custom fields serialize as `"Yes"` / `"No"` again.** The 15 September change that made checkbox `custom_fields` values booleans on read has been rolled back. Checkbox values serialize as before: `"Yes"` when checked, `"No"` when explicitly unchecked, and `null` when the field was never set. If you changed an integration to expect strict booleans, revert that comparison. Writes still take boolean `true` / `false`.

**Fixed — contacts no longer appear in sponsor and exhibitor search.** `POST /v1/event/{eventId}/sponsors` and `.../exhibitors` returned contact records alongside real groups, as unnamed rows with an `ACCT-` id, and `GET /v1/event/{eventId}/sponsors/{contactId}` answered with a sponsor-shaped body. Both endpoints now return only account records that carry the role on that event.

**Fixed — a group holding both roles is visible under both.** One account can be a sponsor and an exhibitor on the same event. Linking the second role (`POST .../exhibitors/create` with the `account_id` of an existing sponsor, or the reverse) reported success but the role stayed invisible: the role's search omitted it, `GET` by id returned `200` with a body of `null`, and `PUT`, `DELETE` and restore addressed to that role failed. The second role is now returned by search and `GET`, and is writable. Bulk create links a second role too. See [Groups: sponsors and exhibitors](https://apidocs.sessionboard.com/api-reference/overview.md#groups-sponsors-and-exhibitors).

**Changed — `GET` for a role a record does not hold is now `404`.** Previously such a request returned `200` with a `null` body — a contact id, or a group linked only as the other role. It now returns `404`, as the reference always documented.

**Changed — writes to integration-owned custom fields are rejected.** Fields hydrated by a connector (`swoogo_session_id`, `cvent_session_id`, `visit_session_id`) are read-only. Including one in `custom_fields` — or in `values` on `PUT /v1/event/{eventId}/sessions/{sessionId}/fields` — previously returned `200` and discarded the value silently. It now returns `400` with error code `FIELD_READ_ONLY` and the field in `details.field`, and no other field in that request is written. Omit these fields from your payload; their values stay readable. See [Custom fields](https://apidocs.sessionboard.com/api-reference/overview.md#custom-fields).

**Fixed — reference: sponsor and exhibitor create return `201`, and the session fields endpoint takes `values`.** The reference documented `200` for both `/create` endpoints, and documented a `custom_fields` object for `PUT /v1/event/{eventId}/sessions/{sessionId}/fields` when that endpoint takes a `values` array of `{ internal_name, value, language? }` and responds with the fields it wrote rather than the whole session.

### September 14 — Categorization filters on session status search

**Added — `POST /v1/event/{eventId}/sessions/status` accepts the five categorization filters** (`trackIds`, `tagIds`, `levelIds`, `formatIds`, `languageIds`), the same camelCase body arrays as [session search](https://apidocs.sessionboard.com/api-reference/sessions/search-sessions.md) and [speaker search](https://apidocs.sessionboard.com/api-reference/speakers/search-speakers.md). IDs within one filter match with OR; separate filters combine with AND. Filters are evaluated against the **top-level session**, so a subsession row matches via its parent — and unlike the other session searches, a deleted root session still matches, since surfacing deleted sessions is what this endpoint is for. Previously these fields were rejected with `400 "data.filters should NOT have additional properties"`. See [Filtering](https://apidocs.sessionboard.com/api-reference/overview.md#filtering) and [Search sessions by status](https://apidocs.sessionboard.com/api-reference/sessions/search-sessions-by-status.md).

### September 14 — Malformed dropdown values no longer fail requests

**Fixed — dropdown custom fields with malformed stored entries.** Endpoints that serialize `custom_fields` — session and speaker search (`POST /v1/event/{eventId}/sessions`, `POST /v1/event/{eventId}/speakers`), the related get endpoints, and `GET /v1/events?expand=custom_fields` — returned `500` when a dropdown-type field's stored value contained a null entry, a shape some legacy data imports produce. Null entries are now ignored: a dropdown whose stored entries are all null serializes as `value: null` (empty), and a value with a mix of null and real selections formats from the real selections only. No request or response shape changed — requests that previously succeeded return identical results.

### September 14 — Documentation correction: speaker search matches the speakers role only

**Corrected — speaker search participant roles.** `POST /v1/event/{eventId}/speakers` returns the contacts attached to a session as **speakers**; chairpersons, moderators and other participants are never returned, with or without a categorization filter. The behavior is unchanged and correct — the page simply did not say so, and the session object exposes four separate participant arrays (`speakers`, `chairpersons`, `moderators`, `participants`) that are easy to mistake for interchangeable. Because roles are per session, the same contact can be a speaker on one session and a moderator on another, so a contact in the unfiltered speaker list may legitimately be missing from a filtered one. See [Filtering](https://apidocs.sessionboard.com/api-reference/overview.md#filtering) and [Search speakers](https://apidocs.sessionboard.com/api-reference/speakers/search-speakers.md).

### September 13 — MCP docs refresh & AI assistant guides

**Docs** — [MCP Server](https://apidocs.sessionboard.com/insights/mcp-server.md) reference updated: full 38-tool inventory (including the `search` / `fetch` pair and the nine read-only list tools), Middle East region URL (`mcp-me.sessionboard.com`), and corrected PII-masking behavior (always on for OAuth query results; per-token **Hide PII** setting for API tokens, off by default). The Sessionboard connector is now listed in Claude's connector directory, and the Help Center has step-by-step connection guides for [Claude](https://learn.sessionboard.com/integrations/connect-claude), [ChatGPT](https://learn.sessionboard.com/integrations/connect-chatgpt), [Microsoft Copilot](https://learn.sessionboard.com/integrations/connect-microsoft-copilot), and [Google Gemini](https://learn.sessionboard.com/integrations/connect-google-gemini) — overview at [Connect AI assistants](https://learn.sessionboard.com/integrations/connect-ai-assistants).

### September 13 — Event tags return real data, translated-fields validation & empty-string clears

**Fixed — `GET /v1/event/{eventId}/tags` returns the event's tags.** The list endpoint previously read from an internal org-level table that the app never writes, so it returned an empty list for every event. It now returns the event's taxonomy tags (the ones organizers create and apply to sessions in the app) with the same `{"data": [...], "pagination": {...}}` envelope and `page`/`page_size` parameters as the sibling metadata endpoints (tracks, levels, formats). Items now include `type`, `color`, and `order`. Writes are consistent too: `POST /tags/create` accepts `color` and `order` and returns `201` (previously created orphan records the app never showed), and `PUT /tags/{id}` supports the `updated_at` optimistic-concurrency check (`409` `STALE_UPDATE`). The search endpoint (`POST /tags`) is unchanged — it already read the correct table.

**Changed — `translated_fields` is validated** (`write:sessions`, `write:contacts`, `write:metadata`)

On every create/update that accepts `translated_fields`, entries are now validated before anything is written: `translated_fields` must be an array, every entry must include a non-empty `translated_language`, and the language must be one of the event's enabled locales (the event default plus enabled variants). Invalid input fails the whole request with `400` `VALIDATION_ERROR` naming the offending key and listing the enabled locales — previously such entries were silently accepted and could write variants under a bogus internal locale. Requests that already send valid entries are unaffected. Related cleanup: the `updated_fields[].language` echo on `PUT .../sessions/{sessionId}/fields` is now omitted when the request didn't specify a language (it previously echoed an internal `"EN_US"` placeholder).

**Fixed — empty string clears numeric custom fields.** For `number`, `currency`, and `percent` custom fields, sending `""` (or a whitespace-only string) in `custom_fields` now clears the stored value, identical to sending `null`. Previously it stored an actual `0`, which is indistinguishable from a real zero. `0` and `"0"` still store zero.

### September 12 — Single-session GET is no longer cached

**Changed — Sessions** (`read:sessions`)

`GET /v1/event/{eventId}/sessions/{sessionId}` no longer serves cached responses. It previously shared the session search cache, which could return a stale `updated_at` for up to 2 minutes after an edit and cause the follow-up `PUT` to fail with `409 Conflict`. The GET now always returns the current record, so the optimistic-concurrency flow (GET → `PUT` with `updated_at`) works immediately after any change.

`POST /v1/event/{eventId}/sessions` (session search) keeps its 2-minute cache — see [Caching](https://apidocs.sessionboard.com/introduction.md#caching).

### September 8 — Events pagination (breaking), ISO 8601 datetimes & sponsor/exhibitor names

> [!WARNING]
>
> **Breaking — `GET /v1/events` now returns the documented pagination envelope.** The response is `{"results": [...], "pagination": {...}}`, and the `page` (1–999, default 1) and `pageSize` (1–100, default 25) query parameters are honored. The previously deployed behavior returned a bare JSON array and ignored both parameters. If your integration parses the response as an array, read the `results` key instead; if you have more than 25 events, page through the results. Events are ordered oldest-first (`created_at` ascending, ties broken by `id`), so pages are stable.

**Changed — datetime custom field values are ISO 8601.** On `GET /v1/events?expand=custom_fields`, datetime values serialize as event-local time with the event's UTC offset (for example `2026-08-15T09:00:00-03:00`), falling back to the raw UTC instant (`2026-08-15T12:00:00.000Z`) when the event has no valid timezone. On session, speaker, sponsor, and exhibitor `custom_fields`, datetime values are the raw UTC instant. Previously datetime values were human-readable strings in server-local time with no timezone marker, which was ambiguous for any consumer parsing them. Date-only fields are unchanged.

**Fixed — sponsor & exhibitor `name` persists and is validated.** Create (`POST .../sponsors/create`, `POST .../exhibitors/create`) now persists `name` and enforces the documented contract: `name` is required unless `account_id` links an existing account (`400` otherwise, and `400` when `account_id` does not reference an existing account). Update rejects an explicit empty-string `name` with `400`. `name` is returned on create, update, get, and search responses. Previously `name` was silently dropped on create/update, and the resulting unnamed records could be missing from search and get-by-id responses — such records are now returned (with `name: null` until a name is set).

**Fixed — session status search `sort.deletedAt`.** `POST /v1/event/{eventId}/sessions/status` with `{"sort": {"deletedAt": {"sort": "desc"}}}` now works as documented (previously it returned `500`). Deleted sessions sort before never-deleted ones in both directions.

**Fixed — single-value `expand` query parameter.** GET endpoints that accept `expand` now take the documented single-value form (`?expand=composition`) in addition to the repeated-key (`?expand=a&expand=b`) and bracket (`?expand[]=a`) forms. Previously the single-value form returned `400` on some endpoints.

### September 8 — Filter validation, speaker filter semantics & zero-value custom fields

**Changed — session list filter validation.** On `GET /v1/event/{eventId}/sessions`, a non-UUID value in any categorization filter param (`track_id`/`track_ids`, `level_id`/`level_ids`, `format_id`/`format_ids`, `language_id`/`language_ids`, `tag_id`/`tag_ids`) now returns `400` with error code `VALIDATION_ERROR` and a message naming the offending parameter (previously an unhandled `500`).

**Fixed — speaker search categorization filters.** On `POST /v1/event/{eventId}/speakers`, the session categorization filters (`filters.trackIds`, `levelIds`, `formatIds`, `languageIds`, `tagIds`) now match against the **top-level session** — a speaker assigned to a subsession matches via the subsession's parent session, aligning with session search semantics. Previously a subsession's own categorization values could match speakers whose parent session did not satisfy the filter.

**Fixed — zero values in numeric custom fields.** Custom field values whose stored value is numeric `0` (`number`, `currency`, `percent`, `quantity`, and formula fields) now serialize as their formatted value instead of `null` across all responses that include `custom_fields`. `null` now reliably means the field is empty.

### September 7 — Documentation corrections: search sort shape & categorization filters

**Corrected — search `sort` shape.** Search endpoints take nested sort keys (`{"sort": {"startsAt": {"sort": "asc"}}}`), not the previously documented flat `{"order": "createdAt", "sort": "asc"}` pair. Session search supports `startsAt` (null `starts_at` sorts last); session status search supports `deletedAt`. See [Sorting](https://apidocs.sessionboard.com/api-reference/overview.md#sorting).

**Documented — session categorization filters.** Tracks, tags, levels, formats, and languages are filterable on session search and speaker search (camelCase body arrays such as `filters.trackIds`) and on the session CRUD list (snake\_case query params such as `track_ids`). These filters are not available on other endpoints. See [Filtering](https://apidocs.sessionboard.com/api-reference/overview.md#filtering).

***

## August 2026

### August 26 — List events expands

**Added — `GET /v1/events` `expand`** (`read:events`)

Opt-in collections on each event. Omitted from the payload when not requested; present as `[]` when requested and empty.

| Expand | Contents |
| - | - |
| `custom_fields` | Org-defined Event custom field values (`CustomFieldValue[]`) |
| `integration_mappings` | Conference connector mappings (`EventIntegrationMapping[]`) from `Integration_Event_Mapping_Union` |

Repeat the query param or comma-separate: `?expand=custom_fields,integration_mappings`. `source` on mappings is the union key (underscores, e.g. `expo_platform`); `name` is the display label. LiveBuzz / apps-engine campaign IDs are **not** included in `integration_mappings`. These values are not on the session `Expand` enum.

The default event shape now also documents `starts_at`, `ends_at`, and `features.primary_speakers` / `speaker_acceptance`.

***

## July 2026

### July 20 — Session & event content packs

**Added — Composed content** (`read:transcriptions`)

One-call packs for mobile and partner apps — composed transcript elements (speaker turns, optional `interval_sec`), preferred summary, insights, podcast status/URL, and summary/event PDF links.

| Method | Path |
| - | - |
| `GET` | `/v1/event/{eventId}/sessions/{sessionId}/content` |
| `GET` | `/v1/event/{eventId}/content` |
| `GET` | `/v1/event/{eventId}/content/event` |
| `GET` | `/v1/event/{eventId}/content/items/{itemId}` |
| `GET` | `.../content/documents/{documentType}` |

Raw `.../transcriptions` CRUD is unchanged. Content is not session [composition](https://apidocs.sessionboard.com/api-reference/sessions-composition.md).

**Documentation**

* New guide: [Consuming session content](https://apidocs.sessionboard.com/guides/consuming-session-content.md)
* Updated: [Transcriptions & Media](https://apidocs.sessionboard.com/api-reference/transcriptions-and-media.md)

### July 10 — Session files & upload paths

**Added — Session files** (`read:sessions` / `write:sessions`)

Attach PDFs, PowerPoint decks, Word docs, and similar files to a session.

| Path | Method | Endpoint | Max size |
| - | - | - | - |
| **Simple upload** | `POST` | `/v1/event/{eventId}/sessions/{sessionId}/files/upload` | 50 MB |
| Direct-to-storage create | `POST` | `/v1/event/{eventId}/sessions/{sessionId}/files` | 500 MB |
| Complete | `POST` | `.../files/{fileId}/complete` | — |
| List | `GET` | `.../files` | — |
| Update | `PUT` | `.../files/{fileId}` | — |
| Replace | `POST` | `.../files/{fileId}/replace` | 500 MB |
| Delete | `DELETE` | `.../files/{fileId}` | — |

**Simple upload** — one multipart request with field `file`. Sessionboard detects size and MIME type, scans the file, and returns the attached file. No presigned URL steps.

**Direct-to-storage** — create → PUT bytes to signed URL → complete. Use for files over 50 MB (up to 500 MB) or when you prefer uploading straight to storage (standard presigned-URL pattern used by AWS, Stripe, and similar APIs).

Session-scoped creates use standard REST (`POST` on the collection path — no `/create` suffix). The `/create` suffix remains only where `POST` on the collection path is search (sessions, contacts, metadata entities, etc.).

**Documentation**

* New guide: [Uploading session files](https://apidocs.sessionboard.com/guides/uploading-session-files.md) — two-tab playbook (simple vs direct-to-storage)
* API overview: [Session Files](https://apidocs.sessionboard.com/api-reference/session-files.md)

### July 7 — Sessions & scopes

**Fixed**

* Embedded contact fields on session responses (`photo_url`, `company_name`, `title`, `address_country`, and related profile fields) now populate correctly on `speakers[]`, `chairpersons[]`, `moderators[]`, and `participants[]` in the event's default language.
* Participant ordering respects configured role sort order and per-participant `order`.

**Changed**

* Read routes for transcriptions and recordings require the `read:transcriptions` scope when your token has explicit scopes.
* Read routes for media items require the `read:media` scope when your token has explicit scopes.
* `read:transcriptions` and `read:media` added to the OAuth scope catalog.

**Documentation**

* Session participant model documented: `participants[]` vs legacy role arrays, `participant_role` shape, and contact fields available without `expand`.

### July 6 — Transcriptions, recordings & media upload

**Added — Transcriptions** (`read:transcriptions` / `write:transcriptions`)

| Method | Path |
| - | - |
| `POST` | `/v1/event/{eventId}/sessions/{sessionId}/transcriptions` |
| `GET` | `/v1/event/{eventId}/sessions/{sessionId}/transcriptions` |
| `GET` | `/v1/event/{eventId}/sessions/{sessionId}/transcriptions/{id}` |
| `PUT` | `/v1/event/{eventId}/sessions/{sessionId}/transcriptions/{id}` |
| `DELETE` | `/v1/event/{eventId}/sessions/{sessionId}/transcriptions/{id}` |
| `GET` | `/v1/event/{eventId}/transcriptions` |
| `GET` | `/v1/event/{eventId}/transcriptions/{id}` |

Supports artifact types: `fragment`, `summary`, `insight`, and `translation`. Fragments written via the API appear in the event's Marketing Media transcript view.

**Added — Session recordings** (`read:transcriptions` / `write:transcriptions`)

| Method | Path |
| - | - |
| `POST` | `/v1/event/{eventId}/sessions/{sessionId}/recordings` |
| `GET` | `/v1/event/{eventId}/sessions/{sessionId}/recordings` |
| `GET` | `/v1/event/{eventId}/sessions/{sessionId}/recordings/{id}` |
| `POST` | `/v1/event/{eventId}/sessions/{sessionId}/recordings/{id}/complete` |

**Added — Media upload** (`read:media` / `write:media`)

Direct-to-S3 multipart upload for video or audio files, with automatic transcription when processing completes.

| Method | Path |
| - | - |
| `POST` | `/v1/event/{eventId}/sessions/{sessionId}/media/upload/initiate` |
| `POST` | `/v1/event/{eventId}/sessions/{sessionId}/media/upload/sign-part` |
| `POST` | `/v1/event/{eventId}/sessions/{sessionId}/media/upload/complete` |
| `POST` | `/v1/event/{eventId}/sessions/{sessionId}/media/upload/abort` |
| `GET` | `/v1/event/{eventId}/sessions/{sessionId}/media/{mediaItemId}` |

**Documentation**

* New guide: [Media & Transcriptions](https://apidocs.sessionboard.com/guides/uploading-media-and-transcriptions.md) (three integration playbooks).
* New reference page: [Transcriptions & Media](https://apidocs.sessionboard.com/api-reference/transcriptions-and-media.md).
* OpenAPI tags split into **Transcriptions**, **Session Recordings**, and **Media**.

### July 5 — Sessions 2.0 participants

**Added**

* `participants[]` on session search and get — flat list from the Sessions 2.0 model, including custom program roles (Author, Panelist, etc.). Always returned; no expand flag required.
* `participant_role` on legacy `speakers[]`, `chairpersons[]`, and `moderators[]` when the event has Sessions 2.0 roles configured.
* `expand=composition` on session search/get — returns composition status, target session summary, and source counts for abstract-to-session linking.
* Richer nested metadata on `language`, `track`, `level`, `room`, and `format` objects (`color`, `order`, `capacity`, timestamps, etc.) instead of `{ id, name }` only.

**Changed**

* Minimal `parent.subsessions[]` shape now includes `participants` alongside `speakers`.

### July 3 — Session write responses

**Added**

* `is_abstract` on session create and bulk create (requires Sessions 2.0 on the event).
* `expand=composition` on create/update/bulk responses — includes composition status block.

**Changed**

* Create, update, bulk, get, and list responses now share one formatter — write responses match read/search shape (nested metadata, composition, etc.).
* `expand=linked_sources` and `expand=composition` supported consistently across session CRUD handlers.

***

## June 2026

### June 18 — Subsessions

**Fixed**

* `GET /v1/event/{eventId}/sessions/{sessionId}` accepts a subsession UUID directly and returns the full session shape (with parent friendly-id keys).
* `expand=subsession_details` includes `is_public` on every subsession in `parent.subsessions[]`.

***

## May 2026

### May 22 — Subsession field parity

**Added**

* `expand=subsession_details` on session search and get — every item in `parent.subsessions[]` returns full parent-shape parity (`status`, `custom_fields`, `chairpersons`, `moderators`, `sponsors`, `exhibitors`, `tags`, nested metadata, and more). Default subsession shape is unchanged for backward compatibility.
* `GET /v1/event/{eventId}/sessions/{sessionId}` with a subsession UUID returns the full session DTO at the top level.
* `POST /v1/event/{eventId}/sessions/status` returns a minimal `subsessions[]` array on parent rows (id, friendly\_id, status, custom\_status, timestamps) for status-sync integrations.

**Documentation**

* Subsession expand behavior and `SubsessionDetailed` schema documented in the [API overview](https://apidocs.sessionboard.com/api-reference/overview.md).

### May 7 — Webhooks

**Documentation**

* Webhook payload schemas updated to match the current event shape.

***

## April 2026

### April 13 — OAuth token lifetimes

**Changed**

* OAuth access tokens expire after **24 hours** (previously shorter).
* OAuth refresh tokens expire after **90 days**.

### April 12 — Deep links & OAuth hardening

**Added**

* `admin_url` on session, contact, exhibitor, and sponsor responses — direct link to the record in the Sessionboard admin UI.

**Changed**

* OAuth Bearer tokens work with all MCP server tools.
* OAuth metadata URLs served correctly over HTTPS behind the load balancer.

### April 11 — OAuth 2.1 & rate limits

**Added**

* OAuth **write scopes** for sessions, contacts, exhibitors, sponsors, fields, metadata, and events.
* RFC 8707 **resource parameter** support on the token endpoint.
* **Per-token rate limit overrides** — contact support to raise limits for high-volume integrations.
* Browser-based OAuth: `/oauth/authorize` redirects to the consent UI.
* OAuth discovery at `/.well-known/oauth-authorization-server`.

**Documentation**

* [OAuth](https://apidocs.sessionboard.com/oauth.md) guide and [Rate limiting](https://apidocs.sessionboard.com/rate-limiting.md) page published.

### April 10 — Convenience routes & insights

**Added**

* **Convenience routes** for org-level dashboards, widgets, and saved reports at `/v1/dashboards`, `/v1/widgets`, and `/v1/queries` (in addition to event-scoped paths).
* Event-scoped insights, dashboards, widgets, and queries routed through the unified v1 proxy.

**Fixed**

* Widget create no longer returns 404 when `dashboardId` is in the path.
* Rules list handles missing pagination without error.
* Contact create/update returns profile fields correctly when locale casing differs.

### April 9 — Agenda planning & metadata

**Added**

* **Agenda drafts** — full CRUD for drafts, draft sessions, commit, and change history.
* **Scheduling rules** — create, read, update, delete.
* **Personas** — create, read, update, delete.
* **Event metadata CRUD** — rooms, tracks, tags, formats, levels, languages, and session statuses (read and write).

### April 7 — Entity write API (major release)

**Added**

* **Session CRUD** — create, update, delete, restore, bulk operations, and custom field updates.
* **Contact CRUD** — create, update, delete, restore, bulk, and list sessions for a contact.
* **Exhibitor & sponsor CRUD** — create, update, delete, restore, and bulk.
* **Custom field definitions** — create, update, delete.
* **Organization-scoped contact** read endpoints.
* **Bearer token** authentication alongside `X-Access-Token`.
* **Write scopes** enforced on all mutating routes.

**Documentation**

* Full OpenAPI schemas for all CRUD endpoints.
* Sidebar reorganized by org scope, event scope, and convenience routes.

### April 8 — Reliability

**Fixed**

* Error details from the API are forwarded through the proxy (clearer validation messages).
* `204 No Content` responses handled correctly on delete/restore.
* Scheduled smoke tests against the dev environment (ongoing quality monitoring).

***

## March 2026

### March 25 — Organization contacts

**Fixed**

* `GET /v1/organization/{orgId}/contacts/{contactId}` path corrected in the OpenAPI spec (was returning 404 for valid requests).

### March 23 — Documentation site

**Added**

* Public API documentation launched at [apidocs.sessionboard.com](https://apidocs.sessionboard.com) (Mintlify).
* [Insights & AI](https://apidocs.sessionboard.com/insights/overview.md) section — MCP server, SbQL, and analytics endpoints.
* EU region base URL documented: `https://public-api-eu.sessionboard.com`.

***

## Earlier capabilities

These endpoints predate 2026 but are part of the current API surface:

* **Session & speaker search** — `POST /v1/event/{eventId}/sessions`, speaker search, and individual session get (search results cached for 2 minutes; the individual session get is not cached — see [Caching](https://apidocs.sessionboard.com/introduction.md#caching)).
* **GDPR** — `GET` and `POST /v1/gdpr/requests`.
* **Webhooks** — real-time notifications on data changes ([Webhooks](https://apidocs.sessionboard.com/webhooks.md)).
* **Insights** — SbQL execute, AI query generation, saved reports, and dashboards ([Insights overview](https://apidocs.sessionboard.com/insights/overview.md)).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.