openapi: 3.1.0
info:
  title: Sessionboard Public API
  version: '1.0'
  description: REST API for managing sessions, speakers, contacts, sponsors, exhibitors, and event data on the Sessionboard platform.
  contact:
    name: Sessionboard Support
    email: support@sessionboard.com
  termsOfService: https://www.sessionboard.com/legal/terms-of-service
servers:
  - url: https://public-api.sessionboard.com
    description: US Region
  - url: https://public-api-eu.sessionboard.com
    description: EU Region
security:
  - ApiKey: []
  - BearerToken: []
tags:
  - name: Events
    description: Manage events on the platform.
  - name: Sessions
    description: Search and retrieve sessions within an event.
  - name: Session Writes
    description: Create, update, delete, and restore sessions. Requires the `write:sessions` scope.
  - name: Contact Writes
    description: Create, update, delete, and restore contacts. Requires the `write:contacts` scope.
  - name: Exhibitor Writes
    description: Create, update, delete, and restore exhibitors. Requires the `write:exhibitors` scope.
  - name: Sponsor Writes
    description: Create, update, delete, and restore sponsors. Requires the `write:sponsors` scope.
  - name: Field Writes
    description: Create, update, and delete custom fields. Requires the `write:fields` scope.
  - name: Speakers
    description: Search and retrieve speakers within an event.
  - name: Participants
    description: |
      Search and retrieve every contact holding a session role on an event —
      speakers, chairpersons, moderators and custom roles — with the roles
      each one holds, and list the event's participant roles.
  - name: Contacts
    description: Search and retrieve contacts at the organization or event level.
  - name: Sponsors
    description: Search and retrieve sponsors within an event.
  - name: Exhibitors
    description: Search and retrieve exhibitors within an event.
  - name: Event Settings
    description: Retrieve event configuration such as fields, tags, tracks, rooms, formats, levels, languages, and session statuses.
  - name: GDPR
    description: Manage GDPR data access and erasure requests.
  - name: Insights
    description: Query event data using SbQL, generate queries with AI, and manage dashboards.
  - name: Ask
    description: Ask a natural-language question about one event and get a cited answer. Requires the `read:ask` scope, granted explicitly.
  - name: Metadata Writes
    description: Create, update, and delete session metadata (rooms, tracks, tags, formats, levels, languages, statuses). Requires the `write:metadata` scope.
  - name: Agenda Planning
    description: Manage agenda drafts, draft sessions, scheduling rules, and evaluation personas. Requires the `write:events` scope.
  - name: Dashboards & Widgets
    description: Create, read, update, and delete dashboards and widgets at the event or organization level. Requires `write:events` scope for writes.
  - name: Reports & Queries
    description: Create, read, update, delete, and run saved SbQL queries/reports at the event or organization level. Requires `write:events` scope for writes.
  - name: Transcriptions
    description: Transcript text on sessions — fragments, summaries, insights, and translations. Requires `read:transcriptions` / `write:transcriptions` scopes.
  - name: Session Recordings
    description: Session audio files for recording archives (live capture or imported audio). Requires `read:transcriptions` / `write:transcriptions` scopes.
  - name: Media
    description: Upload video or audio files for automatic transcription. Requires `read:media` / `write:media` scopes.
  - name: Session Files
    description: |
      Documents attached to sessions (PDFs, PowerPoint, Word, images, and similar).
      Simple upload: `POST .../files/upload` (multipart, max 50 MB).
      Direct-to-storage: create → PUT → complete (max 500 MB).
      Requires `read:sessions` / `write:sessions` scopes.
      See the [Uploading session files](/guides/uploading-session-files) guide.
  - name: OAuth
    description: OAuth 2.1 endpoints for AI client authorization (Claude, ChatGPT). Mounted at `/oauth/` (not `/v1/`).
  - name: Log Export
    description: |
      Pull your organization's API request, security and audit logs into a SIEM
      such as Splunk, Microsoft Sentinel or Elastic. Requires a dedicated API token
      with the `read:audit_logs` scope, which can call nothing else.
      See the [Exporting logs to a SIEM](/guides/exporting-logs-to-a-siem) guide.

  - name: Awards
    description: >-
      Read and manage Awards programs and their categories, rounds, promo codes, submissions and
      reviews. Organization-level: paths start at `/v1/organization/{orgId}/awards`. Requires
      `read:awards` / `write:awards` scopes.
  - name: Registration
    description: |
      Registration inside Sessionboard: registration forms, ticket types, registrations,
      orders, attendees and check-ins for one event. Requires the preview scopes
      `read:registrations` / `write:registrations`, which a token or OAuth client gets only
      when they are named explicitly (not by default, and not listed in `scopes_supported`).
      The event must have Registration turned on (Early Access); otherwise every endpoint
      answers `403 FORBIDDEN`.
      See the [Registration and Expo](/guides/registration-and-expo) guide.
  - name: Expo
    description: |
      Expo floor-map booths and sponsorship inventory for one event. Requires the preview
      scopes `read:expo` / `write:expo`, which a token or OAuth client gets only when they are
      named explicitly (not by default, and not listed in `scopes_supported`). The event must
      have Expo turned on (Early Access); otherwise every endpoint answers `403 FORBIDDEN`.
      See the [Registration and Expo](/guides/registration-and-expo) guide.
paths:
  # ──────────────────────────────────────────────
  # Events
  # ──────────────────────────────────────────────
  /v1/events:
    get:
      operationId: list-events
      summary: List events
      description: |
        Returns events accessible to the authenticated API key.

        Results are paginated: `page` (1–999, default 1) and `pageSize`
        (1–100, default 25) are honored, and the response wraps events in
        the standard `{ results, pagination }` envelope. Events are ordered
        oldest-first (`created_at` ascending, ties broken by `id`), so pages
        are stable across requests.

        By default each event is `id`, `name`, `timezone`, `starts_at`,
        `ends_at`, and `features`. Pass `expand` to attach extra collections.

        `expand=custom_fields` includes org-defined event custom field values
        (`CustomFieldValue[]`). An event with no values returns `[]`.
        Datetime custom field values are ISO 8601 in the event's local time
        with its UTC offset (for example `2026-08-15T09:00:00-03:00`); when
        the event has no valid timezone the raw UTC instant is returned
        (`2026-08-15T12:00:00.000Z`). Date-only (`date`) fields serialize
        as `M-DD-YYYY`: month without a leading zero, day with one, four-digit
        year (for example `9-22-2026` for 22 September 2026, `3-04-2026` for
        4 March 2026). This is not ISO 8601; parse it with that exact pattern.

        `expand=integration_mappings` includes conference connector mappings
        (`EventIntegrationMapping[]`) from `Integration_Event_Mapping_Union`
        (Cvent, Grip, Swoogo, Swapcard, ASP, Stova, Bizzabo, Gleanin,
        ExpoPlatform, ON24). `source` is the union key (underscores, e.g.
        `expo_platform`). LiveBuzz / apps-engine campaign IDs are **not**
        included in this expand.

        Repeat the query param or comma-separate values:
        `?expand=custom_fields&expand=integration_mappings`
      tags:
        - Events
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/ListEventsExpand'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Event'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
              example:
                results:
                  - id: 3960
                    name: Acme Conference
                    timezone: America/Chicago
                    starts_at: '2026-10-05T13:00:00.000Z'
                    ends_at: '2026-10-07T22:00:00.000Z'
                    features:
                      primary_speakers: true
                      speaker_acceptance: false
                      translated_fields: false
                pagination:
                  currentPage: 1
                  pageSize: 25
                  totalPages: 4
                  totalResults: 97
        '401':
          description: Unauthorized

  # ──────────────────────────────────────────────
  # Sessions
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/sessions:
    post:
      operationId: search-sessions
      summary: Search sessions
      description: |
        Search and filter sessions within an event. Supports pagination,
        sorting, filtering by status, date ranges, and session categorization
        (`trackIds`, `tagIds`, `levelIds`, `formatIds`, `languageIds`), and
        expanding related data.

        Each categorization filter needs at least one ID. An empty array is
        rejected with a 400 naming the field — to apply no filter at all, omit
        the property rather than sending `[]`.

        `sort` uses nested keys — each sortable field is an object with a
        `sort` direction, for example `{"sort": {"startsAt": {"sort": "asc"}}}`.
        There is no flat `{"order": "...", "sort": "..."}` form. Sessions with
        a null `starts_at` always sort last when sorting by `startsAt`,
        regardless of direction.

        Every session includes `is_abstract` and `composition_status`. Pass
        `filters.isAbstract` to return only abstract submissions or only
        program sessions. By default, sessions linked as composition sources
        are excluded; pass `expand=linked_sources` to include them. Pass
        `expand=composition` for full target/source details.

        Each session in the response includes a `subsessions` array. By
        default each subsession is returned in a minimal shape (`Subsession`).
        Pass `expand=subsession_details` in the request body to receive full
        parent-shape parity (`SubsessionDetailed`) on every subsession —
        including `status`, `custom_status`, `custom_fields`, `chairpersons`,
        `moderators`, `sponsors`, `exhibitors`, `tags`, `language`, `track`,
        `level`, `room`, etc.

        Each subsession inherits the parent session's `is_abstract`,
        `composition_status`, and optional `composition` graph (when
        `expand=composition` is set).

        Nested `language`, `track`, `level`, `format`, and `room` objects on
        each session include the field sets documented on the `Session` schema.
        Unassigned metadata is returned as `{}`.

        Paginated search responses return matching sessions under `results`
        (not `data`), alongside a `pagination` object.
      tags:
        - Sessions
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionSearchBody'
            examples:
              sortByStartTime:
                summary: Sort by session start time (nested sort syntax)
                description: |
                  Sort keys are nested objects — `{"startsAt": {"sort": "asc"}}`.
                  Sessions with a null `starts_at` always sort last, regardless
                  of direction.
                value:
                  filters:
                    status: accepted
                  sort:
                    startsAt:
                      sort: asc
              filterByCategorization:
                summary: Filter by tracks and tags
                description: |
                  IDs within one filter match with OR; separate filters combine
                  with AND.
                value:
                  filters:
                    trackIds:
                      - 3fa85f64-5717-4562-b3fc-2c963f66afa6
                    tagIds:
                      - 9b2f0c1d-8e47-4a6b-9c3d-1a2b3c4d5e6f
                  sort:
                    startsAt:
                      sort: asc
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Session'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found
    get:
      operationId: list-sessions
      summary: List sessions (CRUD proxy)
      description: |
        Paginated list of top-level sessions via the CRUD proxy. Returns nested
        `language`, `track`, `level`, `format`, and `room` metadata using the same
        field sets documented on the `Session` schema (`null` when unassigned).
        Also includes `is_abstract` and `composition_status`.

        Query params: `is_abstract` (`true`/`false`), `expand=linked_sources`,
        `expand=composition`, plus standard pagination and filters.

        Session categorization filters use snake_case query params on this
        endpoint: `track_id`/`track_ids`, `tag_id`/`tag_ids`,
        `level_id`/`level_ids`, `format_id`/`format_ids`,
        `language_id`/`language_ids`. Plural params take comma-separated UUIDs
        (OR within a filter; separate filters combine with AND).

        Every value passed to these filters must be a UUID. A non-UUID value
        returns `400` with error code `VALIDATION_ERROR` and a message naming
        the offending parameter, for example
        `track_id must be a UUID (received "nope")`.
      tags:
        - Sessions
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - name: page_size
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: is_abstract
          in: query
          schema:
            type: boolean
          description: Filter to abstract submissions or program sessions.
        - $ref: '#/components/parameters/Expand'
        - name: status
          in: query
          schema:
            type: string
        - name: track_id
          in: query
          schema:
            type: string
            format: uuid
          description: Filter by a single track UUID.
        - name: track_ids
          in: query
          schema:
            type: string
          description: Comma-separated track UUIDs (OR). Repeat `track_id` for the same effect.
        - name: level_id
          in: query
          schema:
            type: string
            format: uuid
        - name: level_ids
          in: query
          schema:
            type: string
          description: Comma-separated level UUIDs (OR).
        - name: format_id
          in: query
          schema:
            type: string
            format: uuid
        - name: format_ids
          in: query
          schema:
            type: string
          description: Comma-separated format UUIDs (OR).
        - name: language_id
          in: query
          schema:
            type: string
            format: uuid
        - name: language_ids
          in: query
          schema:
            type: string
          description: Comma-separated language UUIDs (OR).
        - name: tag_id
          in: query
          schema:
            type: string
            format: uuid
        - name: tag_ids
          in: query
          schema:
            type: string
          description: Comma-separated tag UUIDs (OR).
        - name: search
          in: query
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Session'
                  pagination:
                    type: object
                    properties:
                      current_page:
                        type: integer
                      page_size:
                        type: integer
                      total_pages:
                        type: integer
                      total_results:
                        type: integer
        '400':
          description: |
            A categorization filter param (`track_id`/`track_ids`,
            `level_id`/`level_ids`, `format_id`/`format_ids`,
            `language_id`/`language_ids`, `tag_id`/`tag_ids`) carried a
            non-UUID value. The message names the offending parameter.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    examples:
                      - VALIDATION_ERROR
                  message:
                    type: string
                    examples:
                      - track_id must be a UUID (received "nope")
        '401':
          description: Unauthorized

  /v1/event/{eventId}/sessions/status:
    post:
      operationId: search-sessions-by-status
      summary: Search sessions by status
      description: |
        Returns a lightweight list of session statuses, including deleted
        sessions. Supports filtering by `deletedAt` in addition to standard
        filters.

        Also accepts the five session categorization filters (`trackIds`,
        `tagIds`, `levelIds`, `formatIds`, `languageIds`). IDs within one
        filter match with OR; separate filters combine with AND. The filters
        are evaluated against the **top-level (root) session**: a subsession
        row matches when its parent carries the filtered attribute, so a
        matching parent and the flat rows of its `subsessions` array stay
        consistent. Unlike other session searches, a **deleted** root session
        still satisfies a categorization filter — surfacing deleted sessions
        is this endpoint's purpose, and filtering must not hide them.

        Each categorization filter needs at least one ID. An empty array is
        rejected with a 400 naming the field — to apply no filter at all, omit
        the property rather than sending `[]`.

        Parent rows additionally include a `subsessions` array with the same
        minimal status-focused shape for each child subsession
        (`SubsessionStatus`). Subsession rows that appear in the flat
        results continue to surface with their own row (for back-compat) and
        carry an empty `subsessions` array.
      tags:
        - Sessions
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                filters:
                  type: object
                  properties:
                    createdAt:
                      $ref: '#/components/schemas/DateRangeFilter'
                    updatedAt:
                      $ref: '#/components/schemas/DateRangeFilter'
                    deletedAt:
                      $ref: '#/components/schemas/DateRangeFilter'
                    status:
                      type: string
                      enum:
                        - accepted
                        - accept_queue
                        - pending
                        - decline_queue
                        - declined
                    trackIds:
                      type: array
                      minItems: 1
                      items:
                        type: string
                        format: uuid
                      description: Match sessions in any of these tracks (OR). Combined with other filters using AND. Evaluated against the top-level session; a deleted root still matches. Send at least one ID — an empty array is rejected with a 400; to apply no track filter, omit the property.
                    levelIds:
                      type: array
                      minItems: 1
                      items:
                        type: string
                        format: uuid
                    formatIds:
                      type: array
                      minItems: 1
                      items:
                        type: string
                        format: uuid
                    languageIds:
                      type: array
                      minItems: 1
                      items:
                        type: string
                        format: uuid
                    tagIds:
                      type: array
                      minItems: 1
                      items:
                        type: string
                        format: uuid
                      description: Match sessions tagged with any of these tag IDs (OR). Send at least one ID — an empty array is rejected with a 400.
                sort:
                  type: object
                  description: |
                    Nested sort keys — the same shape as other search endpoints
                    (`{"createdAt": {"sort": "desc"}}`), not a flat
                    `{"order": "createdAt", "sort": "desc"}` pair.

                    When sorting by `deletedAt`, deleted sessions sort before
                    never-deleted ones (whose `deleted_at` is null) in both
                    directions.
                  properties:
                    createdAt:
                      $ref: '#/components/schemas/NestedSortField'
                    updatedAt:
                      $ref: '#/components/schemas/NestedSortField'
                    deletedAt:
                      $ref: '#/components/schemas/NestedSortField'
                expand:
                  type: array
                  items:
                    type: string
            examples:
              recentlyDeletedFirst:
                summary: Sort by deletion time (nested sort syntax)
                value:
                  filters:
                    status: declined
                  sort:
                    deletedAt:
                      sort: desc
              declinedInTrack:
                summary: Declined sessions in a track (categorization filter)
                description: |
                  IDs within one filter match with OR; separate filters combine
                  with AND. Filters match the top-level session, so subsession
                  rows match via their parent — and a deleted root session
                  still matches.
                value:
                  filters:
                    status: declined
                    trackIds:
                      - 3fa85f64-5717-4562-b3fc-2c963f66afa6
                  sort:
                    deletedAt:
                      sort: desc
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/SessionStatus'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/sessions/{sessionId}:
    get:
      operationId: get-session
      summary: Get a session
      description: |
        Retrieve a single session by ID, optionally expanding related data.

        Every session includes `is_abstract` and `composition_status`. Pass
        `expand=composition` for full linked target and source details.

        Accepts both parent session UUIDs and subsession UUIDs. When called
        with a subsession UUID, the response is a full session-shaped DTO
        for that subsession (with `parent_session_friendly_id` /
        `parent_session_friendly_id_raw` keys and no nested `subsessions`
        array). Subsessions inherit the parent session's `is_abstract`,
        `composition_status`, and optional `composition` graph (when
        `expand=composition` is set) — subsessions cannot participate in
        composition directly.

        This means callers can iterate `parent.subsessions[]` and then call
        this endpoint with each subsession id to retrieve the same shape
        as a parent session.

        Pass `expand=subsession_details` to receive the full
        `SubsessionDetailed` shape on every entry of `parent.subsessions[]`;
        the flag has no effect at the top level — the top-level response
        always returns the full shape regardless of whether the requested
        id is a parent or a subsession.
      tags:
        - Sessions
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The session ID.
        - $ref: '#/components/parameters/Expand'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Session'
        '401':
          description: Unauthorized
        '404':
          description: Session not found
    put:
      operationId: update-session
      summary: Update a session
      description: Update an existing session. Requires the `write:sessions` scope. Send `updated_at` from the last-fetched session for optimistic concurrency control.
      tags:
        - Session Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The session ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                updated_at:
                  type: string
                  format: date-time
                  description: Required for optimistic concurrency control. Send the `updated_at` value from the last-fetched session.
                title:
                  type: string
                description:
                  type: string
                starts_at:
                  type: string
                  format: date-time
                ends_at:
                  type: string
                  format: date-time
                capacity:
                  type: integer
                room_id:
                  type: string
                  format: uuid
                track_id:
                  type: string
                  format: uuid
                status:
                  type: string
                tag_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                is_public:
                  type: boolean
                custom_fields:
                  type: object
                  additionalProperties: true
                  description: |
                    Custom field values keyed by field internal name.

                    For `number`, `currency`, and `percent` fields, an empty
                    string (`""`) clears the stored value — identical to
                    sending `null`. `0` and `"0"` store an actual zero.

                    For `checkbox` fields, send boolean `true` or `false`.
                    On read, a checked box serializes as the string `"Yes"`, an
                    explicitly unchecked one as `"No"`, and a box that was never
                    set as `null`.

                    Fields owned by an integration (for example
                    `swoogo_session_id`, `cvent_session_id`,
                    `visit_session_id`) are hydrated by the connector and
                    reject writes with `400` `FIELD_READ_ONLY`, naming the
                    field in `details.field`. Omit them from `custom_fields`.
                translated_fields:
                  type: array
                  items:
                    $ref: '#/components/schemas/TranslatedFieldWrite'
                  description: |
                    Language variants to write using the same shape returned on reads.
                    Omit this key on update to leave existing translations unchanged.
      responses:
        '200':
          description: Session updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Session'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:sessions scope
        '404':
          description: Session not found
        '409':
          description: Stale update — session modified since last fetch
    delete:
      operationId: delete-session
      summary: Soft-delete a session
      description: Soft-delete a session. Can be restored with the restore endpoint. Requires the `write:sessions` scope.
      tags:
        - Session Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The session ID.
      responses:
        '200':
          description: Session deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:sessions scope
        '404':
          description: Session not found
        '409':
          description: Cannot delete — has active subsessions

  /v1/event/{eventId}/sessions/{sessionId}/fields:
    put:
      operationId: update-session-fields
      summary: Update session custom fields
      description: Update custom field values on a session without touching other session properties. Requires the `write:sessions` scope.
      tags:
        - Session Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The session ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - values
              properties:
                values:
                  type: array
                  minItems: 1
                  description: |
                    Field values to write. Unlike the session `PUT`, this
                    endpoint takes an array of entries rather than a
                    `custom_fields` object, and each entry may carry its own
                    `language` — so this is the endpoint to use for writing a
                    language variant of a single field. An empty or missing
                    array returns `400` `VALIDATION_ERROR`.

                    Value formatting per type matches `custom_fields`
                    elsewhere: an empty string (`""`) clears a `number`,
                    `currency`, or `percent` field; `checkbox` takes boolean
                    `true` / `false`; `datetime` takes an ISO 8601 string.

                    Fields owned by an integration (for example
                    `swoogo_session_id`, `cvent_session_id`,
                    `visit_session_id`) are hydrated by the connector and
                    reject writes with `400` `FIELD_READ_ONLY`, naming the
                    field in `details.field`.
                  items:
                    type: object
                    required:
                      - internal_name
                    properties:
                      internal_name:
                        type: string
                        description: The field's internal name, as listed by `GET /v1/event/{eventId}/fields`.
                      value:
                        description: The value to store. `null` clears it.
                        nullable: true
                      language:
                        type: string
                        description: >
                          Optional locale for a language variant (for example
                          `es_ES`). Must be one of the event's enabled
                          locales. Omit it to write the event default.
            examples:
              single:
                summary: Write one field value
                value:
                  values:
                    - internal_name: track_code
                      value: T-42
              variant:
                summary: Write a Spanish variant of one field
                value:
                  values:
                    - internal_name: session_subtitle
                      value: Sesión de apertura
                      language: es_ES
      responses:
        '200':
          description: |
            Custom fields updated. The response echoes the fields that were
            written rather than the whole session — refetch with
            `GET /v1/event/{eventId}/sessions/{sessionId}` if you need the
            full record.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      session_id:
                        type: string
                        format: uuid
                      updated_fields:
                        type: array
                        items:
                          type: object
                          properties:
                            field_id:
                              type: string
                              format: uuid
                            language:
                              type: string
                              description: Present only when the request specified a language.
        '400':
          description: >
            Validation error — `values` missing or empty, an unknown field
            (`UNKNOWN_FIELD`), a type mismatch (`TYPE_MISMATCH`), or an
            integration-owned field (`FIELD_READ_ONLY`).
        '401':
          description: Unauthorized
        '403':
          description: Missing write:sessions scope
        '404':
          description: Session not found

  # ──────────────────────────────────────────────
  # Speakers
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/speakers:
    post:
      operationId: search-speakers
      summary: Search speakers
      description: |
        Search and filter speakers within an event. Supports the session
        categorization filters (`trackIds`, `tagIds`, `levelIds`, `formatIds`,
        `languageIds`) to match speakers by the sessions they are assigned to.
        `sort` uses the same nested shape as other search endpoints
        (`{"createdAt": {"sort": "desc"}}`).

        Categorization filters are evaluated against the **top-level
        session**, matching session search semantics: a speaker assigned to a
        subsession matches when the subsession's parent (root) session
        satisfies the filter — a subsession's own categorization values are
        not consulted.

        Each categorization filter needs at least one ID. An empty array is
        rejected with a 400 naming the field — to apply no filter at all, omit
        the property rather than sending `[]`.

        Only a session's **speakers** are matched. A contact attached to a
        matching session as a chairperson, moderator or other participant is
        not returned by this endpoint — with or without a filter. The session
        object from `POST /v1/event/{eventId}/sessions` exposes four separate
        participant arrays (`speakers`, `chairpersons`, `moderators`,
        `participants`) and they are not interchangeable here: reconciling
        "speakers on track X" against a session search must compare against
        the `speakers` array alone, or the counts will not agree. The same
        contact can appear under different roles on different sessions, so a
        contact present in the unfiltered speaker list may still be absent
        from a filtered one when its attachment to the matching session is a
        non-speaker role.

        To retrieve every participant regardless of role — chairpersons,
        moderators and custom roles included, each with the roles it holds —
        use `POST /v1/event/{eventId}/participants`. It accepts the same
        filters and returns the same contact fields plus a `roles` array.
      tags:
        - Speakers
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpeakerSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/speakers/{contactId}:
    get:
      operationId: get-speaker
      summary: Get a speaker
      description: Retrieve a single speaker by contact ID.
      tags:
        - Speakers
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: The contact ID.
        - $ref: '#/components/parameters/Expand'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '401':
          description: Unauthorized
        '404':
          description: Speaker not found

  # ──────────────────────────────────────────────
  # Participants
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/participants:
    post:
      operationId: search-participants
      summary: Search participants
      description: |
        Search every contact assigned to at least one session on the event in
        **any** participant role — the standard roles (speaker, chairperson,
        moderator) and any custom role configured for the event (Panelist,
        Coach, Author, …). This is the superset of
        `POST /v1/event/{eventId}/speakers`, which matches speakers only.

        Each contact is returned **once**, however many sessions or roles it
        holds, with a `roles` array listing every role it has on the event and
        the session ids where it holds each one. Every other field is the same
        `Contact` shape `POST /speakers` returns, in the same order, with
        `roles` appended last. `roles` always describes the contact's full
        set of roles on the event — it is not narrowed by the request filters,
        so a search filtered to the Moderator role still shows that a result is
        also a Speaker elsewhere on the program.

        **Filters.** Every Search Speakers filter is accepted and behaves the
        same: `createdAt`, `updatedAt`, `status` (the *session's* status),
        `trackIds`, `levelIds`, `formatIds`, `languageIds`, `tagIds`, plus
        `sort` and `expand`. Categorization filters are evaluated against the
        **top-level session**, so a participant on a subsession matches when
        the subsession's parent satisfies the filter. Each array filter needs
        at least one id — `[]` is a `400` naming the field; omit the property
        to apply no filter.

        Three filters are specific to this endpoint:

        - `roleIds` — return only participants holding **at least one** of the
          given roles, standard or custom. Get the ids from
          `GET /v1/event/{eventId}/participant-roles`.
        - `roleSlugs` — the same, by role slug (`speaker`, `chairperson`,
          `moderator`, or a custom role's slug). Use this to get every speaker
          without resolving ids per event, or on older events where roles have
          `roleId: null`.
        - `email` — exact, case-insensitive match on the contact's email. An
          unknown address returns an empty `results` array with
          `totalResults: 0`, not an error. For a single-record lookup that
          answers `404` instead, use `GET /v1/event/{eventId}/participants?email=`.

        **Roles on older events.** Roles are stored per event. An event that
        has never been opened in the Sessions editor may not have its standard
        roles created yet; its participants are still returned, but a
        standard role on such an event has `roleId: null` (with `roleName`,
        `slug` and `coreRole` filled in) and cannot be matched by `roleIds`
        until the roles exist — filter by `roleSlugs` instead.
        `GET .../participant-roles` returns an empty list for such an event.
      tags:
        - Participants
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ParticipantSearchBody'
            examples:
              byRole:
                summary: Everyone holding the Moderator or a custom role
                value:
                  filters:
                    roleIds:
                      - "4efeb845-050f-4002-9496-cb586cdf10f2"
                      - "52d60237-19db-4bda-a116-8b9fcb6750e9"
                    status: accepted
              bySlug:
                summary: Every speaker, by role slug
                value:
                  filters:
                    roleSlugs:
                      - speaker
              byEmail:
                summary: Match one contact by email
                value:
                  filters:
                    email: "jane.smith@university.edu"
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Participant'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          description: Validation error — an empty filter array, a non-UUID in `roleIds`, or an out-of-range `page` / `pageSize`.
        '401':
          description: Unauthorized
        '404':
          description: Event not found
    get:
      operationId: get-participant-by-email
      summary: Get a participant by email
      description: |
        Look up one participant by email address. The match is exact and
        case-insensitive. Returns the participant, or `404` when no contact
        with that address holds a session role on the event.

        Search on this API is a `POST`, so a `GET` on the collection path is
        free for the lookup — no `/by-email` sub-path is needed. `email` is
        required; a bare `GET /participants` is a `400`, not an unpaginated
        list.

        Contacts are not unique by email. If more than one contact on the
        event shares the address, the earliest-created one is returned; use
        `POST /v1/event/{eventId}/participants` with the `email` filter to see
        them all.
      tags:
        - Participants
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: email
          in: query
          required: true
          schema:
            type: string
            format: email
          description: The contact's email address. Matched exactly, ignoring case.
        - $ref: '#/components/parameters/Expand'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Participant'
        '400':
          description: '`email` is missing or empty.'
        '401':
          description: Unauthorized
        '404':
          description: Event not found, or no participant with that email on the event.

  /v1/event/{eventId}/participants/{contactId}:
    get:
      operationId: get-participant
      summary: Get a participant
      description: |
        Retrieve a single participant by contact ID, with the `roles` it holds
        on the event. `404` when the contact is not on the event, or is on the
        event but holds no session role — a contact returned by
        `GET /v1/event/{eventId}/contacts/{contactId}` is not necessarily a
        participant.
      tags:
        - Participants
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: contactId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The contact ID.
        - $ref: '#/components/parameters/Expand'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Participant'
        '401':
          description: Unauthorized
        '404':
          description: Participant not found on this event

  /v1/event/{eventId}/participant-roles:
    get:
      operationId: list-participant-roles
      summary: List participant roles
      description: |
        The event's participant roles — the three standard roles (Speaker,
        Chairperson, Moderator) and every custom role configured for the
        event — with the ids to use in the `roleIds` filter of
        `POST /v1/event/{eventId}/participants`. Ordered as the Sessions
        settings page shows them. Deleted roles are not returned.

        `is_custom` is `true` for roles the organizer added. `core_role` is
        the standard category a custom role maps to (`speaker`,
        `chairperson` or `moderator`); it decides which legacy array the
        role's participants appear in on `POST /v1/event/{eventId}/sessions`,
        and is not a display label — use `name`.

        An event that has never been opened in the Sessions editor may have no
        roles yet and answers with an empty `results` array. Its participants
        are still returned by `POST .../participants`, with `roleId: null` on
        their standard roles until the roles are created.
      tags:
        - Participants
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/EventParticipantRole'
              examples:
                withCustomRoles:
                  value:
                    results:
                      - id: "84bd12e5-d5ec-4c64-a604-4051e06c8dc2"
                        name: "Speaker"
                        name_plural: "Speakers"
                        slug: "speaker"
                        core_role: "speaker"
                        is_custom: false
                        sort_order: 0
                        created_at: "2026-02-03T18:04:11.120Z"
                        updated_at: "2026-02-03T18:04:11.120Z"
                      - id: "4efeb845-050f-4002-9496-cb586cdf10f2"
                        name: "Moderator"
                        name_plural: "Moderators"
                        slug: "moderator"
                        core_role: "moderator"
                        is_custom: false
                        sort_order: 2
                        created_at: "2026-02-03T18:04:11.120Z"
                        updated_at: "2026-02-03T18:04:11.120Z"
                      - id: "52d60237-19db-4bda-a116-8b9fcb6750e9"
                        name: "Panelist"
                        name_plural: "Panelists"
                        slug: "panelist"
                        core_role: "speaker"
                        is_custom: true
                        sort_order: 3
                        created_at: "2026-02-10T09:41:52.004Z"
                        updated_at: "2026-02-10T09:41:52.004Z"
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  # ──────────────────────────────────────────────
  # Contacts (Organization-level)
  # ──────────────────────────────────────────────
  /v1/organization/{orgId}/contacts:
    post:
      operationId: search-org-contacts
      summary: Search organization contacts
      description: Search and filter contacts across the organization.
      tags:
        - Contacts
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The organization ID (UUID).
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Organization not found

  /v1/organization/{orgId}/contacts/{contactId}:
    get:
      operationId: get-org-contact
      summary: Get an organization contact
      description: Retrieve a single contact by ID at the organization level.
      tags:
        - Contacts
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The organization ID (UUID).
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: The contact ID.
        - $ref: '#/components/parameters/Expand'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '401':
          description: Unauthorized
        '404':
          description: Contact not found

  # ──────────────────────────────────────────────
  # Contacts (Event-level)
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/contacts:
    post:
      operationId: search-event-contacts
      summary: Search event contacts
      description: Search and filter contacts within an event.
      tags:
        - Contacts
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/contacts/{contactId}:
    get:
      operationId: get-event-contact
      summary: Get an event contact
      description: Retrieve a single contact by ID at the event level.
      tags:
        - Contacts
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: The contact ID.
        - $ref: '#/components/parameters/Expand'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '401':
          description: Unauthorized
        '404':
          description: Contact not found
    put:
      operationId: update-contact
      summary: Update a contact
      description: Update an existing contact. Requires the `write:contacts` scope. Optionally send `updated_at` for optimistic concurrency control. Sending `null` for a standard field clears it; omitting a key leaves it unchanged.
      tags:
        - Contact Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: The contact ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                updated_at:
                  type: string
                  format: date-time
                  description: Optional optimistic concurrency control. Send the `updated_at` value from the last-fetched contact.
                first_name:
                  type: string
                last_name:
                  type: string
                email:
                  type: string
                  format: email
                company:
                  type: string
                title:
                  type: string
                phone:
                  type: string
                about:
                  type: string
                  description: Biography. HTML is allowed and stored as rich text.
                salutation:
                  type: string
                honorific:
                  type: string
                address_line_1:
                  type: string
                address_line_2:
                  type: string
                address_city:
                  type: string
                address_state:
                  type: string
                address_postal_code:
                  type: string
                address_country:
                  type: string
                  description: ISO 3166-1 numeric code (as GET returns), alpha-2 or alpha-3 code, or English country name. Stored and returned as the numeric code. An unknown value returns `400` naming the field and nothing is written.
                custom_fields:
                  type: object
                  additionalProperties: true
                  description: |
                    Custom field values keyed by field internal name.

                    For `number`, `currency`, and `percent` fields, an empty
                    string (`""`) clears the stored value — identical to
                    sending `null`. `0` and `"0"` store an actual zero.

                    For `checkbox` fields, send boolean `true` or `false`.
                    On read, a checked box serializes as the string `"Yes"`, an
                    explicitly unchecked one as `"No"`, and a box that was never
                    set as `null`.

                    Fields owned by an integration (for example
                    `swoogo_session_id`, `cvent_session_id`,
                    `visit_session_id`) are hydrated by the connector and
                    reject writes with `400` `FIELD_READ_ONLY`, naming the
                    field in `details.field`. Omit them from `custom_fields`.
                translated_fields:
                  type: array
                  items:
                    $ref: '#/components/schemas/TranslatedFieldWrite'
                  description: |
                    Language variants to write using the same shape returned on reads.
                    Omit this key on update to leave existing translations unchanged.
      responses:
        '200':
          description: Contact updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:contacts scope
        '404':
          description: Contact not found
        '409':
          description: Stale update — contact modified since last fetch
    delete:
      operationId: delete-contact
      summary: Soft-delete a contact
      description: Soft-delete a contact. Can be restored with the restore endpoint. Requires the `write:contacts` scope.
      tags:
        - Contact Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: The contact ID.
      responses:
        '200':
          description: Contact deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:contacts scope
        '404':
          description: Contact not found

  # ──────────────────────────────────────────────
  # Sponsors
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/sponsors:
    post:
      operationId: search-sponsors
      summary: Search sponsors
      description: >
        Search and filter sponsors within an event. Returns the account
        records carrying the Sponsor role on this event — an account
        holding both roles is returned by both searches, and contacts are
        never returned.
      tags:
        - Sponsors
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Sponsor'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/sponsors/{sponsorId}:
    get:
      operationId: get-sponsor
      summary: Get a sponsor
      description: Retrieve a single sponsor by ID.
      tags:
        - Sponsors
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: sponsorId
          in: path
          required: true
          schema:
            type: string
          description: The sponsor ID.
        - $ref: '#/components/parameters/Expand'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Sponsor'
        '401':
          description: Unauthorized
        '404':
          description: >
            No sponsor with this id on this event. A record that exists but
            does not hold the sponsor role — a contact, or a group linked only
            as the other role — is also a `404` here; look it up on the
            endpoint for the role it holds.
    put:
      operationId: update-sponsor
      summary: Update a sponsor
      description: Update an existing sponsor. Requires the `write:sponsors` scope. Optionally send `updated_at` for optimistic concurrency control.
      tags:
        - Sponsor Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: sponsorId
          in: path
          required: true
          schema:
            type: string
          description: The sponsor ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                updated_at:
                  type: string
                  format: date-time
                  description: Optional optimistic concurrency control. Send the `updated_at` value from the last-fetched sponsor.
                name:
                  type: string
                  description: >
                    New sponsor name. Must be a non-empty string when
                    present — an explicit empty string returns `400`. Omit
                    the key to leave the name unchanged.
                custom_fields:
                  type: object
                  additionalProperties: true
                  description: |
                    Custom field values keyed by field internal name.

                    For `number`, `currency`, and `percent` fields, an empty
                    string (`""`) clears the stored value — identical to
                    sending `null`. `0` and `"0"` store an actual zero.

                    For `checkbox` fields, send boolean `true` or `false`.
                    On read, a checked box serializes as the string `"Yes"`, an
                    explicitly unchecked one as `"No"`, and a box that was never
                    set as `null`.

                    Fields owned by an integration (for example
                    `swoogo_session_id`, `cvent_session_id`,
                    `visit_session_id`) are hydrated by the connector and
                    reject writes with `400` `FIELD_READ_ONLY`, naming the
                    field in `details.field`. Omit them from `custom_fields`.
                translated_fields:
                  type: array
                  items:
                    $ref: '#/components/schemas/TranslatedFieldWrite'
                  description: |
                    Language variants to write using the same shape returned on reads.
                    Omit this key on update to leave existing translations unchanged.
      responses:
        '200':
          description: Sponsor updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Sponsor'
              example:
                id: 7f5e2c1a-9b3d-4e6f-8a2b-1c4d5e6f7a8b
                friendly_id: SPO-12
                name: Globex Corporation
                custom_fields: []
        '400':
          description: Validation error — `name`, when present, must be a non-empty string
        '401':
          description: Unauthorized
        '403':
          description: Missing write:sponsors scope
        '404':
          description: >
            No sponsor with this id on this event. A record that exists but
            does not hold the sponsor role — a contact, or a group linked only
            as the other role — is also a `404` here; look it up on the
            endpoint for the role it holds.
        '409':
          description: Stale update — sponsor modified since last fetch
    delete:
      operationId: delete-sponsor
      summary: Soft-delete a sponsor
      description: >
        Soft-delete a sponsor. Can be restored with the restore endpoint.
        Requires the `write:sponsors` scope.


        **This deletes the group, not just its sponsor role.** The path names
        the role you are calling under, but the record that gets soft-deleted
        is the underlying group, so:


        * A group holding **both** roles loses its exhibitor role too. After
          this call it is absent from both searches and both `GET`s answer
          `404`.

        * A group linked to **other events** is removed from those events as
          well — it is no longer returned by any event's sponsor or exhibitor
          search across the organization.


        This matches the admin UI, where the unit of deletion is the group,
        not one of its roles. There is no role-scoped or event-scoped delete
        on this API. If you only want to stop listing a partner for one
        season, do not call this endpoint.


        `POST /v1/event/{eventId}/sponsors/{sponsorId}/restore` reverses it
        with the same reach — both roles and every event come back — so
        nothing is lost permanently.
      tags:
        - Sponsor Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: sponsorId
          in: path
          required: true
          schema:
            type: string
          description: The sponsor ID.
      responses:
        '204':
          description: >
            Sponsor deleted. The response has no body — do not expect a `200`
            or a JSON payload.
        '401':
          description: Unauthorized
        '403':
          description: Missing write:sponsors scope
        '404':
          description: >
            No sponsor with this id on this event. A record that exists but
            does not hold the sponsor role — a contact, or a group linked only
            as the other role — is also a `404` here; look it up on the
            endpoint for the role it holds. An **already-deleted** sponsor is
            also a `404`, so a repeated delete is safe but is not reported as
            a success.

  # ──────────────────────────────────────────────
  # Exhibitors
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/exhibitors:
    post:
      operationId: search-exhibitors
      summary: Search exhibitors
      description: >
        Search and filter exhibitors within an event. Returns the account
        records carrying the Exhibitor role on this event — an account
        holding both roles is returned by both searches, and contacts are
        never returned.
      tags:
        - Exhibitors
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Exhibitor'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/exhibitors/{exhibitorId}:
    get:
      operationId: get-exhibitor
      summary: Get an exhibitor
      description: Retrieve a single exhibitor by ID.
      tags:
        - Exhibitors
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: exhibitorId
          in: path
          required: true
          schema:
            type: string
          description: The exhibitor ID.
        - $ref: '#/components/parameters/Expand'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Exhibitor'
        '401':
          description: Unauthorized
        '404':
          description: >
            No exhibitor with this id on this event. A record that exists but
            does not hold the exhibitor role — a contact, or a group linked only
            as the other role — is also a `404` here; look it up on the
            endpoint for the role it holds.
    put:
      operationId: update-exhibitor
      summary: Update an exhibitor
      description: Update an existing exhibitor. Requires the `write:exhibitors` scope. Optionally send `updated_at` for optimistic concurrency control.
      tags:
        - Exhibitor Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: exhibitorId
          in: path
          required: true
          schema:
            type: string
          description: The exhibitor ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                updated_at:
                  type: string
                  format: date-time
                  description: Optional optimistic concurrency control. Send the `updated_at` value from the last-fetched exhibitor.
                name:
                  type: string
                  description: New exhibitor name. When present it must be a non-empty string — an explicit empty string returns `400`. Omit the key to leave the name unchanged.
                custom_fields:
                  type: object
                  additionalProperties: true
                  description: |
                    Custom field values keyed by field internal name.

                    For `number`, `currency`, and `percent` fields, an empty
                    string (`""`) clears the stored value — identical to
                    sending `null`. `0` and `"0"` store an actual zero.

                    For `checkbox` fields, send boolean `true` or `false`.
                    On read, a checked box serializes as the string `"Yes"`, an
                    explicitly unchecked one as `"No"`, and a box that was never
                    set as `null`.

                    Fields owned by an integration (for example
                    `swoogo_session_id`, `cvent_session_id`,
                    `visit_session_id`) are hydrated by the connector and
                    reject writes with `400` `FIELD_READ_ONLY`, naming the
                    field in `details.field`. Omit them from `custom_fields`.
                translated_fields:
                  type: array
                  items:
                    $ref: '#/components/schemas/TranslatedFieldWrite'
                  description: |
                    Language variants to write using the same shape returned on reads.
                    Omit this key on update to leave existing translations unchanged.
      responses:
        '200':
          description: Exhibitor updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Exhibitor'
              example:
                id: 4b1de9c7-88a2-4e0f-b1f3-6a9d0c2e4f22
                friendly_id: EXH-8
                name: Initech Booth Services
                custom_fields: []
        '400':
          description: Validation error — `name`, when present, must be a non-empty string
        '401':
          description: Unauthorized
        '403':
          description: Missing write:exhibitors scope
        '404':
          description: >
            No exhibitor with this id on this event. A record that exists but
            does not hold the exhibitor role — a contact, or a group linked only
            as the other role — is also a `404` here; look it up on the
            endpoint for the role it holds.
        '409':
          description: Stale update — exhibitor modified since last fetch
    delete:
      operationId: delete-exhibitor
      summary: Soft-delete an exhibitor
      description: >
        Soft-delete an exhibitor. Can be restored with the restore endpoint.
        Requires the `write:exhibitors` scope.


        **This deletes the group, not just its exhibitor role.** The path
        names the role you are calling under, but the record that gets
        soft-deleted is the underlying group, so:


        * A group holding **both** roles loses its sponsor role too. After
          this call it is absent from both searches and both `GET`s answer
          `404`.

        * A group linked to **other events** is removed from those events as
          well — it is no longer returned by any event's sponsor or exhibitor
          search across the organization.


        This matches the admin UI, where the unit of deletion is the group,
        not one of its roles. There is no role-scoped or event-scoped delete
        on this API. If you only want to stop listing a partner for one
        season, do not call this endpoint.


        `POST /v1/event/{eventId}/exhibitors/{exhibitorId}/restore` reverses
        it with the same reach — both roles and every event come back — so
        nothing is lost permanently.
      tags:
        - Exhibitor Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: exhibitorId
          in: path
          required: true
          schema:
            type: string
          description: The exhibitor ID.
      responses:
        '204':
          description: >
            Exhibitor deleted. The response has no body — do not expect a
            `200` or a JSON payload.
        '401':
          description: Unauthorized
        '403':
          description: Missing write:exhibitors scope
        '404':
          description: >
            No exhibitor with this id on this event. A record that exists but
            does not hold the exhibitor role — a contact, or a group linked only
            as the other role — is also a `404` here; look it up on the
            endpoint for the role it holds. An **already-deleted** exhibitor
            is also a `404`, so a repeated delete is safe but is not reported
            as a success.

  # ──────────────────────────────────────────────
  # Event Settings
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/fields:
    get:
      operationId: list-fields
      summary: List fields
      description: Returns all field definitions for an event.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Field'
        '401':
          description: Unauthorized
        '404':
          description: Event not found
    post:
      operationId: search-fields
      summary: Search fields
      description: Search and filter field definitions for an event, including standard and custom fields.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                filters:
                  type: object
                  properties:
                    module:
                      type: string
                      enum:
                        - session
                        - account
                        - contact
                      description: Filter by module type.
                    source:
                      type: string
                      enum:
                        - standard
                        - custom
                      description: Filter by field source.
                    scope:
                      type: string
                      enum:
                        - global
                        - event
                      description: Filter by field scope.
                    createdAt:
                      $ref: '#/components/schemas/DateRangeFilter'
                    updatedAt:
                      $ref: '#/components/schemas/DateRangeFilter'
                sort:
                  $ref: '#/components/schemas/SortOptions'
                expand:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Field'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/tags:
    get:
      operationId: list-tags
      summary: List tags
      description: |
        Returns the event's taxonomy tags (the tags organizers create and apply
        to sessions in the app), paginated, newest first.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Tag'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found
    post:
      operationId: search-tags
      summary: Search tags
      description: Returns a paginated list of tags for an event.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Tag'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/languages:
    get:
      operationId: list-languages
      summary: List languages
      description: |
        Returns the event's languages, newest first.

        Paginated: up to `pageSize` per page (default 25, maximum 100),
        with a `pagination` object alongside the results. This page
        previously described an unpaginated array, which is why a caller
        with more than 25 languages appeared to be missing some.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Language'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found
    post:
      operationId: search-languages
      summary: Search languages
      description: Returns a paginated list of languages configured for an event.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Language'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/formats:
    get:
      operationId: list-formats
      summary: List formats
      description: |
        Returns the event's session formats, newest first.

        Paginated: up to `pageSize` per page (default 25, maximum 100),
        with a `pagination` object alongside the results. This page
        previously described an unpaginated array, which is why a caller
        with more than 25 session formats appeared to be missing some.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Format'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found
    post:
      operationId: search-formats
      summary: Search formats
      description: Returns a paginated list of session formats for an event.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Format'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/tracks:
    get:
      operationId: list-tracks
      summary: List tracks
      description: |
        Returns the event's tracks, newest first.

        Paginated: up to `pageSize` per page (default 25, maximum 100),
        with a `pagination` object alongside the results. This page
        previously described an unpaginated array, which is why a caller
        with more than 25 tracks appeared to be missing some.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Track'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found
    post:
      operationId: search-tracks
      summary: Search tracks
      description: Returns a paginated list of tracks for an event.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Track'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/levels:
    get:
      operationId: list-levels
      summary: List levels
      description: |
        Returns the event's session levels, newest first.

        Paginated: up to `pageSize` per page (default 25, maximum 100),
        with a `pagination` object alongside the results. This page
        previously described an unpaginated array, which is why a caller
        with more than 25 session levels appeared to be missing some.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Level'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found
    post:
      operationId: search-levels
      summary: Search levels
      description: Returns a paginated list of session levels for an event.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Level'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/rooms:
    get:
      operationId: list-rooms
      summary: List rooms
      description: |
        Returns the event's rooms, newest first.

        Paginated: up to `pageSize` per page (default 25, maximum 100),
        with a `pagination` object alongside the results. This page
        previously described an unpaginated array, which is why a caller
        with more than 25 rooms appeared to be missing some.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Room'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found
    post:
      operationId: search-rooms
      summary: Search rooms
      description: Returns a paginated list of rooms for an event.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Room'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/statuses:
    get:
      operationId: list-statuses
      summary: List session statuses
      description: |
        Returns the event's session status definitions, newest first.

        Paginated: up to `pageSize` per page (default 25, maximum 100),
        with a `pagination` object alongside the results. This page
        previously described an unpaginated array, which is why a caller
        with more than 25 session status definitions appeared to be missing some.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SessionCustomStatus'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  /v1/event/{eventId}/session-statuses:
    post:
      operationId: search-session-statuses
      summary: Search session statuses
      description: Returns a paginated list of custom session status definitions for an event.
      tags:
        - Event Settings
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordSearchBody'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/SessionCustomStatus'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Event not found

  # ──────────────────────────────────────────────
  # Metadata Write Operations
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/rooms/create:
    post:
      operationId: create-room
      summary: Create a room
      description: Create a new room. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                capacity: { type: integer }
      responses:
        '200':
          description: Room created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Room'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/rooms/{id}:
    put:
      operationId: update-room
      summary: Update a room
      description: Update a room. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                capacity: { type: integer }
      responses:
        '200':
          description: Room updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Room'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }
    delete:
      operationId: delete-room
      summary: Delete a room
      description: Delete a room. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Room deleted. No response body. }
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/tracks/create:
    post:
      operationId: create-track
      summary: Create a track
      description: Create a new track. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                color: { type: string }
      responses:
        '200':
          description: Track created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Track'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/tracks/{id}:
    put:
      operationId: update-track
      summary: Update a track
      description: Update a track. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                color: { type: string }
      responses:
        '200':
          description: Track updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Track'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }
    delete:
      operationId: delete-track
      summary: Delete a track
      description: Delete a track. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Track deleted. No response body. }
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/tags/create:
    post:
      operationId: create-tag
      summary: Create a tag
      description: Create a new event taxonomy tag. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                color: { type: string }
                order: { type: integer }
      responses:
        '201':
          description: Tag created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Tag'
        '400': { description: Missing required field (name) }
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/tags/{id}:
    put:
      operationId: update-tag
      summary: Update a tag
      description: |
        Update a tag. Requires the `write:metadata` scope.

        Supports optimistic concurrency: pass the `updated_at` you last read
        and the request fails with `409` (`STALE_UPDATE`) if the tag changed
        since then.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                color: { type: string }
                order: { type: integer }
                updated_at:
                  type: string
                  description: Last-read `updated_at` for optimistic concurrency.
      responses:
        '200':
          description: Tag updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Tag'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }
        '404': { description: Tag not found }
        '409': { description: Stale update — tag modified since last fetch (STALE_UPDATE) }
    delete:
      operationId: delete-tag
      summary: Delete a tag
      description: Delete a tag. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Tag deleted. No response body. }
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }
        '404': { description: Tag not found }

  /v1/event/{eventId}/formats/create:
    post:
      operationId: create-format
      summary: Create a format
      description: Create a new session format. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        '200':
          description: Format created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Format'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/formats/{id}:
    put:
      operationId: update-format
      summary: Update a format
      description: Update a session format. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
      responses:
        '200':
          description: Format updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Format'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }
    delete:
      operationId: delete-format
      summary: Delete a format
      description: Delete a session format. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Format deleted. No response body. }
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/levels/create:
    post:
      operationId: create-level
      summary: Create a level
      description: Create a new session level. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        '200':
          description: Level created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Level'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/levels/{id}:
    put:
      operationId: update-level
      summary: Update a level
      description: Update a session level. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
      responses:
        '200':
          description: Level updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Level'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }
    delete:
      operationId: delete-level
      summary: Delete a level
      description: Delete a session level. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Level deleted. No response body. }
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/languages/create:
    post:
      operationId: create-language
      summary: Create a language
      description: Create a new language. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        '200':
          description: Language created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Language'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/languages/{id}:
    put:
      operationId: update-language
      summary: Update a language
      description: Update a language. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
      responses:
        '200':
          description: Language updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Language'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }
    delete:
      operationId: delete-language
      summary: Delete a language
      description: Delete a language. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Language deleted. No response body. }
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/statuses/create:
    post:
      operationId: create-status
      summary: Create a session status
      description: |
        Create a new custom session status. Requires the `write:metadata`
        scope.

        Unlike the other metadata endpoints, `name` alone is **not** enough.
        `color` and `status` are stored NOT NULL with no defaults, so all three
        are required; a payload missing either answers
        `400 VALIDATION_ERROR` naming the field.

        `status` is the underlying approval state this custom label maps onto —
        several labels may share one. It must be one of the five values below;
        anything else is a `400`, not a `500`.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, color, status]
              properties:
                name: { type: string }
                color: { type: string }
                status:
                  type: string
                  description: Approval state this custom status maps onto.
                  enum:
                    - pending
                    - accepted
                    - declined
                    - accept_queue
                    - decline_queue
      responses:
        '201':
          description: Status created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionCustomStatus'
        '400': { description: 'Missing name, color or status, or a status outside the enum' }
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/statuses/{id}:
    put:
      operationId: update-status
      summary: Update a session status
      description: Update a custom session status. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                color: { type: string }
                status:
                  type: string
                  description: Approval state this custom status maps onto.
                  enum:
                    - pending
                    - accepted
                    - declined
                    - accept_queue
                    - decline_queue
      responses:
        '200':
          description: Status updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionCustomStatus'
        '400': { description: A status outside the enum }
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }
    delete:
      operationId: delete-status
      summary: Delete a session status
      description: Delete a custom session status. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Status deleted. No response body. }
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  /v1/event/{eventId}/statuses/{id}/restore:
    post:
      operationId: restore-status
      summary: Restore a deleted session status
      description: Restore a previously soft-deleted session status. Requires the `write:metadata` scope.
      tags: [Metadata Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Status restored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionCustomStatus'
        '401': { description: Unauthorized }
        '403': { description: Missing write:metadata scope }

  # ──────────────────────────────────────────────
  # Agenda Drafts
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/agenda-drafts:
    get:
      operationId: list-agenda-drafts
      summary: List agenda drafts
      description: Returns all agenda drafts for an event.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AgendaDraft'
        '401':
          description: Unauthorized

  /v1/event/{eventId}/agenda-drafts/create:
    post:
      operationId: create-agenda-draft
      summary: Create an agenda draft
      description: Create a new agenda draft workspace. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: Draft name.
                description:
                  type: string
                  description: Optional description.
      responses:
        '200':
          description: Draft created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgendaDraft'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/event/{eventId}/agenda-drafts/{draftId}:
    get:
      operationId: get-agenda-draft
      summary: Get an agenda draft
      description: Retrieve a single agenda draft by ID.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The draft ID.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgendaDraft'
        '401':
          description: Unauthorized
        '404':
          description: Draft not found
    put:
      operationId: update-agenda-draft
      summary: Update an agenda draft
      description: Update an agenda draft. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
      responses:
        '200':
          description: Draft updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgendaDraft'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope
    delete:
      operationId: delete-agenda-draft
      summary: Delete an agenda draft
      description: Delete an agenda draft. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Draft deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/event/{eventId}/agenda-drafts/{draftId}/changes:
    get:
      operationId: get-agenda-draft-changes
      summary: Preview draft changes
      description: Preview the pending changes in a draft before committing them to the live schedule.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                description: A summary of additions, modifications, and removals.
        '401':
          description: Unauthorized

  /v1/event/{eventId}/agenda-drafts/{draftId}/commit:
    post:
      operationId: commit-agenda-draft
      summary: Commit an agenda draft
      description: Apply all changes in the draft to the live event schedule. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Draft committed
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  # ──────────────────────────────────────────────
  # Draft Sessions (within Agenda Drafts)
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/agenda-drafts/{draftId}/sessions:
    get:
      operationId: list-draft-sessions
      summary: List draft sessions
      description: List session placements within an agenda draft.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DraftSession'
        '401':
          description: Unauthorized

  /v1/event/{eventId}/agenda-drafts/{draftId}/sessions/create:
    post:
      operationId: create-draft-session
      summary: Create a draft session
      description: Place a session in an agenda draft. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - session_id
              properties:
                session_id:
                  type: string
                  format: uuid
                starts_at:
                  type: string
                  format: date-time
                ends_at:
                  type: string
                  format: date-time
                room_id:
                  type: string
                  format: uuid
      responses:
        '200':
          description: Draft session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DraftSession'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/event/{eventId}/agenda-drafts/{draftId}/sessions/{draftSessionId}:
    put:
      operationId: update-draft-session
      summary: Update a draft session
      description: Move or update a session placement in a draft. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: draftSessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                starts_at:
                  type: string
                  format: date-time
                ends_at:
                  type: string
                  format: date-time
                room_id:
                  type: string
                  format: uuid
      responses:
        '200':
          description: Draft session updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DraftSession'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope
    delete:
      operationId: delete-draft-session
      summary: Remove a draft session
      description: Remove a session from an agenda draft. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: draftSessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Draft session removed
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/event/{eventId}/agenda-drafts/{draftId}/sessions/bulk:
    post:
      operationId: bulk-draft-sessions
      summary: Bulk draft session operations
      description: Batch create, update, or delete session placements in a draft. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkOperations'
      responses:
        '200':
          description: Bulk operation completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkResponse'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  # ──────────────────────────────────────────────
  # Event Rules
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/rules:
    get:
      operationId: list-event-rules
      summary: List event rules
      description: Returns all scheduling rules for an event.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/EventRule'
        '401':
          description: Unauthorized

  /v1/event/{eventId}/rules/create:
    post:
      operationId: create-event-rule
      summary: Create an event rule
      description: Create a scheduling constraint. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - type
              properties:
                name:
                  type: string
                type:
                  type: string
                config:
                  type: object
                  additionalProperties: true
                enabled:
                  type: boolean
                  default: true
      responses:
        '200':
          description: Rule created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventRule'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/event/{eventId}/rules/{ruleId}:
    get:
      operationId: get-event-rule
      summary: Get an event rule
      description: Retrieve a single event rule by ID.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: ruleId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventRule'
        '401':
          description: Unauthorized
        '404':
          description: Rule not found
    put:
      operationId: update-event-rule
      summary: Update an event rule
      description: Update a scheduling rule. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: ruleId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                config:
                  type: object
                  additionalProperties: true
                enabled:
                  type: boolean
      responses:
        '200':
          description: Rule updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventRule'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope
    delete:
      operationId: delete-event-rule
      summary: Delete an event rule
      description: Delete a scheduling rule. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: ruleId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Rule deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  # ──────────────────────────────────────────────
  # Personas
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/personas:
    get:
      operationId: list-personas
      summary: List personas
      description: Returns all evaluation personas for an event.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Persona'
        '401':
          description: Unauthorized

  /v1/event/{eventId}/personas/create:
    post:
      operationId: create-persona
      summary: Create a persona
      description: Create an attendee persona for schedule evaluation. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                description:
                  type: string
                config:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Persona created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Persona'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/event/{eventId}/personas/{personaId}:
    get:
      operationId: get-persona
      summary: Get a persona
      description: Retrieve a single persona by ID.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: personaId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Persona'
        '401':
          description: Unauthorized
        '404':
          description: Persona not found
    put:
      operationId: update-persona
      summary: Update a persona
      description: Update a persona. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: personaId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                config:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Persona updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Persona'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope
    delete:
      operationId: delete-persona
      summary: Delete a persona
      description: Delete a persona. Requires the `write:events` scope.
      tags:
        - Agenda Planning
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: personaId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Persona deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  # ──────────────────────────────────────────────
  # Dashboard CRUD
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/dashboards:
    get:
      operationId: list-event-dashboards
      summary: List dashboards
      description: Returns all dashboards for an event. Requires the `read:insights` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    name:
                      type: string
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
        '401':
          description: Unauthorized

  /v1/event/{eventId}/dashboards/create:
    post:
      operationId: create-event-dashboard
      summary: Create a dashboard
      description: Create a new dashboard with optional inline widgets. Requires the `write:events` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                description:
                  type: string
                widgets:
                  type: array
                  items:
                    type: object
                  description: Optional inline widget definitions to create with the dashboard.
      responses:
        '200':
          description: Dashboard created
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/event/{eventId}/dashboards/{id}:
    get:
      operationId: get-event-dashboard
      summary: Get a dashboard
      description: Retrieve a dashboard with all widget data. Requires the `read:insights` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  widgets:
                    type: array
                    items:
                      type: object
        '401':
          description: Unauthorized
        '404':
          description: Dashboard not found
    put:
      operationId: update-event-dashboard
      summary: Update a dashboard
      description: Update dashboard properties. Requires the `write:events` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
      responses:
        '200':
          description: Dashboard updated
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope
    delete:
      operationId: delete-event-dashboard
      summary: Delete a dashboard
      description: Delete a dashboard. Requires the `write:events` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Dashboard deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  # ──────────────────────────────────────────────
  # Widget CRUD
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/dashboards/{dashboardId}/widgets/create:
    post:
      operationId: create-widget
      summary: Create a widget
      description: Add a widget to a dashboard. Requires the `write:events` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: dashboardId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - type
              properties:
                type:
                  type: string
                  description: Widget type (e.g., chart, table, number).
                title:
                  type: string
                config:
                  type: object
                  additionalProperties: true
                  description: Widget-specific configuration.
                query:
                  type: string
                  description: SbQL query for the widget data.
      responses:
        '200':
          description: Widget created
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/event/{eventId}/widgets/{widgetId}:
    put:
      operationId: update-widget
      summary: Update a widget
      description: Update widget configuration. Requires the `write:events` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: widgetId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                config:
                  type: object
                  additionalProperties: true
                query:
                  type: string
      responses:
        '200':
          description: Widget updated
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope
    delete:
      operationId: delete-widget
      summary: Delete a widget
      description: Delete a widget from its dashboard. Requires the `write:events` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: widgetId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Widget deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  # ──────────────────────────────────────────────
  # Saved Queries / Reports CRUD
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/queries:
    get:
      operationId: list-event-queries
      summary: List saved queries for an event
      description: Returns saved SbQL queries scoped to an event. Requires the `read:insights` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        query:
                          type: string
                        category:
                          type: string
        '401':
          description: Unauthorized

  /v1/event/{eventId}/queries/create:
    post:
      operationId: create-event-query
      summary: Create a saved query
      description: Save a SbQL query as a reusable report. Requires the `write:events` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - query
              properties:
                name:
                  type: string
                query:
                  type: string
                  description: The SbQL query string.
                category:
                  type: string
      responses:
        '200':
          description: Query saved
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/event/{eventId}/queries/{queryId}:
    put:
      operationId: update-event-query
      summary: Update a saved query
      description: Update a saved query. Requires the `write:events` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: queryId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                query:
                  type: string
                category:
                  type: string
      responses:
        '200':
          description: Query updated
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope
    delete:
      operationId: delete-event-query
      summary: Delete a saved query
      description: Delete a saved query. Requires the `write:events` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: queryId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Query deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/event/{eventId}/queries/{queryId}/run:
    post:
      operationId: run-event-query
      summary: Run a saved query for an event
      description: Execute a saved SbQL query. Requires the `write:events` scope (reports execute through the write pipeline for quota tracking).
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: queryId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                obfuscatePii:
                  type: boolean
                  default: true
                page:
                  type: integer
                limit:
                  type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Query not found

  # ──────────────────────────────────────────────
  # Convenience Dashboard Routes (org resolved from token)
  # ──────────────────────────────────────────────
  /v1/dashboards:
    get:
      operationId: list-org-dashboards-convenience
      summary: List org dashboards (convenience)
      description: Returns all dashboards across all events in the organization. Organization is resolved from the API token. No orgId required. Requires the `read:insights` scope.
      tags:
        - Dashboards & Widgets
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    name:
                      type: string
                    event_id:
                      type: integer
                      nullable: true
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
        '401':
          description: Unauthorized

  /v1/dashboards/create:
    post:
      operationId: create-org-dashboard-convenience
      summary: Create an org dashboard (convenience)
      description: Create a new cross-event dashboard. Organization is resolved from the API token. No orgId required. Requires the `write:events` scope.
      tags:
        - Dashboards & Widgets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                description:
                  type: string
                widgets:
                  type: array
                  items:
                    type: object
                  description: Optional inline widget definitions.
      responses:
        '200':
          description: Dashboard created
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/dashboards/{id}:
    get:
      operationId: get-org-dashboard-convenience
      summary: Get an org dashboard (convenience)
      description: Retrieve a cross-event dashboard with all widget data. Organization is resolved from the API token. No orgId required. Requires the `read:insights` scope.
      tags:
        - Dashboards & Widgets
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  widgets:
                    type: array
                    items:
                      type: object
        '401':
          description: Unauthorized
        '404':
          description: Dashboard not found
    put:
      operationId: update-org-dashboard-convenience
      summary: Update an org dashboard (convenience)
      description: Update dashboard properties. Organization is resolved from the API token. No orgId required. Requires the `write:events` scope.
      tags:
        - Dashboards & Widgets
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
      responses:
        '200':
          description: Dashboard updated
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope
    delete:
      operationId: delete-org-dashboard-convenience
      summary: Delete an org dashboard (convenience)
      description: Delete a cross-event dashboard. Organization is resolved from the API token. No orgId required. Requires the `write:events` scope.
      tags:
        - Dashboards & Widgets
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Dashboard deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  # ──────────────────────────────────────────────
  # Convenience Widget Routes (org resolved from token)
  # ──────────────────────────────────────────────
  /v1/dashboards/{dashboardId}/widgets/create:
    post:
      operationId: create-org-widget-convenience
      summary: Create an org widget (convenience)
      description: Add a widget to an org-level dashboard. Organization is resolved from the API token. No orgId required. Requires the `write:events` scope.
      tags:
        - Dashboards & Widgets
      parameters:
        - name: dashboardId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - type
              properties:
                type:
                  type: string
                  description: Widget type (e.g., chart, table, number).
                title:
                  type: string
                config:
                  type: object
                  additionalProperties: true
                query:
                  type: string
                  description: SbQL query for the widget data.
      responses:
        '200':
          description: Widget created
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/widgets/{widgetId}:
    put:
      operationId: update-org-widget-convenience
      summary: Update an org widget (convenience)
      description: Update widget configuration. Organization is resolved from the API token. No orgId required. Requires the `write:events` scope.
      tags:
        - Dashboards & Widgets
      parameters:
        - name: widgetId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                config:
                  type: object
                  additionalProperties: true
                query:
                  type: string
      responses:
        '200':
          description: Widget updated
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope
    delete:
      operationId: delete-org-widget-convenience
      summary: Delete an org widget (convenience)
      description: Delete a widget. Organization is resolved from the API token. No orgId required. Requires the `write:events` scope.
      tags:
        - Dashboards & Widgets
      parameters:
        - name: widgetId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Widget deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  # ──────────────────────────────────────────────
  # Convenience Query/Report Routes (org resolved from token)
  # ──────────────────────────────────────────────
  /v1/queries:
    get:
      operationId: list-org-queries-convenience
      summary: List org saved queries (convenience)
      description: Returns all saved SbQL queries across the organization. Organization is resolved from the API token. No orgId required. Requires the `read:insights` scope.
      tags:
        - Reports & Queries
      parameters:
        - name: category
          in: query
          required: false
          schema:
            type: string
          description: Filter by category.
        - name: page
          in: query
          required: false
          schema:
            type: integer
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        query:
                          type: string
                        category:
                          type: string
                        event_id:
                          type: integer
                          nullable: true
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized

  /v1/queries/create:
    post:
      operationId: create-org-query-convenience
      summary: Create an org saved query (convenience)
      description: Save a cross-event SbQL query. Organization is resolved from the API token. No orgId required. Requires the `write:events` scope.
      tags:
        - Reports & Queries
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - query
              properties:
                name:
                  type: string
                query:
                  type: string
                  description: The SbQL query string.
                category:
                  type: string
      responses:
        '200':
          description: Query saved
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/queries/{queryId}:
    put:
      operationId: update-org-query-convenience
      summary: Update an org saved query (convenience)
      description: Update a saved query. Organization is resolved from the API token. No orgId required. Requires the `write:events` scope.
      tags:
        - Reports & Queries
      parameters:
        - name: queryId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                query:
                  type: string
                category:
                  type: string
      responses:
        '200':
          description: Query updated
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope
    delete:
      operationId: delete-org-query-convenience
      summary: Delete an org saved query (convenience)
      description: Delete a saved query. Organization is resolved from the API token. No orgId required. Requires the `write:events` scope.
      tags:
        - Reports & Queries
      parameters:
        - name: queryId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Query deleted
        '401':
          description: Unauthorized
        '403':
          description: Missing write:events scope

  /v1/queries/{queryId}/run:
    post:
      operationId: run-org-query-convenience
      summary: Run an org saved query (convenience)
      description: Execute a saved cross-event SbQL query. Organization is resolved from the API token. No orgId required. Requires the `write:events` scope (reports execute through the write pipeline for quota tracking).
      tags:
        - Reports & Queries
      parameters:
        - name: queryId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                obfuscatePii:
                  type: boolean
                  default: true
                page:
                  type: integer
                limit:
                  type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '404':
          description: Query not found

  # ──────────────────────────────────────────────
  # Event-Scoped Insights
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/insights/execute:
    post:
      operationId: execute-event-query
      summary: Execute a SbQL query (event-scoped)
      description: Execute a SbQL query scoped to a specific event. Requires the `read:insights` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID to scope the query to.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - query
              properties:
                query:
                  type: string
                obfuscatePii:
                  type: boolean
                  default: true
                page:
                  type: integer
                limit:
                  type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          description: Invalid query
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/event/{eventId}/insights/ai/generate:
    post:
      operationId: generate-event-query
      summary: Generate a SbQL query from natural language (event-scoped)
      description: Use AI to translate natural language into a SbQL query scoped to a specific event. Requires the `read:insights` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - prompt
              properties:
                prompt:
                  type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  query:
                    type: string
                  explanation:
                    type: string
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/event/{eventId}/insights/schema:
    get:
      operationId: get-event-insights-schema
      summary: Get event-scoped insights schema
      description: Returns the SbQL schema including event-specific custom fields. Requires the `read:insights` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/event/{eventId}/insights/suggestions:
    get:
      operationId: get-event-suggestions
      summary: Get event-scoped query suggestions
      description: Returns contextual query suggestions for a specific event. Requires the `read:insights` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  suggestions:
                    type: array
                    items:
                      type: object
                      properties:
                        query:
                          type: string
                        description:
                          type: string
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/event/{eventId}/insights/queries:
    get:
      operationId: list-event-insights-queries
      summary: List saved queries (event-scoped insights)
      description: Returns saved queries via the insights proxy. Requires the `read:insights` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/event/{eventId}/insights/queries/{queryId}/run:
    post:
      operationId: run-event-insights-query
      summary: Run a saved query (event-scoped insights)
      description: Execute a saved query via the insights proxy. Requires the `read:insights` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: queryId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/event/{eventId}/insights/dashboards/{id}:
    get:
      operationId: get-event-insights-dashboard
      summary: Get a dashboard (event-scoped insights)
      description: Retrieve a dashboard via the insights proxy. Requires the `read:insights` scope.
      tags:
        - Insights
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  widgets:
                    type: array
                    items:
                      type: object
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  # ──────────────────────────────────────────────
  # Ask
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/ask:
    post:
      operationId: ask-event
      summary: Ask a question about an event
      description: |
        Ask a question about one event in plain language and get an answer written for the
        organizer. The answer is grounded in the event's program (sessions, speakers, exhibitors,
        sponsors), session transcripts, the event's knowledge and pinned answers, and the
        Sessionboard Help Center. `[n]` markers in `answer` point at `citations[]` entry `index` n;
        citations are built from records, never from model text.

        Read-only: nothing in the event changes and `actions` is always `[]`.

        Requires the `read:ask` scope, granted **explicitly** — a legacy token with an empty scope
        list does not get it. The token's event allow-list and the organization's AI features
        switch apply.

        **Slow.** An answer takes several seconds and can take up to 90; set your client timeout
        to at least 90 seconds. If Sessionboard does not answer in time the API returns `504`.

        To ask a follow-up in the same thread, send back the `conversation_id` from the previous
        answer. A conversation continues only for the same token and user; any other
        `conversation_id` starts a new conversation.

        The API request log records the call with `question` redacted.
      tags:
        - Ask
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event to ask about.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AskRequest'
            example:
              question: Which sessions on Tuesday cover AI?
              lang: en
      responses:
        '200':
          description: The answer and its sources.
          headers:
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AskResponse'
              example:
                status: ok
                answer: Two sessions on Tuesday cover AI [1] [2].
                citations:
                  - index: 1
                    type: session
                    id: 3f2a9c1e-5b7d-4e8a-9c0f-1a2b3c4d5e6f
                    title: Applied AI in Clinical Practice
                    starts_at: '2026-11-10T14:00:00.000Z'
                  - index: 2
                    type: session
                    id: 8d1e4b2a-6c3f-4a9e-b7d0-2e3f4a5b6c7d
                    title: 'Panel: Where AI Helps and Where It Hurts'
                    starts_at: '2026-11-10T16:30:00.000Z'
                actions: []
                conversation_id: 7b0c0e0c-9a39-4b8f-9d5d-1e2f3a4b5c6d
                message_id: 2c9e7a41-0b3d-4f6e-8a1c-5d4e3f2a1b0c
        '400':
          description: |
            Malformed body: `question` missing, an unknown key, a `conversation_id` that is not a
            UUID, a `lang` that is not a language tag, or a `question` longer than 1000 characters.
        '401':
          description: Missing, invalid, revoked or expired token.
        '403':
          description: |
            - `SCOPE_REQUIRED` — the token does not hold `read:ask` (legacy tokens with no scopes included).
            - `EVENT_NOT_ALLOWED` — the event is outside the token's event allow-list.
            - `AI_DISABLED` — AI features are switched off for the organization.
            - `FORBIDDEN` — Ask is switched off for this event (`details.reason: off`), AI features are off (`details.reason: ai_disabled`), or the organization is out of AI credits under enforcement (`details.reason: ai_credits`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AskError'
              examples:
                scope_required:
                  summary: Token lacks read:ask
                  value:
                    error: SCOPE_REQUIRED
                    message: "This operation requires the 'read:ask' scope."
                event_not_allowed:
                  summary: Event outside the token's allow-list
                  value:
                    error: EVENT_NOT_ALLOWED
                    message: This API token is not allowed to access this event
                ai_disabled:
                  summary: Organization AI features off
                  value:
                    error: AI_DISABLED
                    message: AI features are not enabled for this organization
                ask_off:
                  summary: Ask switched off for the event
                  value:
                    error: FORBIDDEN
                    message: Ask is switched off for this event.
                    details:
                      reason: 'off'
                out_of_credits:
                  summary: Organization out of AI credits
                  value:
                    error: FORBIDDEN
                    message: Out of AI credits. Add credits to continue.
                    details:
                      reason: ai_credits
        '404':
          description: The event does not exist or belongs to another organization.
        '422':
          description: |
            `VALIDATION_ERROR` — Ask would not take the question. Nothing is spent.
            `details.reason` is `empty` (under 2 characters), `too_long` (over 500 characters),
            `blocked` (flagged as unsafe) or `repeated` (the same question asked twice in a row).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AskError'
              example:
                error: VALIDATION_ERROR
                message: Ask could not take that question.
                details:
                  reason: too_long
                  conversation_id: null
        '429':
          description: |
            - `RATE_LIMITED` — the per-user question limit (by default 10 a minute and 200 a day). `details.reason` is `per_minute` or `daily`; wait `Retry-After` seconds (also `details.retry_after_seconds`).
            - `QUOTA_EXCEEDED` — the event's daily Ask budget is used up (`details.reason: busy`). Try again later.
            - The API's own limit of 100 requests per 15 minutes per token, and 20 requests a minute per organization and user, also return `429` with `Retry-After`. That body is the gateway's `TooManyRequestsError` shape (`name`, `message`, `status`, `success`), not an `AskError` — see [Rate Limiting](/rate-limiting).
          headers:
            Retry-After:
              description: Seconds to wait before asking again.
              schema:
                type: integer
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/AskError'
                  - $ref: '#/components/schemas/TooManyRequestsError'
              examples:
                rate_limited:
                  summary: Per-user question limit
                  value:
                    error: RATE_LIMITED
                    message: Too many questions. Try again later.
                    details:
                      reason: per_minute
                      retry_after_seconds: 17
                      remaining_today: 183
                quota_exceeded:
                  summary: Event's daily Ask budget used up
                  value:
                    error: QUOTA_EXCEEDED
                    message: "Today's Ask budget for this event is used up. Try again later."
                    details:
                      reason: busy
                api_rate_limit:
                  summary: API token or organization request limit
                  value:
                    status: 429
                    success: false
                    message: 'Error while trying to reach the endpoint: Too many requests'
                    name: TooManyRequestsError
        '500':
          description: '`INTERNAL_ERROR` — Ask could not answer right now. Retry later.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AskError'
              example:
                error: INTERNAL_ERROR
                message: Ask could not answer right now.
        '504':
          description: No answer within 90 seconds. Retry, or ask a narrower question.

  # ──────────────────────────────────────────────
  # Registration (read:registrations / write:registrations)
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/registration/forms:
    get:
      operationId: list-registration-forms
      summary: List registration forms
      description: |
        Every registration form on the event — attendee, group, presenter, press, exhibitor
        staff and custom forms — in the order organizers arranged them. Each form names the
        ticket types and registrant types it sells, its questions, and whether it is taking
        registrations right now (`is_open_now`).

        `counts.registrations` is the number of registrations placed through the form,
        not counting canceled or transferred ones.

        Not paginated. Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/RegistrationForm'
              example:
                data:
                  - id: 6c1f2d9e-3b4a-4f5e-8a7b-1c2d3e4f5a6b
                    event_id: 7001
                    name: Attendee registration
                    slug: attendee
                    kind: attendee
                    status: open
                    is_open_now: true
                    opens_at: '2026-09-01T09:00:00'
                    closes_at: '2026-11-08T23:59:00'
                    timezone: America/Chicago
                    audience: public
                    invite_code: null
                    requires_code: false
                    ticket_type_ids:
                      - 0b9d6a3c-2e1f-4a5b-9c8d-7e6f5a4b3c2d
                    registrant_type_ids: []
                    questions: []
                    languages: [en]
                    translations: {}
                    settings: {}
                    counts:
                      registrations: 412
                    sort_order: 0
                    created_at: '2026-08-14T15:02:11.000Z'
                    updated_at: '2026-10-20T10:44:09.000Z'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/registration/forms/{formId}:
    get:
      operationId: get-registration-form
      summary: Get a registration form
      description: |
        One registration form, in the same shape as the list.

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The registration form ID.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/RegistrationForm'
        '400':
          description: '`VALIDATION_ERROR` — `formId` is not a UUID.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '404':
          description: '`NOT_FOUND` — no such form on this event.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/ticket-types:
    get:
      operationId: list-ticket-types
      summary: List ticket types
      description: |
        The event's ticket types — tickets, add-ons, booth packages, sponsorships,
        donations, bundles and session fees — with how many are sold and how many are
        left. Active ones only unless `include_archived=true`.

        `sold` counts seats on placed orders; `available` is `capacity - sold` (never below
        0), or `null` when the ticket type has no capacity.

        Not paginated. Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: type
          in: query
          required: false
          schema:
            type: string
          description: |
            Only these types, comma-separated: `ticket`, `addon`, `booth`, `sponsorship`,
            `donation`, `bundle`, `session_fee`. Example: `type=ticket,addon`.
        - name: include_archived
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: 'Also return archived ticket types (`status: archived`).'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TicketType'
              example:
                data:
                  - id: 0b9d6a3c-2e1f-4a5b-9c8d-7e6f5a4b3c2d
                    event_id: 7001
                    type: ticket
                    name: Full conference
                    description: All three days, including the exhibit hall.
                    price_cents: 89500
                    currency: USD
                    capacity: 1200
                    sold: 412
                    available: 788
                    min_per_order: null
                    max_per_order: 10
                    sales_start_at: '2026-09-01T14:00:00.000Z'
                    sales_end_at: null
                    visibility: public
                    status: active
                    sort_order: 0
                    settings: {}
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'
    post:
      operationId: create-ticket-type
      summary: Create a ticket type
      description: |
        Add a ticket type to the event. Only `name` is required; everything else falls back
        to the event's defaults. A new ticket type is not sold on any form until a form lists
        it — organizers add it to forms in Sessionboard.

        Recorded in the event's History as an API change. Requires `write:registrations`.
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TicketTypeWrite'
            example:
              name: Student pass
              type: ticket
              price_cents: 19500
              currency: USD
              capacity: 150
              max_per_order: 1
              visibility: code
      responses:
        '201':
          description: Created. `sold` is `0` on a new ticket type.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TicketType'
        '400':
          description: |
            `VALIDATION_ERROR` — `name` missing or blank, a `type` / `visibility` / `status`
            outside its list, a count or price that is not a non-negative integer, a sales
            date that is not an ISO date, or a price tier in `settings.pricing` without an end
            date and a price.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
              example:
                error: VALIDATION_ERROR
                message: "'price_cents' must be a non-negative integer"
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/ticket-types/{ticketTypeId}:
    put:
      operationId: update-ticket-type
      summary: Update a ticket type
      description: |
        Change some fields of a ticket type. Send only the fields to change; the rest stay.
        `settings` is merged one key deep: a top-level key you send (for example `pricing`)
        replaces the stored one, keys you leave out stay.

        To stop selling a ticket type, set `status: archived`. Lowering `capacity` below
        `sold` does not cancel anything — `available` reads `0`.

        Recorded in the event's History as an API change. Requires `write:registrations`.
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: ticketTypeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The ticket type ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TicketTypeWrite'
            example:
              capacity: 1400
              sales_end_at: '2026-11-08T23:59:00.000Z'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TicketType'
        '400':
          description: |
            `VALIDATION_ERROR` — `ticketTypeId` is not a UUID, `name` sent blank, or a field
            fails the same checks as on create.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '404':
          description: '`NOT_FOUND` — no such ticket type on this event.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/registrations:
    get:
      operationId: list-registrations
      summary: List registrations
      description: |
        Every registration on the event, one per person, oldest first, in every status
        unless you filter. Each registration carries its ticket type, registrant type, form,
        order, answers to the form's questions, badge printing and approval state.

        **Incremental sync.** Pass `updated_since` with the time of your last sync to get
        only registrations changed since then (including that instant). Each registration
        carries `updated_at`; send the latest one you received (or the time you started the
        run) as `updated_since` next time.

        Paginated (`page`, `page_size` up to 200). Requires `read:registrations` (legacy
        tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/RegistrationPage'
        - $ref: '#/components/parameters/RegistrationPageSize'
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: |
            Only these statuses, comma-separated: `incomplete`, `pending_approval`,
            `waitlisted`, `confirmed`, `checked_in`, `canceled`, `transferred`, `declined`,
            `no_show`. Any other value answers `400`.
        - name: form_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only registrations placed through this form.
        - name: ticket_type_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only registrations holding this ticket type.
        - name: registrant_type_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only registrations of this registrant type (for example Member).
        - name: account_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only registrations attached to this exhibitor or sponsor account.
        - name: email
          in: query
          required: false
          schema:
            type: string
          description: Only registrations with this email (case-insensitive, exact match).
        - name: q
          in: query
          required: false
          schema: { type: string, maxLength: 100 }
          description: |
            Find attendees by a fragment of their name (first, last or full), email or company,
            case-insensitive (for example `ada love`).
        - name: updated_since
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: |
            Only registrations changed at or after this time (ISO 8601). A value that is not
            a date is ignored.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Registration'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
              example:
                data:
                  - id: 5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a
                    event_id: 7001
                    status: confirmed
                    first_name: Priya
                    last_name: Raman
                    email: priya.raman@example.org
                    company: Northwind Health
                    job_title: Clinical Director
                    ticket_type:
                      id: 0b9d6a3c-2e1f-4a5b-9c8d-7e6f5a4b3c2d
                      name: Full conference
                    registrant_type: null
                    form:
                      id: 6c1f2d9e-3b4a-4f5e-8a7b-1c2d3e4f5a6b
                      name: Attendee registration
                      kind: attendee
                    order_id: 9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b
                    account_id: null
                    contact_id: 3a2b1c0d-9e8f-4a7b-6c5d-4e3f2a1b0c9d
                    badge_id: null
                    answers:
                      dietary_requirements: Vegetarian
                    session_ids: []
                    membership: null
                    awaiting_assignment: false
                    checked_in_at: null
                    badge:
                      printed: false
                      print_count: 0
                      last_printed_at: null
                    approval: null
                    created_at: '2026-10-02T16:21:40.000Z'
                    updated_at: '2026-10-02T16:21:41.000Z'
                pagination:
                  current_page: 1
                  page_size: 50
                  total_pages: 9
                  total_results: 412
        '400':
          description: '`VALIDATION_ERROR` — `status` names a value outside the list.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
              example:
                error: VALIDATION_ERROR
                message: "'status' must be a comma list of incomplete, pending_approval, waitlisted, confirmed, checked_in, canceled, transferred, declined, no_show"
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'
    post:
      operationId: create-registration
      summary: Create a registration
      description: |
        Place an order on the integrator's behalf: one order for one or more people, each
        with a ticket type, through one of the event's registration forms. Use it to bring in
        registrations taken somewhere else, or to add comps and invoiced registrations.

        **This creates.** `POST /registrations` is not a search.

        `payment` says how the order is paid:

        | `payment` | Order status | Order `payment_method` |
        | --- | --- | --- |
        | `comp` | `paid`, total `0` — every line is fully discounted | `comp` |
        | `invoice` | `awaiting_invoice` (or `paid` when the total is `0`) | `invoice` |
        | `paid_externally` | `paid` — the money was taken in another system | `external` |
        | `cash` | `paid` | `cash` |
        | `check` | `paid` | `check` |

        The order goes through the same checks as a registration on the form: each ticket
        type must be sold on the form and on sale, capacity and per-order limits apply, the
        form's required questions must be answered, and an email can be registered for the
        event only once. The form's open and close dates do **not** apply, and `invoice` is
        accepted even if the form does not offer pay-later. Each registration is `confirmed`,
        or `pending_approval` when the event's approval rules hold it.

        **Idempotency.** Send an `idempotency_key` (a string of 1–128 characters) unique to
        the order in your system. Repeating a request with a key the event has already seen returns the
        original order and registrations with `200` instead of `201`, and creates nothing —
        even if the rest of the body differs.

        Recorded in the event's History as an API change. Requires `write:registrations`.
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegistrationCreateRequest'
            example:
              form_id: 6c1f2d9e-3b4a-4f5e-8a7b-1c2d3e4f5a6b
              payment: paid_externally
              idempotency_key: crm-order-58213
              external_reference: CRM order 58213
              purchaser:
                email: events@northwind.example
                first_name: Sam
                last_name: Ortiz
                company: Northwind Health
              attendees:
                - email: priya.raman@example.org
                  first_name: Priya
                  last_name: Raman
                  company: Northwind Health
                  job_title: Clinical Director
                  ticket_type_id: 0b9d6a3c-2e1f-4a5b-9c8d-7e6f5a4b3c2d
                  answers:
                    dietary_requirements: Vegetarian
      responses:
        '201':
          description: Created — the order and one registration per entry in `attendees`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/RegistrationCreateResult'
        '200':
          description: |
            Idempotent replay — an order with this `idempotency_key` already exists on the
            event. The original order and its registrations are returned; nothing is created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/RegistrationCreateResult'
        '400':
          description: |
            `VALIDATION_ERROR`:
            - `payment` is not `comp`, `invoice`, `paid_externally`, `cash` or `check`.
            - `form_id` is not a UUID.
            - `attendees` is missing or empty, has more than 100 entries, or an entry has no
              `email` or no UUID `ticket_type_id`.
            - `idempotency_key` is not a string of 1–128 characters.
            - Answers are missing or invalid — `details.reason: answers_invalid`, with the
              problems in `details.errors`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
              examples:
                bad_payment:
                  summary: Unknown payment
                  value:
                    error: VALIDATION_ERROR
                    message: "'payment' must be comp, invoice, paid_externally, cash or check"
                bad_attendee:
                  summary: Attendee without a ticket type
                  value:
                    error: VALIDATION_ERROR
                    message: attendees[0] needs an email and a ticket_type_id
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '404':
          description: '`NOT_FOUND` — no such form on this event.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '409':
          description: |
            `CONFLICT` — the order cannot be placed. `details.reason` says why:
            - `duplicate` — an email is already registered for the event, or appears twice in
              `attendees` (`details.errors.email` names it).
            - `sold_out` — a ticket type has no seats left (`details.errors.ticket_type_id`).
            - `too_many` — more of a ticket type than its per-order maximum.
            - `ticket_unavailable` — a ticket type or add-on is not sold on this form.
            - `not_on_sale` — a ticket type is outside its sales dates.
            - `registrant_type_unavailable` — the form does not offer that registrant type.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
              example:
                error: CONFLICT
                message: priya.raman@example.org is already registered.
                details:
                  reason: duplicate
                  errors:
                    email: priya.raman@example.org
        '422':
          description: |
            `VALIDATION_ERROR` — the `discount_code` is not valid for this order
            (`details.reason: discount_invalid`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
              example:
                error: VALIDATION_ERROR
                message: That promo code is not valid.
                details:
                  reason: discount_invalid
                  errors:
                    reason: not_found
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/registrations/{registrationId}:
    get:
      operationId: get-registration
      summary: Get a registration
      description: |
        One registration, in the same shape as the list.

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/RegistrationId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Registration'
        '400':
          description: '`VALIDATION_ERROR` — `registrationId` is not a UUID.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '404':
          description: '`NOT_FOUND` — no such registration on this event.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'
    patch:
      operationId: update-registration
      summary: Update a registration
      description: |
        Correct a registrant's name, company or job title, and approve, decline or cancel the
        registration. Send any combination; the whole request applies or none of it does.
        The email cannot be changed here.

        `action`:
        - `approve` — a `pending_approval` registration becomes `confirmed`. Any other status
          answers `409`.
        - `decline` — a `pending_approval` or `confirmed` registration becomes `declined`.
          If the event's approval workflow has a request for the registration, the decision
          is recorded there and its notifications go out. Any other status (including
          `waitlisted`) answers `409`.
        - `cancel` — the registration becomes `canceled`. Already `canceled` or `transferred`
          registrations are left as they are (`200`). Cancelling does not refund the order.

        `note` goes on the approval decision.

        Recorded in the event's History as an API change. Requires `write:registrations`.
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/RegistrationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegistrationUpdateRequest'
            examples:
              approve:
                summary: Approve a held registration
                value:
                  action: approve
              rename:
                summary: Correct the job title
                value:
                  job_title: Chief Medical Officer
      responses:
        '200':
          description: The registration after the change.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Registration'
        '400':
          description: |
            `VALIDATION_ERROR` — `registrationId` is not a UUID, or `action` is not `approve`,
            `decline` or `cancel`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '404':
          description: '`NOT_FOUND` — no such registration on this event.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '409':
          description: |
            `CONFLICT` — the registration's status does not allow the action
            (`details.reason: wrong_status`). Nothing in the request is applied.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
              example:
                error: CONFLICT
                message: This registration is confirmed
                details:
                  reason: wrong_status
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/orders:
    get:
      operationId: list-orders
      summary: List orders
      description: |
        Every order on the event, oldest first, with its lines, totals, payments, invoice and
        the registrations it bought. Amounts are integers in the smallest currency unit
        (cents).

        Paginated (`page`, `page_size` up to 200). Requires `read:registrations` (legacy
        tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/RegistrationPage'
        - $ref: '#/components/parameters/RegistrationPageSize'
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: |
            Only these statuses, comma-separated: `draft`, `pending_payment`,
            `awaiting_invoice`, `partially_paid`, `paid`, `partially_refunded`, `refunded`,
            `canceled`, `expired`. `awaiting_invoice,partially_paid` lists orders that still owe.
        - name: account_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only orders attached to this exhibitor or sponsor account.
        - name: number
          in: query
          required: false
          schema: { type: string }
          description: The order number shown on receipts and invoices (exact match).
        - name: email
          in: query
          required: false
          schema: { type: string }
          description: Only orders bought by this email (case-insensitive, exact match).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Order'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
              example:
                data:
                  - id: 9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b
                    number: R-1042
                    event_id: 7001
                    status: paid
                    purchaser:
                      type: contact
                      id: 1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e
                      email: events@northwind.example
                      name: Sam Ortiz
                      company: Northwind Health
                    currency: USD
                    subtotal_cents: 89500
                    discount_cents: 0
                    tax_cents: 0
                    total_cents: 89500
                    paid_cents: 89500
                    balance_cents: 0
                    payment_method: external
                    discount: null
                    lines:
                      - key: seat-0
                        line_item_id: 4f3e2d1c-0b9a-4876-a5b4-c3d2e1f0a9b8
                        ticket_type_id: 0b9d6a3c-2e1f-4a5b-9c8d-7e6f5a4b3c2d
                        type: ticket
                        label: Full conference
                        tier_label: null
                        quantity: 1
                        unit_price_cents: 89500
                        discount_cents: 0
                        tax_cents: 0
                        total_cents: 89500
                        attendee_index: 0
                        session_id: null
                        gl_code: null
                    registrations:
                      - id: 5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a
                        status: confirmed
                        first_name: Priya
                        last_name: Raman
                        email: priya.raman@example.org
                        awaiting_assignment: false
                    invoice: null
                    form_id: 6c1f2d9e-3b4a-4f5e-8a7b-1c2d3e4f5a6b
                    account_id: null
                    payment_mode: external
                    created_at: '2026-10-02T16:21:40.000Z'
                pagination:
                  current_page: 1
                  page_size: 50
                  total_pages: 7
                  total_results: 318
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/orders/{orderId}:
    get:
      operationId: get-order
      summary: Get an order
      description: |
        One order, in the same shape as the list.

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: orderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The order ID (not the order `number`).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Order'
        '400':
          description: '`VALIDATION_ERROR` — `orderId` is not a UUID.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '404':
          description: '`NOT_FOUND` — no such order on this event.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/registration/summary:
    get:
      operationId: get-registration-summary
      summary: Get the registration summary
      description: |
        The event's registration numbers in one call — the same figures as the Registration
        Overview page: registrations by status, per day (event time zone), per ticket type
        (sold, capacity, revenue), per attendee type and per form, the member mix, revenue
        collected and refunded, orders per status, invoices still owed and shared capacity
        pools. Amounts are integers in cents.

        `totals.registrations` counts every live registration (`incomplete`, `pending_approval`,
        `waitlisted`, `confirmed`, `checked_in`).

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/RegistrationSummary'
              example:
                data:
                  totals: { registrations: 318, confirmed: 290, pending_approval: 12, waitlisted: 6, checked_in: 0, canceled: 9 }
                  revenue: { currency: USD, gross_cents: 25841000, refunded_cents: 89500, outstanding_invoices_cents: 1790000 }
                  by_day:
                    - { date: '2026-10-01', registrations: 41 }
                    - { date: '2026-10-02', registrations: 17 }
                  by_ticket_type:
                    - { id: 0b9d6a3c-2e1f-4a5b-9c8d-7e6f5a4b3c2d, name: Full conference, type: ticket, sold: 280, capacity: 400, revenue_cents: 25060000 }
                  by_registrant_type:
                    - { id: 3a2b1c0d-9e8f-4a7b-6c5d-4e3f2a1b0c9d, name: Member, count: 201 }
                  by_form:
                    - { id: 6c1f2d9e-3b4a-4f5e-8a7b-1c2d3e4f5a6b, name: Attendee, count: 318 }
                  membership: { member: 201, lapsed: 14, non_member: 103, joined: 9 }
                  signals: { emitted: 702, registered: 318, interests_declared: 120, session_picks: 260, checked_in: 0, badges_printed: 4 }
                  orders:
                    by_status:
                      - { status: paid, count: 288, total_cents: 25751500, paid_cents: 25751500 }
                      - { status: awaiting_invoice, count: 9, total_cents: 1790000, paid_cents: 0 }
                  outstanding_invoices: { count: 9, overdue: 2, balance_cents: 1790000 }
                  capacity_pools: []
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/payments:
    get:
      operationId: list-payments
      summary: List payments
      description: |
        The payment records of the event's registration orders, oldest first: card and
        bank-account charges and refunds made through your organization's own payment
        gateway (Sessionboard connects to it through Spreedly), and money recorded offline
        (check, cash, paid elsewhere, comp).

        A record never carries a card number, a payment or gateway token, or a gateway
        credential. `card_brand` and `last_four` are filled only when the gateway reported
        them and Sessionboard stored them; otherwise they are `null`.

        A failed card attempt is its own record with `status: failed` and a `failure` code,
        so this list is also the failed-transaction log. A refund names the charge it
        refunds in `parent_payment_id`.

        Paginated (`page`, `page_size` up to 200). Requires `read:registrations` (legacy
        tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/RegistrationPage'
        - $ref: '#/components/parameters/RegistrationPageSize'
        - name: status
          in: query
          required: false
          schema: { type: string }
          description: |
            Only these statuses, comma-separated: `pending`, `processing`, `succeeded`,
            `failed`, `refunded`, `canceled`. An unknown value is a `400`.
        - name: type
          in: query
          required: false
          schema: { type: string }
          description: |
            Only these record types, comma-separated: `charge`, `refund`, `authorize`,
            `capture`, `void`. An unknown value is a `400`.
        - name: order_id
          in: query
          required: false
          schema: { type: string, format: uuid }
          description: Only this order's payments.
        - name: method
          in: query
          required: false
          schema: { type: string }
          description: Only these methods, comma-separated (e.g. `card`, `check`, `cash`, `external`).
        - name: updated_since
          in: query
          required: false
          schema: { type: string, format: date-time }
          description: Only records created or changed at or after this time.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Payment'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
              example:
                data:
                  - id: 2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f
                    order_id: 9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b
                    order_number: R-1042
                    type: charge
                    status: succeeded
                    amount_cents: 89500
                    currency: USD
                    method: card
                    method_type: card
                    processor: spreedly
                    gateway_type: stripe
                    test_mode: false
                    card_brand: visa
                    last_four: '4242'
                    installment: null
                    parent_payment_id: null
                    reference: null
                    failure: null
                    created_at: '2026-10-02T16:21:41.000Z'
                    updated_at: '2026-10-02T16:21:43.000Z'
                pagination: { current_page: 1, page_size: 50, total_pages: 1, total_results: 1 }
        '400':
          description: '`VALIDATION_ERROR` — an unknown `status` or `type`, or `order_id` is not a UUID.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/payments/{paymentId}:
    get:
      operationId: get-payment
      summary: Get a payment
      description: |
        One payment record, in the same shape as the list.

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: paymentId
          in: path
          required: true
          schema: { type: string, format: uuid }
          description: The payment record ID.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Payment'
        '400':
          description: '`VALIDATION_ERROR` — `paymentId` is not a UUID.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '404':
          description: '`NOT_FOUND` — no such payment on this event.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/registration/members:
    get:
      operationId: list-registration-members
      summary: List membership of registrations
      description: |
        Each registration with the membership Registration recorded when the person
        registered — `member`, `lapsed` or `none` — which decided member pricing and the
        attendee type. `membership_enabled` says whether the event uses member pricing at all.

        Paginated (`page`, `page_size` up to 200). Requires `read:registrations` (legacy
        tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/RegistrationPage'
        - $ref: '#/components/parameters/RegistrationPageSize'
        - name: membership
          in: query
          required: false
          schema: { type: string }
          description: 'Only these, comma-separated: `member`, `lapsed`, `none`. An unknown value is a `400`.'
        - name: status
          in: query
          required: false
          schema: { type: string }
          description: Only registrations in these statuses, comma-separated.
        - name: email
          in: query
          required: false
          schema: { type: string }
          description: Only registrations with this email (case-insensitive, exact match).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/RegistrationMembership'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
                  membership_enabled:
                    type: boolean
                    description: Whether the event uses member pricing.
              example:
                data:
                  - registration_id: 5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a
                    status: confirmed
                    email: priya.raman@example.org
                    first_name: Priya
                    last_name: Raman
                    registrant_type_id: 3a2b1c0d-9e8f-4a7b-6c5d-4e3f2a1b0c9d
                    membership: member
                    member_id: null
                    created_at: '2026-10-02T16:21:40.000Z'
                pagination: { current_page: 1, page_size: 50, total_pages: 1, total_results: 1 }
                membership_enabled: true
        '400':
          description: '`VALIDATION_ERROR` — an unknown `membership` value.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/registration/members/lookup:
    get:
      operationId: lookup-registration-member
      summary: Look up a member
      description: |
        Checks one email the way the registration form does before pricing a ticket: is
        this person a `member`, `lapsed` or neither (`none`) according to your organization's
        member records, which attendee type applies, and which registrations they already
        hold for this event. Nothing is written.

        `status_value` is the member-status value that decided the answer (for example
        `Active`); no other member attribute is returned. When the event does not use member
        pricing, `membership_enabled` is `false` and `result` is `none`.

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: email
          in: query
          required: true
          schema: { type: string, format: email }
          description: The email to check (case-insensitive).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/MembershipLookup'
              example:
                data:
                  email: priya.raman@example.org
                  membership_enabled: true
                  result: member
                  applied: member
                  registrant_type_id: 3a2b1c0d-9e8f-4a7b-6c5d-4e3f2a1b0c9d
                  status_value: Active
                  member_id: null
                  registrations:
                    - { id: 5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a, status: confirmed, membership: member, created_at: '2026-10-02T16:21:40.000Z' }
        '400':
          description: '`VALIDATION_ERROR` — `email` is missing or not an email address.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/registration/capacity-pools:
    get:
      operationId: list-capacity-pools
      summary: List capacity pools
      description: |
        Shared capacity pools: several ticket types drawing on one number ("Full conference,
        Member and One-day together cap at 800"). Each pool has its capacity, seats sold,
        seats left, percent full, and the sold count of every ticket type in it. A cart must
        fit both the ticket type's own capacity and every pool it belongs to.

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CapacityPool'
              example:
                data:
                  - id: pool-1
                    name: Thursday lunches
                    capacity: 400
                    sold: 362
                    left: 38
                    percent: 91
                    tickets:
                      - { ticket_type_id: 7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d, name: Lunch A, status: active, own_capacity: 200, sold: 190 }
                      - { ticket_type_id: 8b7c6d5e-4f3a-4b2c-8d9e-0f1a2b3c4d5e, name: Lunch B, status: active, own_capacity: 250, sold: 172 }
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/registration/fee-groups:
    get:
      operationId: list-fee-groups
      summary: List fee groups
      description: |
        Fee groups are labels on ticket types and add-ons (for example `meals`) that promo
        codes can be limited to. Each group lists its ticket types and the promo codes that
        apply to it. A ticket type without a fee group is not listed.

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FeeGroup'
              example:
                data:
                  - name: meals
                    ticket_types:
                      - { id: 7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d, name: Lunch A, type: addon, status: active }
                    promo_codes:
                      - { id: 1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b, code: MEALS10, kind: percent, value: 10, status: active }
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/registration/donation-funds:
    get:
      operationId: list-donation-funds
      summary: List donation funds
      description: |
        Every donation ticket type with its open-amount limits (minimum, maximum, suggested
        amounts) and its funds, and for each fund the number of gifts, what has been raised
        (orders paid) and what is pledged (orders still to pay), in cents. `unassigned` counts
        gifts that name no fund.

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DonationFee'
              example:
                data:
                  - ticket_type_id: 2d3e4f5a-6b7c-4d8e-9f0a-1b2c3d4e5f6a
                    name: Give to the Foundation
                    status: active
                    currency: USD
                    open_amount: true
                    min_cents: 500
                    max_cents: 100000000
                    suggested_cents: [2500, 5000, 10000]
                    funds:
                      - { id: f1, name: Scholarships, gl_code: '4900', gifts: 41, raised_cents: 205000, pledged_cents: 10000 }
                    unassigned: { gifts: 0, raised_cents: 0, pledged_cents: 0 }
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/registration/workshop-rules:
    get:
      operationId: get-workshop-rules
      summary: Get workshop rules
      description: |
        How attendees pick workshops on the registration form: whether sessions without a
        fee and general sessions are offered, required picks per time slot, what happens when
        two picks overlap (`off`, `warn`, `block`), per-session visibility by ticket type or
        attendee type, and whether attendees may change paid picks themselves and until when.

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/WorkshopRules'
              example:
                data:
                  include_free_sessions: true
                  general_session_ids: []
                  overlap_mode: block
                  slots:
                    - { id: thu-am, label: Thursday morning, session_ids: [4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8], min_picks: 1, max_picks: 1 }
                  session_rules: []
                  self_service: { enabled: true, until: '2026-11-01T23:59:00.000Z' }
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/badge-print-jobs:
    get:
      operationId: list-badge-print-jobs
      summary: List badge print jobs
      description: |
        The event's badge print stations with their queue counts, and the latest 200 badge
        print jobs, newest first (`queued`, `printing`, `printed`, `failed`, `canceled`).
        Read-only.

        Requires `read:registrations` (legacy tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: station_id
          in: query
          required: false
          schema: { type: string }
          description: Only jobs sent to this station.
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [queued, printing, printed, failed, canceled]
          description: Only jobs in this status. Any other value is a `400`.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/BadgePrintQueue'
              example:
                data:
                  stations:
                    - { id: hall-a, name: Hall A, stock: cr80, template_id: null, offset_x_in: 0, offset_y_in: 0, queued: 2, printing: 0, failed: 1 }
                  jobs:
                    - { id: 6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0, station_id: hall-a, status: failed, attendee_id: 5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a, first_name: Priya, last_name: Raman, company: Northwind Health, attempts: 1, error: Out of stock, created_at: '2026-11-10T13:01:00.000Z', claimed_at: '2026-11-10T13:01:05.000Z', printed_at: null }
        '400':
          description: '`VALIDATION_ERROR` — an unknown `status`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/attendees:
    get:
      operationId: list-attendees
      summary: List attendees
      description: |
        Who is coming: registrations that are `confirmed` or `checked_in`, sorted by last
        name, then first name. A short shape meant for badge printers, door lists and
        lead-retrieval apps — use [List registrations](/api-reference/registration/list-registrations)
        for answers, orders and every other status.

        Takes the same filters as List registrations except `status`.

        Paginated (`page`, `page_size` up to 200). Requires `read:registrations` (legacy
        tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/RegistrationPage'
        - $ref: '#/components/parameters/RegistrationPageSize'
        - name: form_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only attendees registered through this form.
        - name: ticket_type_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only attendees holding this ticket type.
        - name: registrant_type_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only attendees of this registrant type.
        - name: account_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only attendees attached to this exhibitor or sponsor account (booth staff).
        - name: email
          in: query
          required: false
          schema:
            type: string
          description: Only attendees with this email (case-insensitive, exact match).
        - name: updated_since
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: Only attendees changed at or after this time (ISO 8601). A value that is not a date is ignored.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Attendee'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
              example:
                data:
                  - id: 5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a
                    status: confirmed
                    first_name: Priya
                    last_name: Raman
                    email: priya.raman@example.org
                    company: Northwind Health
                    job_title: Clinical Director
                    ticket_type_id: 0b9d6a3c-2e1f-4a5b-9c8d-7e6f5a4b3c2d
                    registrant_type_id: null
                    account_id: null
                    checked_in_at: null
                    updated_at: '2026-10-02T16:21:41.000Z'
                pagination:
                  current_page: 1
                  page_size: 50
                  total_pages: 8
                  total_results: 397
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/check-ins:
    get:
      operationId: list-check-ins
      summary: List check-ins
      description: |
        Attendees who are checked in, in check-in order (`checked_in_at`, oldest first). Poll
        with `since` to pick up new check-ins from Sessionboard's own door app, badge kiosks
        and other integrations.

        `since` compares against `checked_in_at`, so a later edit to a checked-in
        registration does not return it again. The registration `id` is the stable key for
        each check-in.

        A check-in that is undone at the door leaves this list (the registration returns to
        `confirmed`).

        Paginated (`page`, `page_size` up to 200). Requires `read:registrations` (legacy
        tokens with no scopes pass).
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/RegistrationPage'
        - $ref: '#/components/parameters/RegistrationPageSize'
        - name: since
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: Only check-ins made at or after this time (`checked_in_at`, ISO 8601). A value that is not a date is ignored.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Attendee'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
              example:
                data:
                  - id: 5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a
                    status: checked_in
                    first_name: Priya
                    last_name: Raman
                    email: priya.raman@example.org
                    company: Northwind Health
                    job_title: Clinical Director
                    ticket_type_id: 0b9d6a3c-2e1f-4a5b-9c8d-7e6f5a4b3c2d
                    registrant_type_id: null
                    account_id: null
                    checked_in_at: '2026-11-10T13:02:55.000Z'
                    updated_at: '2026-11-10T13:02:55.412Z'
                pagination:
                  current_page: 1
                  page_size: 50
                  total_pages: 1
                  total_results: 1
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'
    post:
      operationId: create-check-in
      summary: Check someone in
      description: |
        Record that a person arrived — from a partner's scanner, kiosk or door app. Name the
        registration one of three ways, in this order of precedence:

        1. `registration_id` — the registration's ID.
        2. `email` — the registrant's email, among registrations that are `confirmed`,
           `checked_in`, `pending_approval` or `waitlisted`.
        3. `badge_code` — the code scanned from the attendee's QR: the `sb-checkin:<id>` code
           in the confirmation email, or the code on a printed or in-app badge.

        A `confirmed`, `pending_approval` or `waitlisted` registration becomes `checked_in`,
        with `checked_in_at` set to `checked_in_at` from the request or now.

        **Idempotent.** Checking in someone already checked in changes nothing and answers
        `200` with `already_checked_in: true`; the original `checked_in_at` stays.

        A registration in any other status (`canceled`, `declined`, `transferred`,
        `incomplete`, `no_show`) is not checked in and answers `409 CONFLICT` with
        `details.reason: wrong_status` and the registration's status in `details.status`.

        Recorded in the event's History as an API change. Requires `write:registrations`.
      tags: [Registration]
      parameters:
        - $ref: '#/components/parameters/EventId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckInRequest'
            examples:
              by_badge:
                summary: Scanned badge
                value:
                  badge_code: sb-checkin:5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a
              by_email:
                summary: Looked up by email, with the scan time
                value:
                  email: priya.raman@example.org
                  checked_in_at: '2026-11-10T13:02:55Z'
      responses:
        '201':
          description: Checked in.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CheckIn'
              example:
                data:
                  id: 5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a
                  status: checked_in
                  first_name: Priya
                  last_name: Raman
                  email: priya.raman@example.org
                  company: Northwind Health
                  job_title: Clinical Director
                  ticket_type_id: 0b9d6a3c-2e1f-4a5b-9c8d-7e6f5a4b3c2d
                  registrant_type_id: null
                  account_id: null
                  checked_in_at: '2026-11-10T13:02:55.000Z'
                  updated_at: '2026-11-10T13:02:55.412Z'
                  already_checked_in: false
        '200':
          description: 'Already checked in — nothing changed (`already_checked_in: true`).'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CheckIn'
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/RegistrationForbidden'
        '404':
          description: |
            `NOT_FOUND` — no registration on this event matches the `registration_id`,
            `email` or `badge_code`, or none of them was sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
              example:
                error: NOT_FOUND
                message: No registration matches that registration, badge or email
        '409':
          description: |
            `CONFLICT` — the registration's status does not allow check-in
            (`details.reason: wrong_status`; `details.status` names the status). Nothing changes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
              example:
                error: CONFLICT
                message: This registration is canceled and cannot be checked in
                details:
                  reason: wrong_status
                  status: canceled
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  # ──────────────────────────────────────────────
  # Expo (read:expo / write:expo)
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/booths:
    get:
      operationId: list-booths
      summary: List booths
      description: |
        The booths drawn on the event's floor maps, by booth number, with their status, price,
        the booth package they are sold in, and the exhibitor or sponsor holding them.

        `status` is the booth's status now: a `hold` whose `hold_until` has passed reads
        `available`. `stored_status` is the status as last set.

        Not paginated. Requires `read:expo` (legacy tokens with no scopes pass).
      tags: [Expo]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: floor_map_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Only booths on this floor map.
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: |
            Only booths whose current `status` is one of these, comma-separated:
            `available`, `hold`, `reserved`, `sold`, `blocked`.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Booth'
              example:
                data:
                  - id: 2f1e0d9c-8b7a-4695-a4b3-c2d1e0f9a8b7
                    floor_map_id: 7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d
                    hall_id: 5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e
                    number: '1207'
                    status: reserved
                    stored_status: reserved
                    booth_type: linear
                    list_price_cents: null
                    price_cents: 450000
                    currency: USD
                    hold_until: null
                    geom: {}
                    fill_color: null
                    product:
                      id: 8c7b6a5f-4e3d-4c2b-a1f0-e9d8c7b6a5f4
                      name: 10x10 inline booth
                      price_cents: 450000
                    account:
                      id: e1d2c3b4-a5f6-4789-8a9b-0c1d2e3f4a5b
                      name: Contoso Diagnostics
                    application_id: null
                    order_id: null
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/ExpoForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/booths/{boothId}:
    put:
      operationId: update-booth
      summary: Update a booth
      description: |
        Hold, reserve, sell, block or release a booth, assign it to an exhibitor or sponsor,
        set its own price, or move it to another booth package. Send only the fields to
        change.

        - `available` releases the booth: its account and hold are cleared, unless you send
          an `account_id` in the same request.
        - `reserved` and `sold` need an account — the one already on the booth, or
          `account_id`.
        - `hold` with `hold_until` keeps the booth for the account until then; after that the
          booth reads `available`.
        - `account_id: null` removes the account.

        `account_id` must be an exhibitor or sponsor of this event. `product_id` must be a
        booth package of this event (a ticket type of type `booth`); the booth moves into that
        package and out of any other. `product_id: null` takes it out of every package.

        Recorded in the event's History as an API change. Requires `write:expo`.
      tags: [Expo]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: boothId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The booth ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BoothUpdateRequest'
            examples:
              hold:
                summary: Hold for an exhibitor for a week
                value:
                  status: hold
                  account_id: e1d2c3b4-a5f6-4789-8a9b-0c1d2e3f4a5b
                  hold_until: '2026-11-03T23:59:00Z'
              release:
                summary: Release
                value:
                  status: available
      responses:
        '200':
          description: The booth after the change, in the same shape as the list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Booth'
        '400':
          description: |
            `VALIDATION_ERROR`:
            - `boothId` or `account_id` is not a UUID.
            - `status` is not `available`, `hold`, `reserved`, `sold` or `blocked`.
            - `account_id` is not an exhibitor or sponsor of this event.
            - `hold_until` is not an ISO date or `null`.
            - `list_price_cents` is not a non-negative integer or `null`.
            - `product_id` is not a booth package of this event.
            - `reserved` or `sold` without an account (`details.reason: account_required`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
              example:
                error: VALIDATION_ERROR
                message: Pick the exhibitor this booth goes to.
                details:
                  reason: account_required
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/ExpoForbidden'
        '404':
          description: '`NOT_FOUND` — no such booth on this event (`details.reason: booth_not_found`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationApiError'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  /v1/event/{eventId}/sponsorships:
    get:
      operationId: list-sponsorships
      summary: List sponsorships
      description: |
        The event's sponsorship packages (ticket types of type `sponsorship`) with how many
        are sold and left, in the order organizers arranged them. Active packages only unless
        `include_archived=true`.

        Package details — tier, staff badges, lead licenses and deliverables — are in
        `settings.sponsorship`.

        Not paginated. Requires `read:expo` (legacy tokens with no scopes pass).
      tags: [Expo]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: include_archived
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: 'Also return archived packages (`status: archived`).'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TicketType'
              example:
                data:
                  - id: 3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f
                    event_id: 7001
                    type: sponsorship
                    name: Gold sponsor
                    description: Keynote recognition, two booth staff badges, one lead license.
                    price_cents: 2500000
                    currency: USD
                    capacity: 4
                    sold: 3
                    available: 1
                    min_per_order: null
                    max_per_order: 1
                    sales_start_at: null
                    sales_end_at: null
                    visibility: hidden
                    status: active
                    sort_order: 0
                    settings:
                      audience: sponsor
                      sponsorship:
                        tier_id: gold
                        staff_badges: 2
                        lead_licenses: 1
                        deliverables:
                          - key: d1
                            title: Logo (SVG)
                            due_days_before: 30
        '401':
          $ref: '#/components/responses/RegistrationUnauthorized'
        '403':
          $ref: '#/components/responses/ExpoForbidden'
        '429':
          $ref: '#/components/responses/RegistrationTooManyRequests'

  # ──────────────────────────────────────────────
  # GDPR
  # ──────────────────────────────────────────────
  /v1/gdpr/requests:
    get:
      operationId: list-gdpr-requests
      summary: List GDPR requests
      description: |
        Returns the organization's GDPR data access and erasure requests,
        newest first.

        The response is a bare JSON array, not a `results` envelope. This page
        previously showed an envelope; the endpoint has always returned the
        array.

        Paginated with `page` and `pageSize` (default 25, maximum 100). Keep
        requesting pages until one comes back with fewer than `pageSize`
        entries.

        Both parameters are validated. A `page` or `pageSize` outside its
        documented range, and any query parameter this operation does not
        declare, are rejected with a `400`. Out-of-range values used to be
        accepted and silently clamped.
      tags:
        - GDPR
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 999
            default: 1
          description: 1-based page number, 1 to 999. Outside that range is a `400`.
        - name: pageSize
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          description: Requests per page, 1 to 100. Outside that range is a `400`; values above 100 are no longer clamped.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/GDPRRequest'
        '400':
          description: >-
            `page` or `pageSize` is outside its documented range, or an
            undeclared query parameter was sent.
        '401':
          description: Unauthorized
    post:
      operationId: create-gdpr-request
      summary: Create a GDPR request
      description: |
        Submit a GDPR data access or erasure request for one email address, and
        process it synchronously against the organization's data.

        For `requestType: erasure` the call returns once the erasure has run.
        Read `status` to find out what happened — it is recorded by the
        erasure itself and is never assumed:

        - `complete` — the email matched at least one record and erasure ran
          against it.
        - `no_records_found` — nothing in this organization matched the email.
          Nothing was erased. Treat this as an outcome to escalate, not a
          fulfilled request.

        `response` carries the identifiers that were matched, and on requests
        processed recently also `counts`, `users_scrubbed` and
        `users_retained` — the compliance evidence of what the erasure actually
        did. A user account shared with another organization is retained rather
        than scrubbed, and appears in `users_retained`.

        A `502` means the erasure did not run and **nothing was recorded or
        erased**; retry the request.
      tags:
        - GDPR
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - requestType
                - identityType
                - identityValue
              properties:
                requestType:
                  type: string
                  enum:
                    - erasure
                    - access
                  description: The type of GDPR request.
                identityType:
                  type: string
                  enum:
                    - email
                  description: The type of identity to look up.
                identityValue:
                  type: string
                  description: The identity value (e.g., an email address).
                  examples:
                    - jane.doe@example.com
      responses:
        '200':
          description: >-
            The request was recorded and, for an erasure, processed. Check
            `status` for the outcome.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GDPRRequest'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '502':
          description: >-
            The erasure service could not be reached. Nothing was recorded and
            nothing was erased — retry.

  # ──────────────────────────────────────────────
  # Insights
  # ──────────────────────────────────────────────
  /v1/insights/execute:
    post:
      operationId: execute-query
      summary: Execute a SbQL query
      description: Execute a SbQL query against event data and return results. Requires the `read:insights` scope.
      tags:
        - Insights
      security:
        - ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - query
              properties:
                query:
                  type: string
                  description: The SbQL query string to execute.
                  examples:
                    - "SELECT sessions.title, sessions.status FROM sessions"
                eventId:
                  type: integer
                  description: Scope query to a single event.
                eventIds:
                  type: array
                  items:
                    type: integer
                  description: Scope query to multiple events.
                obfuscatePii:
                  type: boolean
                  default: true
                  description: Whether to obfuscate personally identifiable information in results.
                format:
                  type: string
                  description: Output format for results.
                page:
                  type: integer
                  description: Page number for paginated results.
                limit:
                  type: integer
                  description: Maximum number of rows to return.
                search:
                  type: string
                  description: Full-text search within results.
                sortBy:
                  type: string
                  description: Column name to sort results by.
                sortDirection:
                  type: string
                  enum:
                    - asc
                    - desc
                  description: Sort direction.
                groupBy:
                  type: string
                  description: Column name to group results by.
                pivot:
                  type: string
                  description: Column name to pivot results on.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          description: Invalid query
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/insights/ai/generate:
    post:
      operationId: generate-query
      summary: Generate a SbQL query from natural language
      description: Use AI to translate a natural language prompt into a SbQL query. Requires the `read:insights` scope.
      tags:
        - Insights
      security:
        - ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - prompt
              properties:
                prompt:
                  type: string
                  description: Natural language description of the data you want to query.
                  examples:
                    - "Show me all accepted sessions with their speakers"
                eventId:
                  type: integer
                  description: Scope to a single event for context.
                eventIds:
                  type: array
                  items:
                    type: integer
                  description: Scope to multiple events for context.
                conversationHistory:
                  type: array
                  items:
                    type: object
                    properties:
                      role:
                        type: string
                        description: The message role (e.g., user, assistant).
                      content:
                        type: string
                        description: The message content.
                  description: Previous conversation turns for multi-turn query refinement.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  query:
                    type: string
                    description: The generated SbQL query.
                  explanation:
                    type: string
                    description: Human-readable explanation of the generated query.
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/insights/schema:
    get:
      operationId: get-schema
      summary: Get insights schema
      description: Returns the SbQL schema for the organization, including available entities and fields. Requires the `read:insights` scope.
      tags:
        - Insights
      security:
        - ApiKey: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                description: The SbQL schema definition.
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/insights/events/{eventId}/schema:
    get:
      operationId: get-event-schema
      summary: Get event-specific insights schema
      description: Returns the SbQL schema scoped to a specific event, including event-specific custom fields. Requires the `read:insights` scope.
      tags:
        - Insights
      security:
        - ApiKey: []
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                description: The event-scoped SbQL schema definition.
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope
        '404':
          description: Event not found

  /v1/insights/suggestions:
    get:
      operationId: get-suggestions
      summary: Get query suggestions
      description: Returns contextual SbQL query suggestions. Requires the `read:insights` scope.
      tags:
        - Insights
      security:
        - ApiKey: []
      parameters:
        - name: eventId
          in: query
          required: false
          schema:
            type: integer
          description: Scope suggestions to a specific event.
        - name: category
          in: query
          required: false
          schema:
            type: string
          description: Filter suggestions by category.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  suggestions:
                    type: array
                    items:
                      type: object
                      properties:
                        query:
                          type: string
                        description:
                          type: string
                        category:
                          type: string
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/insights/queries:
    get:
      operationId: list-queries
      summary: List saved queries
      description: Returns a list of saved SbQL queries. Requires the `read:insights` scope.
      tags:
        - Insights
      security:
        - ApiKey: []
      parameters:
        - name: eventId
          in: query
          required: false
          schema:
            type: integer
          description: Filter queries by event.
        - name: category
          in: query
          required: false
          schema:
            type: string
          description: Filter queries by category.
        - name: page
          in: query
          required: false
          schema:
            type: integer
          description: Page number.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
          description: Results per page.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        query:
                          type: string
                        category:
                          type: string
                        eventId:
                          type: integer
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope

  /v1/insights/queries/{queryId}/run:
    post:
      operationId: run-query
      summary: Run a saved query
      description: Execute a previously saved SbQL query by ID. Requires the `read:insights` scope.
      tags:
        - Insights
      security:
        - ApiKey: []
      parameters:
        - name: queryId
          in: path
          required: true
          schema:
            type: string
          description: The saved query ID.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                eventId:
                  type: integer
                  description: Override the event scope for this run.
                eventIds:
                  type: array
                  items:
                    type: integer
                  description: Override with multiple event scopes.
                obfuscatePii:
                  type: boolean
                  default: true
                format:
                  type: string
                page:
                  type: integer
                limit:
                  type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope
        '404':
          description: Saved query not found

  /v1/insights/dashboards/{id}:
    get:
      operationId: get-dashboard
      summary: Get a dashboard
      description: Retrieve a dashboard with all its widget data. Requires the `read:insights` scope.
      tags:
        - Insights
      security:
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The dashboard ID.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  widgets:
                    type: array
                    items:
                      type: object
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope
        '404':
          description: Dashboard not found

  # ──────────────────────────────────────────────
  # Session CRUD (Write Operations)
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/sessions/create:
    post:
      operationId: create-session
      summary: Create a session
      description: Create a new session in an event. Requires the `write:sessions` scope. Subject to rate limiting (100 req/15min) and daily write quota (10,000/day per token).
      tags:
        - Session Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
              properties:
                title:
                  type: string
                  description: Session title.
                description:
                  type: string
                  description: Session description (HTML allowed).
                starts_at:
                  type: string
                  format: date-time
                  description: Session start time.
                ends_at:
                  type: string
                  format: date-time
                  description: Session end time.
                capacity:
                  type: integer
                  description: Maximum capacity for the session.
                room_id:
                  type: string
                  format: uuid
                  description: Room ID to assign.
                track_id:
                  type: string
                  format: uuid
                  description: Track ID to assign.
                level_id:
                  type: string
                  format: uuid
                  description: Level ID to assign.
                format_id:
                  type: string
                  format: uuid
                  description: Format ID to assign.
                language_id:
                  type: string
                  format: uuid
                  description: Language ID to assign.
                parent_session_id:
                  type: string
                  format: uuid
                  description: Parent session ID (for subsessions).
                tag_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: Tag IDs to assign.
                status:
                  type: string
                  description: Session status (e.g., pending, accepted).
                is_public:
                  type: boolean
                  description: Whether the session is publicly visible.
                is_abstract:
                  type: boolean
                  default: false
                  description: |
                    Create an abstract (CFP) submission instead of a program session.
                    Requires the Sessions 2.0 feature on the event.
                custom_fields:
                  type: object
                  additionalProperties: true
                  description: |
                    Custom field values keyed by field internal name.

                    For `number`, `currency`, and `percent` fields, an empty
                    string (`""`) clears the stored value — identical to
                    sending `null`. `0` and `"0"` store an actual zero.

                    For `checkbox` fields, send boolean `true` or `false`.
                    On read, a checked box serializes as the string `"Yes"`, an
                    explicitly unchecked one as `"No"`, and a box that was never
                    set as `null`.

                    Fields owned by an integration (for example
                    `swoogo_session_id`, `cvent_session_id`,
                    `visit_session_id`) are hydrated by the connector and
                    reject writes with `400` `FIELD_READ_ONLY`, naming the
                    field in `details.field`. Omit them from `custom_fields`.
                translated_fields:
                  type: array
                  items:
                    $ref: '#/components/schemas/TranslatedFieldWrite'
                  description: |
                    Language variants to write using the same shape returned on reads.
                    Omit this key on update to leave existing translations unchanged.
      responses:
        '200':
          description: Session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Session'
        '400':
          description: Validation error
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope or feature not enabled
        '429':
          description: Rate limit or daily quota exceeded

  /v1/event/{eventId}/sessions/{sessionId}/restore:
    post:
      operationId: restore-session
      summary: Restore a deleted session
      description: Restore a previously soft-deleted session. Requires the `write:sessions` scope.
      tags:
        - Session Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The session ID.
      responses:
        '200':
          description: Session restored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Session'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:sessions scope
        '404':
          description: Session not found or not deleted

  /v1/event/{eventId}/sessions/bulk:
    post:
      operationId: bulk-sessions
      summary: Bulk session operations
      description: Create, update, or delete multiple sessions in a single request (max 100 operations). Requires the `write:sessions` scope.
      tags:
        - Session Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkOperations'
      responses:
        '200':
          description: Bulk operation completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkResponse'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:sessions scope
        '429':
          description: Rate limit or daily quota exceeded

  # ──────────────────────────────────────────────
  # Contact Write Operations
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/contacts/create:
    post:
      operationId: create-contact
      summary: Create a contact
      description: |
        Create a new contact in an event. Requires the `write:contacts` scope. Subject to rate limiting (100 req/15min) and daily write quota (10,000/day per token).

        Fields an event marks **Required** in Record Settings (the Add Contact layout) are not enforced here or by `POST /v1/event/{eventId}/contacts/bulk`; they apply only to the event's Add Contact form in the app. Validate them in your integration if you rely on them.
      tags:
        - Contact Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  description: Contact email address (required, must be unique within the event).
                first_name:
                  type: string
                  description: First name.
                last_name:
                  type: string
                  description: Last name.
                company:
                  type: string
                  description: Company or organization name.
                title:
                  type: string
                  description: Job title.
                phone:
                  type: string
                  description: Phone number.
                about:
                  type: string
                  description: Biography. HTML is allowed and stored as rich text.
                salutation:
                  type: string
                honorific:
                  type: string
                address_line_1:
                  type: string
                address_line_2:
                  type: string
                address_city:
                  type: string
                address_state:
                  type: string
                address_postal_code:
                  type: string
                address_country:
                  type: string
                  description: ISO 3166-1 numeric code (as GET returns), alpha-2 or alpha-3 code, or English country name. Stored and returned as the numeric code. An unknown value returns `400` naming the field and nothing is written.
                custom_fields:
                  type: object
                  additionalProperties: true
                  description: |
                    Custom field values keyed by field internal name.

                    For `number`, `currency`, and `percent` fields, an empty
                    string (`""`) clears the stored value — identical to
                    sending `null`. `0` and `"0"` store an actual zero.

                    For `checkbox` fields, send boolean `true` or `false`.
                    On read, a checked box serializes as the string `"Yes"`, an
                    explicitly unchecked one as `"No"`, and a box that was never
                    set as `null`.

                    Fields owned by an integration (for example
                    `swoogo_session_id`, `cvent_session_id`,
                    `visit_session_id`) are hydrated by the connector and
                    reject writes with `400` `FIELD_READ_ONLY`, naming the
                    field in `details.field`. Omit them from `custom_fields`.
                translated_fields:
                  type: array
                  items:
                    $ref: '#/components/schemas/TranslatedFieldWrite'
                  description: |
                    Language variants to write using the same shape returned on reads.
                    Omit this key on update to leave existing translations unchanged.
      responses:
        '200':
          description: Contact created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '400':
          description: Validation error
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope or feature not enabled
        '429':
          description: Rate limit or daily quota exceeded


  /v1/event/{eventId}/contacts/{contactId}/restore:
    post:
      operationId: restore-contact
      summary: Restore a deleted contact
      description: Restore a previously soft-deleted contact. Requires the `write:contacts` scope.
      tags:
        - Contact Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: contactId
          in: path
          required: true
          schema:
            type: string
          description: The contact ID.
      responses:
        '200':
          description: Contact restored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:contacts scope
        '404':
          description: Contact not found or not deleted

  /v1/event/{eventId}/contacts/bulk:
    post:
      operationId: bulk-contacts
      summary: Bulk contact operations
      description: |
        Create, update, or delete multiple contacts in a single request (max
        100 operations). Requires the `write:contacts` scope. Fields an event
        marks **Required** in Record Settings are not enforced on create, as
        with `contacts/create`.

        Send the `{ action, id, data }` envelope every other bulk endpoint
        uses. This endpoint previously read the contact fields off the
        operation itself and rejected the documented envelope with
        `email is required` on every action; that is fixed, and the flat form
        below is still accepted for existing integrations.

        Operations are applied **in request order, one transaction each**. An
        operation that fails leaves nothing behind — no partially written
        contact, no dangling event association — and the operations around it
        still apply. `results` comes back in request order, so
        `results[i]` always describes `operations[i]`.

        `data` accepts the same fields as `POST /contacts/create`, including
        `custom_fields` and `translated_fields`. A create whose email already
        exists in the organization links that contact to the event instead of
        duplicating it, and reports `status: "associated"`.

        <Warning>
          **Deprecated: flat operations.** An operation with no `action` key is
          treated as a create, with the operation object itself as its `data`:
          `{ "email": "a@b.com", "first_name": "Ada" }` behaves like
          `{ "action": "create", "data": { "email": "a@b.com", "first_name": "Ada" } }`.
          This shape is supported for existing callers only and will not gain
          new capabilities. Send the envelope.
        </Warning>
      tags:
        - Contact Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkOperations'
      responses:
        '200':
          description: Bulk operation completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkResponse'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:contacts scope
        '429':
          description: Rate limit or daily quota exceeded

  # ──────────────────────────────────────────────
  # Exhibitor Write Operations
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/exhibitors/create:
    post:
      operationId: create-exhibitor
      summary: Create an exhibitor
      description: Create a new exhibitor in an event. Requires the `write:exhibitors` scope. Subject to rate limiting (100 req/15min) and daily write quota (10,000/day per token).
      tags:
        - Exhibitor Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Exhibitor name. Required if `account_id` is not provided.
                account_id:
                  type: string
                  format: uuid
                  description: >
                    Existing account ID to link, instead of creating a new
                    account. Required if `name` is not provided. This is also
                    how a group takes its second role: pass the `account_id` of
                    a record that is already a sponsor or an exhibitor and it
                    holds both roles on this event, under the same id.
                    Repeating a link it already holds is a no-op.
                custom_fields:
                  type: object
                  additionalProperties: true
                  description: |
                    Custom field values keyed by field internal name.

                    For `number`, `currency`, and `percent` fields, an empty
                    string (`""`) clears the stored value — identical to
                    sending `null`. `0` and `"0"` store an actual zero.

                    For `checkbox` fields, send boolean `true` or `false`.
                    On read, a checked box serializes as the string `"Yes"`, an
                    explicitly unchecked one as `"No"`, and a box that was never
                    set as `null`.

                    Fields owned by an integration (for example
                    `swoogo_session_id`, `cvent_session_id`,
                    `visit_session_id`) are hydrated by the connector and
                    reject writes with `400` `FIELD_READ_ONLY`, naming the
                    field in `details.field`. Omit them from `custom_fields`.
                translated_fields:
                  type: array
                  items:
                    $ref: '#/components/schemas/TranslatedFieldWrite'
                  description: |
                    Language variants to write using the same shape returned on reads.
                    Omit this key on update to leave existing translations unchanged.
      responses:
        '201':
          description: Exhibitor created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Exhibitor'
        '400':
          description: Validation error
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope or feature not enabled
        '429':
          description: Rate limit or daily quota exceeded


  /v1/event/{eventId}/exhibitors/{exhibitorId}/restore:
    post:
      operationId: restore-exhibitor
      summary: Restore a deleted exhibitor
      description: >
        Restore a previously soft-deleted exhibitor. Requires the
        `write:exhibitors` scope.


        Restore has the same reach as the delete it reverses: it restores the
        **group**, so a group that held both roles comes back as both a
        sponsor and an exhibitor, and it reappears on every event it was
        linked to — not only the event in the path.
      tags:
        - Exhibitor Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: exhibitorId
          in: path
          required: true
          schema:
            type: string
          description: The exhibitor ID.
      responses:
        '200':
          description: Exhibitor restored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Exhibitor'
        '400':
          description: >
            The exhibitor is not deleted, so there is nothing to restore.
            Restore is not idempotent — check before retrying.
        '401':
          description: Unauthorized
        '403':
          description: Missing write:exhibitors scope
        '404':
          description: >
            No exhibitor with this id on this event. A record that exists but
            does not hold the exhibitor role — a contact, or a group linked only
            as the other role — is also a `404` here; look it up on the
            endpoint for the role it holds.

  /v1/event/{eventId}/exhibitors/bulk:
    post:
      operationId: bulk-exhibitors
      summary: Bulk exhibitor operations
      description: Create, update, or delete multiple exhibitors in a single request (max 100 operations). Requires the `write:exhibitors` scope.
      tags:
        - Exhibitor Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkOperations'
      responses:
        '200':
          description: Bulk operation completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkResponse'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:exhibitors scope
        '429':
          description: Rate limit or daily quota exceeded

  # ──────────────────────────────────────────────
  # Sponsor Write Operations
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/sponsors/create:
    post:
      operationId: create-sponsor
      summary: Create a sponsor
      description: Create a new sponsor in an event. Requires the `write:sponsors` scope. Subject to rate limiting (100 req/15min) and daily write quota (10,000/day per token).
      tags:
        - Sponsor Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Sponsor name. Required if `account_id` is not provided.
                account_id:
                  type: string
                  format: uuid
                  description: >
                    Existing account ID to link, instead of creating a new
                    account. Required if `name` is not provided. This is also
                    how a group takes its second role: pass the `account_id` of
                    a record that is already a sponsor or an exhibitor and it
                    holds both roles on this event, under the same id.
                    Repeating a link it already holds is a no-op.
                custom_fields:
                  type: object
                  additionalProperties: true
                  description: |
                    Custom field values keyed by field internal name.

                    For `number`, `currency`, and `percent` fields, an empty
                    string (`""`) clears the stored value — identical to
                    sending `null`. `0` and `"0"` store an actual zero.

                    For `checkbox` fields, send boolean `true` or `false`.
                    On read, a checked box serializes as the string `"Yes"`, an
                    explicitly unchecked one as `"No"`, and a box that was never
                    set as `null`.

                    Fields owned by an integration (for example
                    `swoogo_session_id`, `cvent_session_id`,
                    `visit_session_id`) are hydrated by the connector and
                    reject writes with `400` `FIELD_READ_ONLY`, naming the
                    field in `details.field`. Omit them from `custom_fields`.
                translated_fields:
                  type: array
                  items:
                    $ref: '#/components/schemas/TranslatedFieldWrite'
                  description: |
                    Language variants to write using the same shape returned on reads.
                    Omit this key on update to leave existing translations unchanged.
      responses:
        '201':
          description: Sponsor created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Sponsor'
        '400':
          description: Validation error
        '401':
          description: Unauthorized
        '403':
          description: Insufficient scope or feature not enabled
        '429':
          description: Rate limit or daily quota exceeded


  /v1/event/{eventId}/sponsors/{sponsorId}/restore:
    post:
      operationId: restore-sponsor
      summary: Restore a deleted sponsor
      description: >
        Restore a previously soft-deleted sponsor. Requires the
        `write:sponsors` scope.


        Restore has the same reach as the delete it reverses: it restores the
        **group**, so a group that held both roles comes back as both a
        sponsor and an exhibitor, and it reappears on every event it was
        linked to — not only the event in the path.
      tags:
        - Sponsor Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
        - name: sponsorId
          in: path
          required: true
          schema:
            type: string
          description: The sponsor ID.
      responses:
        '200':
          description: Sponsor restored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Sponsor'
        '400':
          description: >
            The sponsor is not deleted, so there is nothing to restore.
            Restore is not idempotent — check before retrying.
        '401':
          description: Unauthorized
        '403':
          description: Missing write:sponsors scope
        '404':
          description: >
            No sponsor with this id on this event. A record that exists but
            does not hold the sponsor role — a contact, or a group linked only
            as the other role — is also a `404` here; look it up on the
            endpoint for the role it holds.

  /v1/event/{eventId}/sponsors/bulk:
    post:
      operationId: bulk-sponsors
      summary: Bulk sponsor operations
      description: Create, update, or delete multiple sponsors in a single request (max 100 operations). Requires the `write:sponsors` scope.
      tags:
        - Sponsor Writes
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
          description: The event ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkOperations'
      responses:
        '200':
          description: Bulk operation completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkResponse'
        '401':
          description: Unauthorized
        '403':
          description: Missing write:sponsors scope
        '429':
          description: Rate limit or daily quota exceeded

  # ──────────────────────────────────────────────
  # Field Write Operations
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/fields/create:
    post:
      operationId: create-field
      summary: Create a custom field
      description: Create a new custom field for an event module. Requires the `write:fields` scope.
      tags: [Field Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                type: { type: string }
                module_id: { type: string }
      responses:
        '200': { description: Field created }
        '401': { description: Unauthorized }
        '403': { description: Missing write:fields scope }

  /v1/event/{eventId}/fields/{fieldId}:
    put:
      operationId: update-field
      summary: Update a custom field
      tags: [Field Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: fieldId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object }
      responses:
        '200': { description: Field updated }
        '401': { description: Unauthorized }
    delete:
      operationId: delete-field
      summary: Delete a custom field
      tags: [Field Writes]
      parameters:
        - name: eventId
          in: path
          required: true
          schema: { type: integer }
        - name: fieldId
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Field deleted. No response body. }
        '401': { description: Unauthorized }

  # ──────────────────────────────────────────────
  # OAuth 2.1
  # ──────────────────────────────────────────────
  /oauth/authorize:
    get:
      operationId: oauth-authorize
      summary: Validate authorization request
      description: Validates OAuth authorization request parameters (client_id, redirect_uri, scopes, PKCE challenge). Returns the validated request for display on the consent page. No authentication required.
      tags:
        - OAuth
      security: []
      parameters:
        - name: client_id
          in: query
          required: true
          schema:
            type: string
        - name: redirect_uri
          in: query
          required: true
          schema:
            type: string
        - name: response_type
          in: query
          required: true
          schema:
            type: string
            enum: [code]
        - name: code_challenge
          in: query
          required: true
          schema:
            type: string
        - name: code_challenge_method
          in: query
          required: true
          schema:
            type: string
            enum: [S256]
        - name: scope
          in: query
          schema:
            type: string
          description: Space-separated scopes
        - name: state
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Authorization request validated
          content:
            application/json:
              schema:
                type: object
                properties:
                  authorization_request:
                    type: object
                    properties:
                      client_id:
                        type: string
                      client_name:
                        type: string
                      redirect_uri:
                        type: string
                      scopes:
                        type: array
                        items:
                          type: string
        '400':
          description: Invalid request parameters

  /oauth/token:
    post:
      operationId: oauth-token
      summary: Exchange code for tokens
      description: Exchange an authorization code for access and refresh tokens (authorization_code grant), or refresh an expired access token (refresh_token grant). Uses PKCE verification — no client secret required.
      tags:
        - OAuth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  title: Authorization Code Exchange
                  required: [grant_type, code, redirect_uri, client_id, code_verifier]
                  properties:
                    grant_type:
                      type: string
                      enum: [authorization_code]
                    code:
                      type: string
                    redirect_uri:
                      type: string
                    client_id:
                      type: string
                    code_verifier:
                      type: string
                      description: PKCE code verifier
                - type: object
                  title: Refresh Token
                  required: [grant_type, refresh_token, client_id]
                  properties:
                    grant_type:
                      type: string
                      enum: [refresh_token]
                    refresh_token:
                      type: string
                    client_id:
                      type: string
      responses:
        '200':
          description: Tokens issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token:
                    type: string
                  token_type:
                    type: string
                    example: Bearer
                  expires_in:
                    type: integer
                    example: 3600
                  refresh_token:
                    type: string
                  scope:
                    type: string
        '400':
          description: Invalid grant or parameters

  /oauth/revoke:
    post:
      operationId: oauth-revoke
      summary: Revoke a token
      description: Revoke an access or refresh token. Conforms to RFC 7009. Always returns 200 regardless of whether the token was found.
      tags:
        - OAuth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token:
                  type: string
                token_type_hint:
                  type: string
                  enum: [access_token, refresh_token]
      responses:
        '200':
          description: Token revoked (or was already invalid)

  /oauth/.well-known/oauth-authorization-server:
    get:
      operationId: oauth-metadata
      summary: Authorization server metadata
      description: Returns OAuth 2.0 Authorization Server Metadata per RFC 8414. Includes supported scopes, grant types, and endpoint URLs.
      tags:
        - OAuth
      security: []
      responses:
        '200':
          description: Server metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  issuer:
                    type: string
                  authorization_endpoint:
                    type: string
                  token_endpoint:
                    type: string
                  revocation_endpoint:
                    type: string
                  response_types_supported:
                    type: array
                    items:
                      type: string
                  grant_types_supported:
                    type: array
                    items:
                      type: string
                  scopes_supported:
                    type: array
                    items:
                      type: string
                  code_challenge_methods_supported:
                    type: array
                    items:
                      type: string

  # ──────────────────────────────────────────────
  # Contact Sessions
  # ──────────────────────────────────────────────
  /v1/event/{eventId}/contacts/{contactId}/sessions:
    get:
      operationId: get-contact-sessions
      summary: Get a contact's sessions
      description: |
        Returns all sessions associated with a specific contact within an
        event, including linked composition sources. Each session summary
        includes `is_abstract` and `composition_status`. The full
        `composition` graph is not available on this lightweight endpoint —
        use session GET or search with `expand=composition` for details.
      tags:
        - Contacts
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: integer
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactSessionsResponse'
        '401':
          description: Unauthorized
        '404':
          description: Contact not found

  /v1/organization/{orgId}/contacts/{contactId}/sessions:
    get:
      operationId: get-org-contact-sessions
      summary: Get an org contact's sessions
      description: |
        Returns all sessions associated with a specific contact across the
        organization, including linked composition sources. Each session
        summary includes `is_abstract`, `composition_status`, and
        `event_id`. The full `composition` graph is not available on this
        lightweight endpoint.
      tags:
        - Contacts
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: string
        - name: contactId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrgContactSessionsResponse'
        '401':
          description: Unauthorized
        '404':
          description: Contact not found

  /v1/event/{eventId}/content:
    get:
      operationId: list-event-content
      summary: List composed session content for an event
      description: |
        Paginated list of session content packs (composed transcript, preferred summary,
        insights, podcast status, and document links). Prefer this over the raw
        polymorphic transcriptions list for mobile and partner apps.

        Requires `read:transcriptions` scope (legacy tokens with empty scopes pass).

        Default packs are compact (`elements` empty on transcription items). Pass
        `expand=items` (plus other types) for full element arrays.
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - name: session_id
          in: query
          schema:
            type: string
            format: uuid
        - name: expand
          in: query
          description: |
            Comma-separated item types. Default `transcription,summary,insight,podcast,document`.
            Add `items` on the event list for full transcription elements (list is compact by default).
            `expand=items` alone still includes the default types. Session GET always returns full elements.
          schema:
            type: string
            example: transcription,summary,items
        - name: interval_sec
          in: query
          description: Optional time window (seconds) for rechunking same-speaker transcript elements. Omit for speaker turns only. Clamped to 3600.
          schema:
            type: integer
            minimum: 1
            maximum: 3600
        - name: provider
          in: query
          description: Preferred summary provider when multiple exist.
          schema:
            type: string
            enum: [claude, openai]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionContentListResponse'
        '403':
          description: Missing read:transcriptions scope

  /v1/event/{eventId}/content/event:
    get:
      operationId: get-event-content
      summary: Get event-level content artifacts
      description: |
        Event report document metadata and recap rollup (quotes, topics, summary excerpts).
        Requires `read:transcriptions` scope. Default expand is `document,recap`.
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: expand
          in: query
          description: Comma-separated. Default `document,recap`. Pass only the types you want.
          schema:
            type: string
            example: document,recap
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventContentResponse'
        '403':
          description: Missing read:transcriptions scope

  /v1/event/{eventId}/content/items/{itemId}:
    get:
      operationId: get-content-item
      summary: Get a single content item
      description: |
        Resolve one content item. `itemId` may be a session UUID (composed transcription),
        a summary/insight/podcast UUID, or a document type (`summary_pdf`, `summary_card`,
        `event_report`, `recap`). Document types `summary_pdf` / `summary_card` require
        `session_id`. Requires `read:transcriptions` scope.
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: itemId
          in: path
          required: true
          schema:
            type: string
        - name: session_id
          in: query
          schema:
            type: string
            format: uuid
        - name: interval_sec
          in: query
          schema:
            type: integer
            minimum: 1
        - name: provider
          in: query
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentItemResponse'
        '400':
          description: Validation error
        '403':
          description: Missing read:transcriptions scope
        '404':
          description: Not found

  /v1/event/{eventId}/content/documents/{documentType}:
    get:
      operationId: download-event-content-document
      summary: Download an event content document
      description: Streams `event_report` PDF. Requires `read:transcriptions` scope.
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: documentType
          in: path
          required: true
          schema:
            type: string
            enum: [event_report]
        - name: variant
          in: query
          schema:
            type: string
            enum: [full, daily, theme]
            default: full
      responses:
        '200':
          description: PDF bytes
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '403':
          description: Missing read:transcriptions scope
        '404':
          description: Not available

  /v1/event/{eventId}/sessions/{sessionId}/content:
    get:
      operationId: get-session-content
      summary: Get composed content for a session
      description: |
        Canonical one-call session pack for mobile and partner apps — composed
        transcription elements, preferred summary, insights, podcast URL/status,
        and summary PDF/card links. Distinct from session **composition**
        (abstract/parent-child linking).

        Requires `read:transcriptions` scope (legacy tokens with empty scopes pass).
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - name: expand
          in: query
          description: Comma-separated item types. Default `transcription,summary,insight,podcast,document`.
          schema:
            type: string
        - name: interval_sec
          in: query
          description: Optional time window (seconds) for rechunking same-speaker elements.
          schema:
            type: integer
            minimum: 1
        - name: provider
          in: query
          schema:
            type: string
            enum: [claude, openai]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionContentResponse'
        '403':
          description: Missing read:transcriptions scope
        '404':
          description: Session not found

  /v1/event/{eventId}/sessions/{sessionId}/content/documents/{documentType}:
    get:
      operationId: download-session-content-document
      summary: Download a session content document
      description: Streams `summary_pdf` or `summary_card`. Requires `read:transcriptions` scope.
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - name: documentType
          in: path
          required: true
          schema:
            type: string
            enum: [summary_pdf, summary_card]
      responses:
        '200':
          description: Document bytes
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
        '403':
          description: Missing read:transcriptions scope
        '404':
          description: Not available

  /v1/event/{eventId}/transcriptions:
    get:
      operationId: list-event-transcriptions
      summary: List transcriptions for an event
      description: Paginated list of transcription artifacts across all sessions. Filter by type, session_id, or language_code. Requires `read:transcriptions` scope (legacy tokens with empty scopes pass). Prefer `/content` for composed mobile-friendly packs.
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - name: type
          in: query
          schema:
            type: string
            enum: [fragment, summary, insight, translation]
        - name: session_id
          in: query
          schema:
            type: string
            format: uuid
        - name: language_code
          in: query
          schema:
            type: string
        - name: include_partial
          in: query
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptionListResponse'
        '403':
          description: Missing read:transcriptions scope

  /v1/event/{eventId}/transcriptions/{transcriptionId}:
    get:
      operationId: get-transcription
      summary: Get a transcription by ID
      description: Requires `read:transcriptions` scope (legacy tokens with empty scopes pass).
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: transcriptionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptionResponse'
        '403':
          description: Missing read:transcriptions scope
        '404':
          description: Not found

  /v1/event/{eventId}/sessions/{sessionId}/transcriptions:
    get:
      operationId: list-session-transcriptions
      summary: List transcriptions for a session
      description: Requires `read:transcriptions` scope (legacy tokens with empty scopes pass).
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptionListResponse'
        '403':
          description: Missing read:transcriptions scope
    post:
      operationId: create-transcription
      summary: Create a transcription artifact
      description: Requires `write:transcriptions` scope.
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TranscriptionCreateRequest'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptionResponse'
        '403':
          description: Missing write:transcriptions scope

  /v1/event/{eventId}/sessions/{sessionId}/transcriptions/{transcriptionId}:
    get:
      operationId: get-session-transcription
      summary: Get a session transcription by ID
      description: Requires `read:transcriptions` scope (legacy tokens with empty scopes pass).
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - name: transcriptionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptionResponse'
        '403':
          description: Missing read:transcriptions scope
        '404':
          description: Not found
    put:
      operationId: update-transcription
      summary: Update a transcription artifact
      description: Requires `write:transcriptions` scope.
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - name: transcriptionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptionResponse'
        '403':
          description: Missing write:transcriptions scope
    delete:
      operationId: delete-transcription
      summary: Delete a transcription artifact
      description: Requires `write:transcriptions` scope.
      tags: [Transcriptions]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - name: transcriptionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Deleted
        '403':
          description: Missing write:transcriptions scope

  /v1/event/{eventId}/sessions/{sessionId}/recordings:
    get:
      operationId: list-session-recordings
      summary: List session audio recordings
      description: Requires `read:transcriptions` scope (legacy tokens with empty scopes pass).
      tags: [Session Recordings]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Recording'
        '403':
          description: Missing read:transcriptions scope
    post:
      operationId: create-session-recording
      summary: Initiate session audio upload
      description: Returns a presigned upload URL for the audio file. Requires `write:transcriptions`.
      tags: [Session Recordings]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, size_bytes]
              properties:
                filename:
                  type: string
                content_type:
                  type: string
                size_bytes:
                  type: integer
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecordingCreateResponse'
        '403':
          description: Missing write:transcriptions scope

  /v1/event/{eventId}/sessions/{sessionId}/recordings/{recordingId}:
    get:
      operationId: get-session-recording
      summary: Get a session audio recording
      description: Requires `read:transcriptions` scope (legacy tokens with empty scopes pass).
      tags: [Session Recordings]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecordingResponse'
        '403':
          description: Missing read:transcriptions scope
        '404':
          description: Not found

  /v1/event/{eventId}/sessions/{sessionId}/recordings/{recordingId}/complete:
    post:
      operationId: complete-session-recording
      summary: Finalize session audio upload
      description: |
        Marks the recording as completed and normalizes common audio formats for playback.
        Already-completed recordings return 200 without re-processing. Requires `write:transcriptions`.
      tags: [Session Recordings]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                duration_seconds:
                  type: number
                file_size_bytes:
                  type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecordingResponse'
        '403':
          description: Missing write:transcriptions scope
        '422':
          description: PCM transcode failed (TRANSCODE_ERROR)

  /v1/event/{eventId}/sessions/{sessionId}/media/upload/initiate:
    post:
      operationId: initiate-media-upload
      summary: Start multipart media upload
      description: Begins a multipart upload for a video or audio file on a session. Requires `write:media`.
      tags: [Media]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, size_bytes]
              properties:
                filename:
                  type: string
                content_type:
                  type: string
                size_bytes:
                  type: integer
      responses:
        '201':
          description: Created
        '403':
          description: Missing write:media scope

  /v1/event/{eventId}/sessions/{sessionId}/media/upload/sign-part:
    post:
      operationId: sign-media-upload-part
      summary: Sign multipart upload parts
      description: Requires `write:media`.
      tags: [Media]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key, upload_id, part_numbers]
              properties:
                key:
                  type: string
                upload_id:
                  type: string
                part_numbers:
                  type: array
                  items:
                    type: integer
      responses:
        '200':
          description: OK
        '403':
          description: Missing write:media scope

  /v1/event/{eventId}/sessions/{sessionId}/media/upload/abort:
    post:
      operationId: abort-media-upload
      summary: Abort multipart upload
      description: Requires `write:media`.
      tags: [Media]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key, upload_id]
              properties:
                key:
                  type: string
                upload_id:
                  type: string
      responses:
        '200':
          description: OK
        '403':
          description: Missing write:media scope

  /v1/event/{eventId}/sessions/{sessionId}/media/upload/complete:
    post:
      operationId: complete-media-upload
      summary: Complete multipart media upload
      description: |
        Finalizes the upload and starts automatic transcription. When you upload on a session URL,
        transcript lines appear in the session transcription list when processing finishes.
        Requires `write:media`.
      tags: [Media]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key, upload_id, parts, filename, size_bytes]
              properties:
                key:
                  type: string
                upload_id:
                  type: string
                parts:
                  type: array
                  items:
                    type: object
                    required: [part_number, etag]
                    properties:
                      part_number:
                        type: integer
                      etag:
                        type: string
                filename:
                  type: string
                size_bytes:
                  type: integer
                duration_seconds:
                  type: number
      responses:
        '201':
          description: Created
        '403':
          description: Missing write:media scope

  /v1/event/{eventId}/sessions/{sessionId}/media/{mediaItemId}:
    get:
      operationId: get-media-item
      summary: Get media item status
      description: Check upload and transcription progress after completing an upload. Requires `read:media` (legacy tokens with empty scopes pass).
      tags: [Media]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - name: mediaItemId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaItemResponse'
        '403':
          description: Missing read:media scope
        '404':
          description: Not found

  /v1/event/{eventId}/content-slots:
    get:
      operationId: list-content-slots
      summary: List upload slots
      description: |
        The files an event asks its speakers for — one slot per ask (Poster,
        Presentation, Handout, …), each with its own deadline, accepted file
        types and file count, in the order speakers see them. Configured by
        organizers under **Program → Settings → Files → Use upload slots**.

        `content_slot_id` on a [session file](/api-reference/session-files/list-files-attached-to-a-session)
        points at one of these slots, so you can tell a poster from a deck without
        parsing filenames.

        Read-only. Active slots only unless `include_archived=true`. An event
        that still uses a single file request for every session (the switch
        is off) returns an empty list. Requires `read:sessions` scope (legacy
        tokens with empty scopes pass).

        Pass `session_id` to get only the slots that apply to one session
        (organizers can scope a slot to, say, poster-format sessions). That is
        the list to pick a `content_slot_id` from before
        [creating a file](/api-reference/session-files/create-a-file-upload-for-a-session)
        on that session.
      tags: [Session Files]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - name: include_archived
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: "Also return archived slots (`is_active: false`). Ignored when `session_id` is set — an archived slot never applies to a session."
        - name: session_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: |
            Return only the active slots that apply to this session, in the
            order its speakers see them. `404` if the session is not on the
            event.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentSlotListResponse'
              examples:
                two-slots:
                  summary: A poster and a presentation ask
                  value:
                    data:
                      - id: 2783cb27-958a-4a24-89d9-2d71387f04cc
                        name: Poster
                        content_type: poster
                        description: Upload your poster as a single-page PDF.
                        due_date: null
                        due_date_extended: null
                        allowed_file_types: [pdf]
                        required: true
                        min_files: null
                        max_files: 1
                        allow_after_deadline: false
                        order: 0
                        is_active: true
                        created_at: '2026-10-02T12:04:11.000Z'
                        updated_at: '2026-10-02T19:48:30.000Z'
                      - id: 9a1c6f0e-4a0b-4a4e-8e3f-0c2b1d5e7f90
                        name: Presentation
                        content_type: presentation
                        description: null
                        due_date: '2026-08-30T21:15:00.000Z'
                        due_date_extended: null
                        allowed_file_types: [word, pdf]
                        required: false
                        min_files: null
                        max_files: 1
                        allow_after_deadline: false
                        order: 1
                        is_active: true
                        created_at: '2026-10-02T12:04:11.000Z'
                        updated_at: '2026-10-02T12:04:11.000Z'
                single-request:
                  summary: Event still on a single file request
                  value:
                    data: []
        '403':
          description: Missing read:sessions scope
        '404':
          description: Event not found

  /v1/event/{eventId}/sessions/{sessionId}/files:
    get:
      operationId: list-session-files
      summary: List files attached to a session
      description: |
        Returns completed files on the session (latest version per file group).
        Incomplete uploads (create/replace without complete) are omitted.
        Each file carries `content_slot_id` — the [upload slot](/api-reference/session-files/list-upload-slots)
        it answers — once the event uses upload slots; `null` otherwise.
        Requires `read:sessions` scope (legacy tokens with empty scopes pass).
      tags: [Session Files]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - name: type
          in: query
          required: false
          description: |
            Only return files of these types. `type` is the organizer-facing
            file type (`presentation`, `handout`, `poster`, `holding_slide`, …).
            Pass one value, a comma-separated list, or repeat the parameter
            (`?type=holding_slide&type=handout`). Values are matched
            case-insensitively; at most 20 values per request. A value with
            characters other than letters, digits, `_` and `-` answers `400`.
          schema:
            type: string
          examples:
            holdingSlide:
              summary: Generated holding slides only
              value: holding_slide
            several:
              summary: Several types
              value: presentation,handout
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionFileListResponse'
        '400':
          description: Malformed `type` filter
        '403':
          description: Missing read:sessions scope
        '404':
          description: Session not found
    post:
      operationId: create-session-file
      summary: Initiate session file upload
      description: |
        Creates a file record and returns a short-lived S3 upload URL.
        Requires `write:sessions`.

        Next: `PUT` the file bytes to `data.upload.url`, then call
        `POST .../files/{fileId}/complete`. For files up to **50 MB**, prefer
        `POST .../files/upload` (one multipart call). This direct-to-storage
        path supports up to **500 MB**.
        See [Uploading session files](/guides/uploading-session-files).
      tags: [Session Files]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionFileCreateRequest'
            examples:
              powerpoint:
                summary: PowerPoint slides
                value:
                  filename: keynote-slides.pptx
                  size_bytes: 2457600
                  content_type: application/vnd.openxmlformats-officedocument.presentationml.presentation
                  title: Keynote slides
              pdf:
                summary: PDF handout
                value:
                  filename: handout.pdf
                  size_bytes: 512000
                  content_type: application/pdf
                  title: Session handout
              posterSlot:
                summary: Poster into the event's Poster slot
                value:
                  filename: poster.pdf
                  size_bytes: 3145728
                  content_type: application/pdf
                  title: Poster
                  content_slot_id: 2783cb27-958a-4a24-89d9-2d71387f04cc
      responses:
        '201':
          description: Created — upload URL included
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionFileCreateResponse'
        '400':
          description: Validation error (missing fields, oversize, denied type, or a `content_slot_id` that is inactive, does not apply to the session, rejects the file type or is full)
        '403':
          description: Missing write:sessions scope

  /v1/event/{eventId}/sessions/{sessionId}/files/upload:
    post:
      operationId: upload-session-file
      summary: Upload a session file (simple)
      description: |
        Upload a file in one multipart request. Sessionboard detects size and
        MIME type from the bytes, runs security scanning, and returns the
        finalized file. Max **50 MB**. Requires `write:sessions`.

        For files larger than 50 MB (up to 500 MB), use the direct-to-storage
        flow: `POST .../files` → PUT to signed URL → `POST .../complete`.
        See [Uploading session files](/guides/uploading-session-files).
      tags: [Session Files]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: The file to attach (field name must be `file`).
                title:
                  type: string
                  description: Display title. Defaults to the uploaded filename.
                assigned_participant_id:
                  type: string
                  format: uuid
                  description: Contact UUID of a session participant.
                content_slot_id:
                  type: string
                  format: uuid
                  description: |
                    Optional [upload slot](/api-reference/session-files/list-upload-slots)
                    this file answers. Same rules as `content_slot_id` on
                    `POST .../files`: the slot must be active, apply to the
                    session, accept the file's type and have room; `400`
                    names the slot otherwise. Sets the file's `type`.
      responses:
        '201':
          description: Created — file attached and scanned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionFileResponse'
        '400':
          description: Validation error (missing file, oversize, denied type)
        '403':
          description: Missing write:sessions scope

  /v1/event/{eventId}/sessions/{sessionId}/files/{fileId}/complete:
    post:
      operationId: complete-session-file
      summary: Finalize session file upload
      description: |
        Verifies the object in Sessionboard storage, runs security scanning
        (type deny-list + magic-byte checks; SVG sanitization when applicable),
        and finalizes the file on the session. Requires `write:sessions`.
      tags: [Session Files]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - $ref: '#/components/parameters/FileId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionFileResponse'
        '400':
          description: Upload missing, empty, or failed security checks
        '403':
          description: Missing write:sessions scope
        '404':
          description: File not found

  /v1/event/{eventId}/sessions/{sessionId}/files/{fileId}/replace:
    post:
      operationId: replace-session-file
      summary: Replace session file bytes
      description: |
        Issues a new upload URL for the same file record. After `PUT`ing the new
        bytes, call `complete` again. Requires `write:sessions`. Max **500 MB**
        (direct-to-storage flow).
      tags: [Session Files]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - $ref: '#/components/parameters/FileId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionFileCreateRequest'
      responses:
        '200':
          description: OK — new upload URL included
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionFileCreateResponse'
        '400':
          description: Validation error
        '403':
          description: Missing write:sessions scope
        '404':
          description: File not found

  /v1/event/{eventId}/sessions/{sessionId}/files/{fileId}:
    put:
      operationId: update-session-file
      summary: Update session file metadata
      description: |
        Updates display title and/or assigned participant. Does not change file
        bytes — use `replace` for that. Unknown fields are ignored.
        Requires `write:sessions`.
      tags: [Session Files]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - $ref: '#/components/parameters/FileId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionFileUpdateRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionFileResponse'
        '400':
          description: Validation error
        '403':
          description: Missing write:sessions scope
        '404':
          description: File not found
    delete:
      operationId: delete-session-file
      summary: Delete a session file
      description: Soft-deletes the file from the session. Requires `write:sessions`.
      tags: [Session Files]
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/SessionId'
        - $ref: '#/components/parameters/FileId'
      responses:
        '204':
          description: No Content
        '403':
          description: Missing write:sessions scope
        '404':
          description: File not found

  # ──────────────────────────────────────────────
  # Log Export
  # ──────────────────────────────────────────────
  /v1/logs/api-requests:
    get:
      operationId: list-log-api-requests
      summary: List API request logs
      description: |
        Every request made to this Public API, the MCP server and Insights with your
        organization's credentials, including requests rejected at authentication
        when the credential can be tied to your organization (`reason` carries
        `revoked_token`, `expired_token`, `org_archived`, `member_inactive`,
        `owner_inactive`, `ai_disabled`, `log_export_only`, `cross_org_event` or
        `event_not_allowed`). A request with no key or with a key Sessionboard
        does not recognize belongs to no organization and is not exported.
        Kept for 365 days.
        Request bodies are omitted unless you ask for them with `include=request_body`.
      tags: [Log Export]
      security:
        - ApiKey: []
      parameters:
        - $ref: '#/components/parameters/LogCursor'
        - $ref: '#/components/parameters/LogSince'
        - $ref: '#/components/parameters/LogLimit'
        - name: outcome
          in: query
          required: false
          schema:
            type: string
            enum: [success, failure]
          description: Only successful or only failed events.
        - name: source
          in: query
          required: false
          schema:
            type: string
            enum: [public-api, mcp, insights]
          description: Only requests that arrived through one surface.
        - name: include
          in: query
          required: false
          schema:
            type: string
            enum: [request_body]
          description: Add the logged request body to `details.request_body`.
      responses:
        '200':
          description: One page of events, oldest first.
          headers:
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogPage'
        '400':
          description: |
            `VALIDATION_ERROR` for a bad parameter; `INVALID_CURSOR` for a cursor that is malformed or was issued for another stream or organization;
            `CURSOR_FILTER_MISMATCH` when the filters differ from the ones the cursor was issued with;
            `SINCE_BEFORE_RETENTION` when `since` is older than the stream keeps.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogExportError'
        '401':
          description: Missing, invalid, revoked or expired API token.
        '403':
          description: The token does not hold `read:audit_logs`, or it is an OAuth token. Log export accepts API tokens only.
        '410':
          description: '`CURSOR_EXPIRED` — the cursor points before the start of retention. Restart from `since`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogExportError'
        '429':
          description: Rate limited. 60 requests per minute per token; honor `Retry-After`.

  /v1/logs/security-events:
    get:
      operationId: list-log-security-events
      summary: List security events
      description: |
        Sign-ins, sign-in failures, two-factor and passkey events, password resets and
        session changes for the people in your organization. Covers events recorded
        against the organization and those of its current members.
        Sessionboard staff activity is not included. No retention limit.
      tags: [Log Export]
      security:
        - ApiKey: []
      parameters:
        - $ref: '#/components/parameters/LogCursor'
        - $ref: '#/components/parameters/LogSince'
        - $ref: '#/components/parameters/LogLimit'
        - name: outcome
          in: query
          required: false
          schema:
            type: string
            enum: [success, failure]
          description: Only successful or only failed events.
      responses:
        '200':
          description: One page of events, oldest first.
          headers:
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogPage'
        '400':
          description: |
            `VALIDATION_ERROR` for a bad parameter; `INVALID_CURSOR` for a cursor that is malformed or was issued for another stream or organization;
            `CURSOR_FILTER_MISMATCH` when the filters differ from the ones the cursor was issued with;
            `SINCE_BEFORE_RETENTION` when `since` is older than the stream keeps.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogExportError'
        '401':
          description: Missing, invalid, revoked or expired API token.
        '403':
          description: The token does not hold `read:audit_logs`, or it is an OAuth token. Log export accepts API tokens only.
        '410':
          description: '`CURSOR_EXPIRED` — the cursor points before the start of retention. Restart from `since`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogExportError'
        '429':
          description: Rate limited. 60 requests per minute per token; honor `Retry-After`.

  /v1/logs/audit-trail:
    get:
      operationId: list-log-audit-trail
      summary: List audit trail
      description: |
        Every recorded change to your organization's data — who changed which record,
        which property, and (by default) the new value. `action` is
        `<subject_type>.<operation>`, for example `session.update`. No retention limit.
      tags: [Log Export]
      security:
        - ApiKey: []
      parameters:
        - $ref: '#/components/parameters/LogCursor'
        - $ref: '#/components/parameters/LogSince'
        - $ref: '#/components/parameters/LogLimit'
        - name: subject_type
          in: query
          required: false
          schema:
            type: string
            example: session,contact
          description: Comma-separated record types to include, for example `session,contact`.
        - name: include_values
          in: query
          required: false
          schema:
            type: boolean
            default: true
          description: Set `false` to omit changed values (`details.value`) — useful when the SIEM must not hold personal data.
      responses:
        '200':
          description: One page of events, oldest first.
          headers:
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogPage'
        '400':
          description: |
            `VALIDATION_ERROR` for a bad parameter; `INVALID_CURSOR` for a cursor that is malformed or was issued for another stream or organization;
            `CURSOR_FILTER_MISMATCH` when the filters differ from the ones the cursor was issued with;
            `SINCE_BEFORE_RETENTION` when `since` is older than the stream keeps.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogExportError'
        '401':
          description: Missing, invalid, revoked or expired API token.
        '403':
          description: The token does not hold `read:audit_logs`, or it is an OAuth token. Log export accepts API tokens only.
        '410':
          description: '`CURSOR_EXPIRED` — the cursor points before the start of retention. Restart from `since`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogExportError'
        '429':
          description: Rate limited. 60 requests per minute per token; honor `Retry-After`.

# ════════════════════════════════════════════════
# Components
# ════════════════════════════════════════════════
  # ──────────────────────────────────────────────
  # Awards
  # ──────────────────────────────────────────────
  /v1/organization/{orgId}/awards/programs:
    get:
      operationId: list-awards-programs
      summary: List programs
      description: Requires the `read:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/AwardsPageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AwardsProgram'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
  /v1/organization/{orgId}/awards/programs/create:
    post:
      operationId: create-awards-program
      summary: Create a program
      description: >-
        Creates the program with its submission form, a default round, and the token owner as program
        manager — the same as creating it in the admin app. Counts against the plan's Awards program
        limit. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/AwardsProgramWrite'
              required:
                - name
      responses:
        '201':
          description: Program created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsProgram'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Your plan allows 3 Awards programs (403 also means the token lacks `write:awards`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: FORBIDDEN
                message: Your plan allows 3 Awards programs (403 also means the token lacks `write:awards`)
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
  /v1/organization/{orgId}/awards/programs/{programId}:
    get:
      operationId: get-awards-program
      summary: Get a program
      description: Requires the `read:awards` scope.
      tags:
        - Awards
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsProgram'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
    put:
      operationId: update-awards-program
      summary: Update a program
      description: >-
        Fields you omit keep their stored value. `PUT` and `PATCH` behave the same: send only the
        fields you are changing. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardsProgramWrite'
      responses:
        '200':
          description: Program updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsProgram'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
    patch:
      operationId: patch-awards-program
      summary: Update a program (PATCH)
      description: >-
        Fields you omit keep their stored value. `PUT` and `PATCH` behave the same: send only the
        fields you are changing. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardsProgramWrite'
      responses:
        '200':
          description: Program updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsProgram'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
    delete:
      operationId: delete-awards-program
      summary: Delete a program
      description: >-
        Soft-deletes the program (it moves to Trash and can be restored in the admin app). A program
        that has submissions returns `409` unless `force=true` is sent — the API equivalent of the
        admin app's confirmation dialog. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/AwardsForce'
      responses:
        '204':
          description: Program deleted
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
        '409':
          description: Program has 12 submissions; send force=true to delete it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: CONFLICT
                message: Program has 12 submissions; send force=true to delete it
  /v1/organization/{orgId}/awards/programs/{programId}/categories:
    get:
      operationId: list-awards-categorys
      summary: List categorys
      description: Requires the `read:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/AwardsPageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AwardsCategory'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
  /v1/organization/{orgId}/awards/programs/{programId}/categories/create:
    post:
      operationId: create-awards-category
      summary: Create a category
      description: >-
        Set `parent_category_id` to another category in the same program to create a subcategory.
        `order` defaults to the end of the list. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/AwardsCategoryWrite'
              required:
                - name
      responses:
        '201':
          description: Category created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsCategory'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
  /v1/organization/{orgId}/awards/programs/{programId}/categories/{categoryId}:
    get:
      operationId: get-awards-category
      summary: Get a category
      description: Requires the `read:awards` scope.
      tags:
        - Awards
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsCategory'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: categoryId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The category ID.
    put:
      operationId: update-awards-category
      summary: Update a category
      description: >-
        A parent in another program, the category itself, or one of its own subcategories returns
        `400`. `PUT` and `PATCH` behave the same: send only the fields you are changing. Requires the
        `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: categoryId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The category ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardsCategoryWrite'
      responses:
        '200':
          description: Category updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsCategory'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
    patch:
      operationId: patch-awards-category
      summary: Update a category (PATCH)
      description: >-
        A parent in another program, the category itself, or one of its own subcategories returns
        `400`. `PUT` and `PATCH` behave the same: send only the fields you are changing. Requires the
        `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: categoryId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The category ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardsCategoryWrite'
      responses:
        '200':
          description: Category updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsCategory'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
    delete:
      operationId: delete-awards-category
      summary: Delete a category
      description: >-
        Soft-deletes the category. One with subcategories returns `409` unless `cascade=true` is sent,
        which deletes the whole subtree. One that submissions are filed under (directly or through a
        subcategory) also returns `409` unless `force=true` is sent. Requires the `write:awards`
        scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: categoryId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The category ID.
        - $ref: '#/components/parameters/AwardsCascade'
        - $ref: '#/components/parameters/AwardsForce'
      responses:
        '204':
          description: Category deleted
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
        '409':
          description: Category has 2 subcategories; send cascade=true to delete them too
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: CONFLICT
                message: Category has 2 subcategories; send cascade=true to delete them too
  /v1/organization/{orgId}/awards/programs/{programId}/rounds:
    get:
      operationId: list-awards-rounds
      summary: List rounds
      description: Requires the `read:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/AwardsPageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AwardsRound'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
  /v1/organization/{orgId}/awards/programs/{programId}/rounds/create:
    post:
      operationId: create-awards-round
      summary: Create a round
      description: Adds a review round to the program. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/AwardsRoundWrite'
              required:
                - name
      responses:
        '201':
          description: Round created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsRound'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
  /v1/organization/{orgId}/awards/programs/{programId}/rounds/{roundId}:
    get:
      operationId: get-awards-round
      summary: Get a round
      description: Requires the `read:awards` scope.
      tags:
        - Awards
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsRound'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: roundId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The round ID.
    put:
      operationId: update-awards-round
      summary: Update a round
      description: >-
        Fields you omit keep their stored value. `PUT` and `PATCH` behave the same: send only the
        fields you are changing. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: roundId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The round ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardsRoundWrite'
      responses:
        '200':
          description: Round updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsRound'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
    patch:
      operationId: patch-awards-round
      summary: Update a round (PATCH)
      description: >-
        Fields you omit keep their stored value. `PUT` and `PATCH` behave the same: send only the
        fields you are changing. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: roundId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The round ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardsRoundWrite'
      responses:
        '200':
          description: Round updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsRound'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
    delete:
      operationId: delete-awards-round
      summary: Delete a round
      description: >-
        Soft-deletes the round and moves its submissions back to the previous round, as the admin app
        does. The program's default (first) round cannot be deleted and returns `422`. A round with
        reviewer assignments returns `409` unless `force=true` is sent. Requires the `write:awards`
        scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: roundId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The round ID.
        - $ref: '#/components/parameters/AwardsForce'
      responses:
        '204':
          description: Round deleted
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
        '409':
          description: Round has 8 reviewer assignments; send force=true to delete it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: CONFLICT
                message: Round has 8 reviewer assignments; send force=true to delete it
        '422':
          description: The default round cannot be deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: The default round cannot be deleted
  /v1/organization/{orgId}/awards/programs/{programId}/promo-codes:
    get:
      operationId: list-awards-promo-codes
      summary: List promo codes
      description: Requires the `read:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/AwardsPageSize'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AwardsPromoCode'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
  /v1/organization/{orgId}/awards/programs/{programId}/promo-codes/create:
    post:
      operationId: create-awards-promo-code
      summary: Create a promo code
      description: >-
        Codes are stored upper-case and must be unique within the program. A `percentage` amount above
        100 returns `400`; `percentage` 100 waives the fee entirely. Requires the `write:awards`
        scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/AwardsPromoCodeWrite'
              required:
                - code
                - type
                - amount
      responses:
        '201':
          description: Promo code created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsPromoCode'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
        '409':
          description: Promo code SAVE20 already exists in this program
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: CONFLICT
                message: Promo code SAVE20 already exists in this program
  /v1/organization/{orgId}/awards/programs/{programId}/promo-codes/{promoCodeId}:
    get:
      operationId: get-awards-promo-code
      summary: Get a promo code
      description: Requires the `read:awards` scope.
      tags:
        - Awards
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsPromoCode'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: promoCodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The promo code ID.
    put:
      operationId: update-awards-promo-code
      summary: Update a promo code
      description: >-
        Renaming to a code already used in the program returns `409`. `PUT` and `PATCH` behave the
        same: send only the fields you are changing. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: promoCodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The promo code ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardsPromoCodeWrite'
      responses:
        '200':
          description: Promo code updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsPromoCode'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
        '409':
          description: Promo code SAVE20 already exists in this program
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: CONFLICT
                message: Promo code SAVE20 already exists in this program
    patch:
      operationId: patch-awards-promo-code
      summary: Update a promo code (PATCH)
      description: >-
        Renaming to a code already used in the program returns `409`. `PUT` and `PATCH` behave the
        same: send only the fields you are changing. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: promoCodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The promo code ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardsPromoCodeWrite'
      responses:
        '200':
          description: Promo code updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsPromoCode'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
        '409':
          description: Promo code SAVE20 already exists in this program
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: CONFLICT
                message: Promo code SAVE20 already exists in this program
    delete:
      operationId: delete-awards-promo-code
      summary: Delete a promo code
      description: >-
        Soft-deletes the code. A code already applied to a submission returns `409` unless
        `force=true` is sent; those submissions keep their discount. Requires the `write:awards`
        scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - name: promoCodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The promo code ID.
        - $ref: '#/components/parameters/AwardsForce'
      responses:
        '204':
          description: Promo code deleted
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
        '409':
          description: Promo code has been redeemed 3 times; send force=true to delete it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: CONFLICT
                message: Promo code has been redeemed 3 times; send force=true to delete it
  /v1/organization/{orgId}/awards/programs/{programId}/submissions:
    get:
      operationId: list-awards-submissions
      summary: List submissions
      description: >-
        Lists top-level submissions (merged duplicates are folded into their primary). Submissions are
        created by submitters through the program's portal, not through the API. Requires the
        `read:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/AwardsPageSize'
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Only submissions in this status, e.g. `submitted`.
        - name: category_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: current_round_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AwardsSubmission'
                  pagination:
                    $ref: '#/components/schemas/ListPagination'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
  /v1/organization/{orgId}/awards/programs/{programId}/submissions/{submissionId}:
    get:
      operationId: get-awards-submission
      summary: Get a submission
      description: Includes `values`, the form answers keyed by form field ID. Requires the `read:awards` scope.
      tags:
        - Awards
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsSubmission'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/AwardsSubmissionId'
    put:
      operationId: update-awards-submission
      summary: Update a submission
      description: >-
        Writes answers to the submission form, keyed by form field ID, with the same required-field
        checks and edit history as an admin edit. `PUT` and `PATCH` behave the same: send only the
        fields you are changing. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/AwardsSubmissionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardsSubmissionWrite'
      responses:
        '200':
          description: Submission updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsSubmission'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
    patch:
      operationId: patch-awards-submission
      summary: Update a submission (PATCH)
      description: >-
        Writes answers to the submission form, keyed by form field ID, with the same required-field
        checks and edit history as an admin edit. `PUT` and `PATCH` behave the same: send only the
        fields you are changing. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/AwardsSubmissionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardsSubmissionWrite'
      responses:
        '200':
          description: Submission updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsSubmission'
        '400':
          description: Invalid body or id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: VALIDATION_ERROR
                message: '"name" is not allowed to be empty'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
    delete:
      operationId: delete-awards-submission
      summary: Delete a submission
      description: >-
        Soft-deletes the submission (it moves to Trash and can be restored in the admin app). Requires
        the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/AwardsSubmissionId'
      responses:
        '204':
          description: Submission deleted
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
  /v1/organization/{orgId}/awards/programs/{programId}/submissions/{submissionId}/reviews:
    get:
      operationId: list-awards-reviews
      summary: List reviews
      description: >-
        One entry per reviewer assigned to the submission, with their scores. Reviews are written by
        reviewers in the review portal, not through the API. Requires the `read:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/AwardsSubmissionId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AwardsReview'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
  /v1/organization/{orgId}/awards/programs/{programId}/submissions/{submissionId}/reviews/{reviewId}:
    get:
      operationId: get-awards-review
      summary: Get a review
      description: Requires the `read:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/AwardsSubmissionId'
        - name: reviewId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The review (reviewer assignment) ID.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AwardsReview'
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `read:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: read:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found
    delete:
      operationId: delete-awards-review
      summary: Remove a review
      description: >-
        Unassigns the reviewer from the submission for that round and removes their scores, as
        unassigning in the admin app does. Requires the `write:awards` scope.
      tags:
        - Awards
      parameters:
        - $ref: '#/components/parameters/AwardsOrgId'
        - $ref: '#/components/parameters/AwardsProgramId'
        - $ref: '#/components/parameters/AwardsSubmissionId'
        - name: reviewId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The review (reviewer assignment) ID.
      responses:
        '204':
          description: Review removed
        '401':
          description: Missing or invalid token
        '403':
          description: Token lacks `write:awards`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: SCOPE_REQUIRED
                message: 'Token missing required scope: write:awards'
        '404':
          description: Not found — also returned for any id belonging to another organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwardsError'
              example:
                error: NOT_FOUND
                message: Not found

components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: x-access-token
      description: Organization API token. Generate from Organization Settings → API Tokens.
    BearerToken:
      type: http
      scheme: bearer
      description: OAuth 2.1 access token. Obtained via the OAuth PKCE flow at `/oauth/token`.

  headers:
    RateLimitLimit:
      description: Requests allowed in the current window.
      schema:
        type: integer
    RateLimitRemaining:
      description: Requests left in the current window.
      schema:
        type: integer

  parameters:
    RegistrationPage:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
      description: Page number, from 1. A value that is not a positive integer is read as `1`.
    RegistrationPageSize:
      name: page_size
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
      description: Results per page. Values above 200 are read as 200; a value that is not a positive integer is read as 50.
    RegistrationId:
      name: registrationId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: The registration ID.
    AwardsOrgId:
      name: orgId
      in: path
      required: true
      schema:
        type: string
      description: Your organization ID. Any other organization returns `404`.
    AwardsProgramId:
      name: programId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: The Awards program ID.
    AwardsSubmissionId:
      name: submissionId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: The submission ID.
    AwardsPageSize:
      name: pageSize
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
      description: Results per page. `page_size` is also accepted.
    AwardsForce:
      name: force
      in: query
      required: false
      schema:
        type: boolean
        default: false
      description: Delete even though the record is in use (see the `409` response).
    AwardsCascade:
      name: cascade
      in: query
      required: false
      schema:
        type: boolean
        default: false
      description: Also delete the category's subcategories.
    LogCursor:
      name: cursor
      in: query
      required: false
      schema:
        type: string
        maxLength: 2048
      description: |
        `next_cursor` from the previous page. Store it after each page and send it back unchanged;
        it remembers your filters, so repeat the same filters with it.
    LogSince:
      name: since
      in: query
      required: false
      schema:
        type: string
        format: date-time
      description: Where to start when you have no cursor. Defaults to 24 hours ago. Ignored when `cursor` is sent.
    LogLimit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 500
      description: Events per page.
    EventId:
      name: eventId
      in: path
      required: true
      schema:
        type: integer
      description: Event ID
    SessionId:
      name: sessionId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Session UUID
    FileId:
      name: fileId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Session file (Content) UUID
    Page:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 999
        default: 1
      description: Page number for pagination.
    PageSize:
      name: pageSize
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
      description: |
        Number of results per page.

        The event-settings list endpoints (tags, tracks, levels, formats,
        rooms, languages, statuses) also accept the older spelling
        `page_size`, and used to accept only that — a request sending
        `pageSize` silently got the default 25. Both now work, `pageSize`
        wins when both are sent, and on these endpoints a value outside
        1–100 is clamped rather than rejected. Everywhere else `pageSize`
        is the only spelling.
    Expand:
      name: expand
      in: query
      required: false
      schema:
        type: array
        items:
          type: string
          enum:
            - translated_fields
            - subsession_details
            - linked_sources
            - composition
      description: |
        Expand records with additional data.

        Possible values:
        - `translated_fields` — includes the `translated_fields` array on records when the event has language variants configured.
        - `subsession_details` — applies to session responses. Each subsession inside `parent.subsessions[]` is returned with full parent-session field parity (`status`, `custom_status`, `custom_fields`, `chairpersons`, `moderators`, `sponsors`, `exhibitors`, `tags`, `language`, `track`, `level`, `room`, `is_public`, `external_url`, `client_session_id`, `ceu_credits`, `capacity`, `source`) instead of the default minimal shape. Subsessions inherit `is_abstract`, `composition_status`, and optional `composition` from the parent. Not required when calling `GET /v1/event/{eventId}/sessions/{sessionId}` with a subsession UUID — that endpoint always returns the full shape at the top level.
        - `linked_sources` — include sessions that are linked as composition sources (excluded from list/search results by default).
        - `composition` — attach a full `composition` object with target and source details on each returned session. Recommended for single-session GET; use cautiously on broad searches.

        A single value can be passed directly, and multiple values repeat
        the parameter — all of these are accepted:
        - `?expand=composition`
        - `?expand=translated_fields&expand=composition`
        - `?expand[]=translated_fields&expand[]=composition`

    ListEventsExpand:
      name: expand
      in: query
      required: false
      style: form
      explode: true
      schema:
        type: array
        items:
          type: string
          enum:
            - custom_fields
            - integration_mappings
      description: |
        Extra collections on each event in `GET /v1/events`. Not the same enum
        as session `Expand` (`translated_fields`, `composition`, …).

        Repeat the parameter (`?expand=custom_fields&expand=integration_mappings`)
        or comma-separate (`?expand=custom_fields,integration_mappings`).

        - `custom_fields` — org-defined Event custom field values. Omitted from
          the payload when this expand is not requested. Present as `[]` when
          requested and the event has no values.
        - `integration_mappings` — `{ name, source, external_event_id }` rows
          from conference `Integration_*_Event_Mappings` (via the union view).
          `source` uses underscores (`expo_platform`, not `expo-platform`).
          LiveBuzz / apps-engine campaign IDs are not included.

  schemas:
    AwardsError:
      type: object
      properties:
        error:
          type: string
          enum:
            - VALIDATION_ERROR
            - SCOPE_REQUIRED
            - FORBIDDEN
            - NOT_FOUND
            - CONFLICT
        message:
          type: string
    AwardsProgram:
      type: object
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
        name:
          type: string
          examples:
            - 2027 Excellence Awards
        slug:
          type: string
          examples:
            - 2027-excellence-awards
        nomenclature:
          type: string
          enum:
            - awards
            - grants
            - scholarships
        description:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - draft
            - open
            - reviewing
            - closed
            - archived
        year:
          type:
            - integer
            - 'null'
        deadline:
          type:
            - string
            - 'null'
          format: date-time
        collects_fees:
          type: boolean
        pricing_type:
          type:
            - string
            - 'null'
        base_fee:
          type:
            - number
            - 'null'
        submitter_editing_policy:
          type: string
          enum:
            - never
            - while_open
            - while_open_after_payment
        notify_on_submitter_edit:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AwardsProgramWrite:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        nomenclature:
          type: string
          enum:
            - awards
            - grants
            - scholarships
        description:
          type:
            - string
            - 'null'
        slug:
          type: string
          minLength: 1
          maxLength: 255
          description: Defaults to one derived from `name` on create.
        status:
          type: string
          enum:
            - draft
            - open
            - reviewing
            - closed
            - archived
          description: Update only.
        year:
          type:
            - integer
            - 'null'
        deadline:
          type:
            - string
            - 'null'
          format: date-time
        collects_fees:
          type: boolean
          description: Update only.
        submitter_editing_policy:
          type: string
          enum:
            - never
            - while_open
            - while_open_after_payment
        notify_on_submitter_edit:
          type: boolean
        default_submission_status:
          type:
            - string
            - 'null'
          enum:
            - submitted
            - under_review
            - accepted
            - rejected
            - ''
            - null
          description: Status a new submission starts in. Empty string or `null` clears it.
    AwardsCategory:
      type: object
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        name:
          type: string
          examples:
            - Best Innovation
        description:
          type:
            - string
            - 'null'
        parent_category_id:
          type:
            - string
            - 'null'
          format: uuid
        submission_limit:
          type:
            - integer
            - 'null'
        expertise_tags:
          type: array
          items:
            type: string
        order:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AwardsCategoryWrite:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type:
            - string
            - 'null'
          maxLength: 10000
        parent_category_id:
          type:
            - string
            - 'null'
          format: uuid
          description: Another category in the same program. `null` makes it top-level.
        submission_limit:
          type:
            - integer
            - 'null'
          minimum: 0
          description: Most submissions one submitter may file in this category.
        expertise_tags:
          type: array
          items:
            type: string
        order:
          type: integer
    AwardsRound:
      type: object
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        name:
          type: string
          examples:
            - Final judging
        round_type:
          type: string
          enum:
            - submission_review
            - review_only
        is_default:
          type: boolean
          description: The program's first round. It cannot be deleted.
        submission_start_date:
          type:
            - string
            - 'null'
        submission_end_date:
          type:
            - string
            - 'null'
        review_start_date:
          type:
            - string
            - 'null'
        review_end_date:
          type:
            - string
            - 'null'
        passing_score:
          type:
            - number
            - 'null'
        blind_review:
          type: boolean
        instructions:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AwardsRoundWrite:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        round_type:
          type: string
          enum:
            - submission_review
            - review_only
        submission_start_date:
          type:
            - string
            - 'null'
          description: '`YYYY-MM-DD` or an ISO 8601 timestamp.'
        submission_end_date:
          type:
            - string
            - 'null'
          description: '`YYYY-MM-DD` or an ISO 8601 timestamp.'
        review_start_date:
          type:
            - string
            - 'null'
          description: '`YYYY-MM-DD` or an ISO 8601 timestamp.'
        review_end_date:
          type:
            - string
            - 'null'
          description: '`YYYY-MM-DD` or an ISO 8601 timestamp.'
        passing_score:
          type:
            - number
            - 'null'
        blind_review:
          type: boolean
          description: Hide submitter identity from reviewers.
        instructions:
          type:
            - string
            - 'null'
          maxLength: 20000
    AwardsPromoCode:
      type: object
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        code:
          type: string
          examples:
            - SAVE20
        type:
          type: string
          enum:
            - percentage
            - flat
        amount:
          type: number
          examples:
            - 20
        starts_at:
          type:
            - string
            - 'null'
          format: date-time
        ends_at:
          type:
            - string
            - 'null'
          format: date-time
        redemption_count:
          type: integer
          description: Submissions the code has been applied to. Returned by the single-code GET only.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AwardsPromoCodeWrite:
      type: object
      additionalProperties: false
      properties:
        code:
          type: string
          maxLength: 255
          pattern: ^[A-Za-z0-9]+$
          description: Letters and digits; stored upper-case.
        type:
          type: string
          enum:
            - percentage
            - flat
        amount:
          type: number
          minimum: 0
          description: Percent off (at most 100) or a flat amount in the program's currency.
        starts_at:
          type:
            - string
            - 'null'
          format: date-time
        ends_at:
          type:
            - string
            - 'null'
          format: date-time
    AwardsSubmission:
      type: object
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        category_id:
          type:
            - string
            - 'null'
          format: uuid
        friendly_id:
          type: string
          examples:
            - AWD-0042
        status:
          type: string
          examples:
            - submitted
        current_round_id:
          type:
            - string
            - 'null'
          format: uuid
        payment_status:
          type:
            - string
            - 'null'
        promo_code_id:
          type:
            - string
            - 'null'
          format: uuid
        submitted_by_user_id:
          type:
            - integer
            - 'null'
        submitted_at:
          type:
            - string
            - 'null'
          format: date-time
        total_score:
          type:
            - number
            - 'null'
        merged_into_submission_id:
          type:
            - string
            - 'null'
          format: uuid
        values:
          type: object
          additionalProperties: true
          description: Form answers keyed by form field ID. Returned by the single-submission GET and by updates.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AwardsSubmissionWrite:
      type: object
      additionalProperties: false
      properties:
        values:
          type: object
          additionalProperties: true
          description: >-
            Answers keyed by form field ID. Fields you omit are left unchanged; a required field sent
            empty returns `400`.
        allow_submitter_editing_override:
          type:
            - boolean
            - 'null'
          description: >-
            Let this submitter edit regardless of the program's editing policy. `null` clears the
            override.
    AwardsReview:
      type: object
      properties:
        id:
          type: string
          format: uuid
        submission_id:
          type: string
          format: uuid
        round_id:
          type: string
          format: uuid
        reviewer_user_id:
          type: integer
        is_abstained:
          type: boolean
        abstain_reason:
          type:
            - string
            - 'null'
        scores:
          type: array
          items:
            type: object
            properties:
              field_id:
                type: string
                format: uuid
              value:
                description: Number, text, or JSON depending on the scorecard field.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    LogPage:
      type: object
      required: [data, next_cursor, has_more]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/LogEvent'
        next_cursor:
          type: string
          description: |
            Send this as `cursor` on the next call. On an empty page it is the cursor you sent,
            so a caught-up poller keeps it and waits.
        has_more:
          type: boolean
          description: '`true` when more events are ready now — fetch again immediately. `false` means wait before polling.'
    LogEvent:
      type: object
      description: |
        One event, in the same shape on every stream. Events appear about a minute after they happen
        and are ordered by when Sessionboard recorded them, so a late-arriving event is never skipped.
      properties:
        id:
          type: string
          description: Stable event id. Use it to de-duplicate if you replay a page.
        stream:
          type: string
          enum: [api_request, security_event, audit]
          description: Which log the event came from — `api_request` from `/v1/logs/api-requests`, `security_event` from `/v1/logs/security-events`, `audit` from `/v1/logs/audit-trail`.
        occurred_at:
          type: string
          format: date-time
          description: When the event happened.
        ingested_at:
          type: string
          format: date-time
          description: When Sessionboard recorded it. Pages are ordered by this.
        action:
          type: string
          description: '`GET /v1/events` for API requests, the event type (for example `login.password_ok` or `login.challenge_failed`) for security events, `<subject_type>.<operation>` for the audit trail.'
          example: GET /v1/events
        outcome:
          type: string
          enum: [success, failure, unknown]
        reason:
          type: string
          nullable: true
          description: Why it failed, when it did.
        actor:
          type: object
          properties:
            type:
              type: string
              description: '`api_token`, `oauth_token` or `anonymous` for API requests; `user` or `admin` for security events; `user`, `system` or `rule` (an automation) for the audit trail.'
            id:
              type: string
              nullable: true
            name:
              type: string
              nullable: true
            token_hint:
              type: string
              nullable: true
              description: Last characters of the token id, for API requests.
            user_email:
              type: string
              nullable: true
        src_ip:
          type: string
          nullable: true
        user_agent:
          type: string
          nullable: true
        status:
          type: integer
          nullable: true
          description: HTTP status, for API requests.
        duration_ms:
          type: integer
          nullable: true
          description: Response time, for API requests.
        target:
          type: object
          nullable: true
          properties:
            type:
              type: string
              example: event
            id:
              type: string
        details:
          type: object
          additionalProperties: true
          description: Stream-specific fields (for example `source` and `request_id` for API requests, `property` and `value` for the audit trail).
    LogExportError:
      type: object
      properties:
        error:
          type: string
          enum: [VALIDATION_ERROR, INVALID_CURSOR, CURSOR_FILTER_MISMATCH, SINCE_BEFORE_RETENTION, CURSOR_EXPIRED]
        message:
          type: string
    AskRequest:
      type: object
      additionalProperties: false
      required:
        - question
      properties:
        question:
          type: string
          minLength: 2
          maxLength: 500
          description: |
            The question, in plain language. Whitespace is collapsed before the length check:
            under 2 or over 500 characters returns `422` (`empty` / `too_long`), and a value over
            1000 characters is rejected with `400`.
        conversation_id:
          type: string
          format: uuid
          description: The `conversation_id` from a previous answer, to ask a follow-up in the same thread.
        lang:
          type: string
          maxLength: 16
          pattern: '^[A-Za-z]{2,3}([-_][A-Za-z0-9]{2,8})?$'
          description: Answer language as a language tag, e.g. `en`, `fr`, `pt-BR`.
    AskCitation:
      type: object
      description: |
        A source card built from a record. `index` matches the `[n]` marker in `answer`. The
        fields beyond `index`, `type` and `id` depend on `type` — for example `title` and
        `starts_at` on a session, `name` on a speaker, `session_id`, `session_title`, `start_ms`
        and `text` on a transcript excerpt.
      required:
        - index
        - type
        - id
      properties:
        index:
          type: integer
          description: The `n` in the answer's `[n]` marker.
        type:
          type: string
          enum:
            - session
            - speaker
            - exhibitor
            - sponsor
            - product
            - poster
            - room
            - pin
            - info
            - transcript
            - doc
            - pinned
            - help
        id:
          type: string
          description: The id of the cited record.
        title:
          type: string
        name:
          type: string
        starts_at:
          type: string
          format: date-time
        session_id:
          type: string
        session_title:
          type: string
        start_ms:
          type: integer
          description: Offset into the session recording, for a transcript excerpt.
        text:
          type: string
          description: The quoted passage, for a transcript excerpt.
      additionalProperties: true
    AskResponse:
      type: object
      required:
        - status
        - answer
        - citations
        - actions
        - conversation_id
        - message_id
      properties:
        status:
          type: string
          enum:
            - ok
        answer:
          type: string
          description: The answer as Markdown. `[n]` marks citation `n`.
        citations:
          type: array
          items:
            $ref: '#/components/schemas/AskCitation'
        actions:
          type: array
          maxItems: 0
          items: {}
          description: Always empty — this endpoint is read-only.
        conversation_id:
          type: string
          format: uuid
          description: Send this back with a follow-up question to continue the conversation.
        message_id:
          type: string
          format: uuid
          description: The id of this answer.
    TooManyRequestsError:
      type: object
      description: Body of a `429` from the API's own request limits (per token, and per organization and user). Wait `Retry-After` seconds.
      required:
        - name
        - message
      properties:
        status:
          type: integer
          example: 429
        success:
          type: boolean
          example: false
        message:
          type: string
          example: 'Error while trying to reach the endpoint: Too many requests'
        name:
          type: string
          enum:
            - TooManyRequestsError
    AskError:
      type: object
      # `error` is always present on Ask errors; requiring it keeps the
      # gateway's own 429 body ({status, success, message, name}) from also
      # matching this schema in the 429 `oneOf`.
      required:
        - error
      properties:
        error:
          type: string
          enum:
            - VALIDATION_ERROR
            - SCOPE_REQUIRED
            - EVENT_NOT_ALLOWED
            - AI_DISABLED
            - FORBIDDEN
            - RATE_LIMITED
            - QUOTA_EXCEEDED
            - INTERNAL_ERROR
        message:
          type: string
        details:
          type: object
          description: '`reason`, and for `RATE_LIMITED` also `retry_after_seconds` and `remaining_today`.'
          properties:
            reason:
              type: string
              enum:
                - empty
                - too_long
                - blocked
                - repeated
                - per_minute
                - daily
                - busy
                - 'off'
                - ai_disabled
                - ai_credits
            retry_after_seconds:
              type: integer
            remaining_today:
              type: integer
          additionalProperties: true
    # ────────────────────────────────────────────
    # Shared filter / sort helpers
    # ────────────────────────────────────────────
    DateRangeFilter:
      type: object
      description: Filter by a date range using before and/or after bounds.
      properties:
        before:
          type: string
          format: date-time
          description: Return records before this date-time.
        after:
          type: string
          format: date-time
          description: Return records after this date-time.

    NestedSortField:
      type: object
      description: |
        One sort key. `sort` is the direction; `order` is the priority when
        multiple keys are set (lower numbers apply first).
      properties:
        sort:
          type: string
          enum:
            - asc
            - desc
        order:
          type: integer

    SortOptions:
      type: object
      description: |
        Nested sort keys matching the search POST body (`createdAt.sort`, not a
        flat `order` enum). Each key is optional.
      properties:
        createdAt:
          $ref: '#/components/schemas/NestedSortField'
        updatedAt:
          $ref: '#/components/schemas/NestedSortField'

    SessionSortOptions:
      type: object
      description: Session search sort. Same nested shape as `SortOptions`, plus `startsAt`.
      properties:
        createdAt:
          $ref: '#/components/schemas/NestedSortField'
        updatedAt:
          $ref: '#/components/schemas/NestedSortField'
        startsAt:
          allOf:
            - $ref: '#/components/schemas/NestedSortField'
          description: |
            Sort by session start time. Null `starts_at` values always sort last.

    RecordSearchBody:
      type: object
      description: Common search request body used across multiple endpoints.
      properties:
        filters:
          type: object
          properties:
            createdAt:
              $ref: '#/components/schemas/DateRangeFilter'
            updatedAt:
              $ref: '#/components/schemas/DateRangeFilter'
            status:
              type: string
              enum:
                - accepted
                - accept_queue
                - pending
                - decline_queue
                - declined
              description: Filter by session status (applicable to session searches).
        sort:
          $ref: '#/components/schemas/SortOptions'
        expand:
          type: array
          items:
            type: string
            enum:
              - translated_fields
              - subsession_details
              - linked_sources
              - composition
          description: |
            Expand records with additional data. See the `Expand` parameter for `linked_sources` and `composition` details.

    SessionSearchBody:
      type: object
      description: Search request body for sessions with status filtering.
      properties:
        filters:
          type: object
          properties:
            createdAt:
              $ref: '#/components/schemas/DateRangeFilter'
            updatedAt:
              $ref: '#/components/schemas/DateRangeFilter'
            status:
              type: string
              enum:
                - accepted
                - accept_queue
                - pending
                - decline_queue
                - declined
            isAbstract:
              type: boolean
              description: Filter to abstract submissions (`true`) or program sessions (`false`).
            sessionIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            trackIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
              description: Match sessions with any of these track IDs (OR). Combined with other filters using AND. Send at least one ID — an empty array is rejected with a 400; to apply no track filter, omit the property.
            levelIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            formatIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            languageIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            tagIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
              description: Match sessions tagged with any of these tag IDs (OR). Send at least one ID — an empty array is rejected with a 400.
        sort:
          $ref: '#/components/schemas/SessionSortOptions'
        expand:
          type: array
          items:
            type: string
            enum:
              - translated_fields
              - subsession_details
              - linked_sources
              - composition

    SpeakerSearchBody:
      type: object
      description: |
        Search request body for speakers, including session metadata filters.
        Categorization filters (`trackIds`, `levelIds`, `formatIds`,
        `languageIds`, `tagIds`) match against the speaker's **top-level
        session** — a speaker on a subsession matches via the subsession's
        parent (root) session, mirroring session search semantics.
      properties:
        filters:
          type: object
          properties:
            createdAt:
              $ref: '#/components/schemas/DateRangeFilter'
            updatedAt:
              $ref: '#/components/schemas/DateRangeFilter'
            status:
              type: string
              enum:
                - accepted
                - accept_queue
                - pending
                - decline_queue
                - declined
            trackIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
              description: Send at least one ID — an empty array is rejected with a 400; to apply no track filter, omit the property.
            levelIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            formatIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            languageIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            tagIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
        sort:
          $ref: '#/components/schemas/SortOptions'
        expand:
          type: array
          items:
            type: string
            enum:
              - translated_fields

    ParticipantSearchBody:
      type: object
      description: |
        Search request body for participants. Every `SpeakerSearchBody` filter
        is accepted with the same semantics (categorization filters match the
        participant's **top-level** session), plus `roleIds`, `roleSlugs` and `email`.
      properties:
        filters:
          type: object
          properties:
            createdAt:
              $ref: '#/components/schemas/DateRangeFilter'
            updatedAt:
              $ref: '#/components/schemas/DateRangeFilter'
            status:
              type: string
              enum:
                - accepted
                - accept_queue
                - pending
                - decline_queue
                - declined
              description: Match participants on sessions with this session status.
            trackIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
              description: Send at least one ID — an empty array is rejected with a 400; to apply no track filter, omit the property.
            levelIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            formatIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            languageIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            tagIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
            roleIds:
              type: array
              minItems: 1
              items:
                type: string
                format: uuid
              description: |
                Return participants holding at least one of these roles
                (standard or custom). Ids come from
                `GET /v1/event/{eventId}/participant-roles`. An empty array is a
                400; omit the property to match every role.
            roleSlugs:
              type: array
              minItems: 1
              items:
                type: string
                minLength: 1
                maxLength: 100
              description: |
                Return participants holding at least one role with these slugs —
                the standard `speaker`, `chairperson`, `moderator`, or a custom
                role's `slug` from `GET .../participant-roles`. Works on events
                whose roles have `roleId: null`. Case-insensitive. Combines with
                `roleIds` as AND. An empty array is a 400.
            email:
              type: string
              minLength: 1
              maxLength: 320
              description: |
                Exact, case-insensitive match on the contact's email. An
                unknown address returns an empty result set, not an error.
        sort:
          $ref: '#/components/schemas/SortOptions'
        expand:
          type: array
          items:
            type: string
            enum:
              - translated_fields

    Pagination:
      type: object
      description: >-
        Pagination metadata returned with POST search responses. The
        event-settings GET lists return the same numbers under snake_case keys
        — see `ListPagination`.
      properties:
        currentPage:
          type: integer
          examples:
            - 1
        pageSize:
          type: integer
          examples:
            - 25
        totalPages:
          type: integer
          examples:
            - 4
        totalResults:
          type: integer
          examples:
            - 97

    ListPagination:
      type: object
      description: >-
        Pagination metadata returned by the event-settings GET lists (tags,
        tracks, levels, formats, rooms, languages, statuses). Same numbers as
        `Pagination`, snake_case keys — these endpoints are served by a
        different layer, and the casing is longstanding.
      properties:
        current_page:
          type: integer
          examples:
            - 1
        page_size:
          type: integer
          examples:
            - 25
        total_pages:
          type: integer
          examples:
            - 4
        total_results:
          type: integer
          examples:
            - 97

    # ────────────────────────────────────────────
    # Core domain objects
    # ────────────────────────────────────────────
    CompositionTargetSummary:
      type: object
      description: Summary of a related session in a composition relationship.
      properties:
        id:
          type: string
          format: uuid
        friendly_id:
          type: string
        title:
          type: string
        is_abstract:
          type: boolean

    CompositionStatus:
      type: object
      description: |
        Summary of how this session participates in a composition (merge or link).
        Always present on session responses.
      properties:
        role:
          type: string
          enum:
            - standalone
            - target
            - source
          description: |
            - `standalone` — normal editable record
            - `target` — composite parent with one or more linked sources
            - `source` — linked into another record (still editable)
        is_linked:
          type: boolean
        is_read_only:
          type: boolean
          enum:
            - false
          description: Always `false`. Linked sources stay editable; the key is kept for compatibility.
        source_count:
          type: integer
          description: Number of records merged or linked into this session.
        target:
          allOf:
            - $ref: '#/components/schemas/CompositionTargetSummary'
          nullable: true
          description: Present when `role` is `source`.

    CompositionSource:
      type: object
      properties:
        composition_id:
          type: string
          format: uuid
        relationship_type:
          type: string
          enum:
            - merged_session
            - merged_abstract
            - composed_into_session
        created_at:
          type: string
          format: date-time
        id:
          type: string
          format: uuid
        friendly_id:
          type: string
        title:
          type: string
        is_abstract:
          type: boolean

    CompositionDetail:
      type: object
      description: Full composition graph. Present when `expand=composition`.
      properties:
        target:
          allOf:
            - $ref: '#/components/schemas/CompositionTargetSummary'
          nullable: true
        sources:
          type: array
          items:
            $ref: '#/components/schemas/CompositionSource'

    ContactSessionSummary:
      type: object
      description: |
        Lightweight session summary for contact session lists. Includes
        composition summary fields but not the full `composition` graph.
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        status:
          type: string
        custom_status:
          type: string
          nullable: true
        is_abstract:
          type: boolean
        composition_status:
          $ref: '#/components/schemas/CompositionStatus'

    OrgContactSessionSummary:
      allOf:
        - $ref: '#/components/schemas/ContactSessionSummary'
        - type: object
          properties:
            event_id:
              type: integer
              description: Event the session belongs to.

    ContactSessionsResponse:
      type: object
      properties:
        sessions:
          type: array
          items:
            $ref: '#/components/schemas/ContactSessionSummary'
        status:
          type: string
          description: |
            Aggregated contact status derived from linked sessions
            (`accepted`, `pending`, or a single session's status).

    OrgContactSessionsResponse:
      type: object
      properties:
        sessions:
          type: array
          items:
            $ref: '#/components/schemas/OrgContactSessionSummary'
        status:
          type: string
          description: |
            Aggregated contact status derived from linked sessions
            (`accepted`, `pending`, or a single session's status).

    Event:
      type: object
      description: An event on the Sessionboard platform.
      properties:
        id:
          type: integer
          examples:
            - 3960
        name:
          type: string
          examples:
            - "Acme Conference"
        timezone:
          type: string
          examples:
            - "America/Chicago"
        starts_at:
          type: string
          format: date-time
          nullable: true
        ends_at:
          type: string
          format: date-time
          nullable: true
        features:
          type: object
          properties:
            primary_speakers:
              type: boolean
            speaker_acceptance:
              type: boolean
            translated_fields:
              type: boolean
        custom_fields:
          type: array
          description: |
            Present only when `expand=custom_fields` is passed to
            `GET /v1/events`. Empty array when the event has no values.
          items:
            $ref: '#/components/schemas/CustomFieldValue'
        integration_mappings:
          type: array
          description: |
            Present only when `expand=integration_mappings` is passed to
            `GET /v1/events`. Conference connector event IDs only — LiveBuzz
            / apps-engine campaign IDs are not included. Empty array when
            none are mapped.
          items:
            $ref: '#/components/schemas/EventIntegrationMapping'

    EventIntegrationMapping:
      type: object
      description: A conference integration's identifier for this Sessionboard event.
      properties:
        name:
          type: string
          description: Display name (for example `Cvent`, `Grip`).
          examples:
            - Cvent
        source:
          type: string
          description: >
            Integration key from `Integration_Event_Mapping_Union` (table name
            minus `Integration_` / `_Event_Mappings`, lowercased). Multi-word
            vendors use underscores, e.g. `expo_platform`.
          examples:
            - cvent
            - expo_platform
        external_event_id:
          type: string
          description: Vendor event id, always a string.
          examples:
            - a1b2c3d4-e5f6-7890-abcd-ef1234567890

    Session:
      type: object
      description: A session within an event (e.g., talk, workshop, panel, or CFP abstract submission).
      properties:
        id:
          type: string
          format: uuid
        friendly_id:
          type: string
        friendly_id_raw:
          type: integer
        title:
          type: string
        description:
          type: string
        status:
          type: string
          enum:
            - accepted
            - accept_queue
            - pending
            - decline_queue
            - declined
        custom_status_id:
          type: string
        custom_status:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        is_public:
          type: boolean
        is_abstract:
          type: boolean
          description: True for CFP abstract submissions; false for program sessions.
        composition_status:
          $ref: '#/components/schemas/CompositionStatus'
        composition:
          $ref: '#/components/schemas/CompositionDetail'
          description: Present when `expand=composition` is requested.
        external_url:
          type: string
          format: uri
        client_session_id:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        ceu_credits:
          type: number
          format: float
        capacity:
          type: integer
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomFieldValue'
        translated_fields:
          type: array
          items:
            $ref: '#/components/schemas/TranslatedField'
        speakers:
          type: array
          description: |
            Legacy junction-table speakers (`Session_Speakers`). Retained for backwards
            compatibility. Each entry includes `participant_role` when the event has
            Sessions 2.0 roles configured. Prefer `participants` for new integrations
            that need custom program roles or a single flat participant list.
          items:
            $ref: '#/components/schemas/SessionSpeaker'
        chairpersons:
          type: array
          description: |
            Legacy junction-table chairpersons (`Session_Chairpersons`). See `speakers`
            for migration guidance.
          items:
            $ref: '#/components/schemas/SessionSpeaker'
        moderators:
          type: array
          description: |
            Legacy junction-table moderators (`Session_Moderators`). See `speakers`
            for migration guidance.
          items:
            $ref: '#/components/schemas/SessionSpeaker'
        participants:
          type: array
          description: |
            Flat list of session participants from the Sessions 2.0 data model
            (`Session_Participants` + `Session_Roles`). Always returned on session
            search and get (no expand flag). Includes custom program roles (for example
            Author, Panelist) in addition to the legacy speaker/chairperson/moderator
            junction tables. Legacy arrays above remain for backwards compatibility;
            each legacy entry also includes `participant_role` when event roles are
            configured. Contact profile fields (`photo_url`, `company_name`, `title`,
            `address_country`, etc.) are hydrated from the contact record in the event's
            default language without requiring expand.
          items:
            $ref: '#/components/schemas/SessionParticipant'
        sponsors:
          type: array
          items:
            $ref: '#/components/schemas/SessionSponsor'
        exhibitors:
          type: array
          items:
            $ref: '#/components/schemas/SessionExhibitor'
        content:
          type: array
          items:
            $ref: '#/components/schemas/Content'
        tags:
          type: array
          items:
            $ref: '#/components/schemas/Tag'
        language:
          allOf:
            - $ref: '#/components/schemas/Language'
          description: |
            Assigned session language. Includes `id`, `event_id`, `name`, `order`,
            `created_at`, and `updated_at` when set. Unassigned values are an
            empty object `{}` on `POST` session search responses and `null` on
            CRUD proxy responses (`GET` list, create, update, restore).
        track:
          allOf:
            - $ref: '#/components/schemas/Track'
          description: |
            Assigned session track. Includes `id`, `event_id`, `name`, `color`,
            `order`, `created_at`, and `updated_at` when set. Unassigned values
            are `{}` on `POST` session search and `null` on CRUD proxy responses.
        level:
          allOf:
            - $ref: '#/components/schemas/Level'
          description: |
            Assigned session level. Includes `id`, `name`, `order`, `created_at`,
            and `updated_at` when set (no `event_id` on nested objects). Unassigned
            values are `{}` on `POST` session search and `null` on CRUD proxy responses.
        format:
          type: object
          description: |
            Assigned session format. Nested session objects include only `id` and
            `name` (not the full standalone `Format` schema).
          properties:
            id:
              type: string
            name:
              type: string
        room:
          allOf:
            - $ref: '#/components/schemas/Room'
          description: |
            Assigned room when `location` is `room`. Includes `id`, `name`,
            `order`, `capacity`, `created_at`, and `updated_at` when set. Omitted
            when location is `external` or unset (`{}` on `POST` search, `null` on
            CRUD proxy). `event_id` is not included on nested room objects.
        subsessions:
          type: array
          description: |
            Subsessions of this parent session. Each item defaults to the
            minimal `Subsession` shape; pass `expand=subsession_details` to
            receive the full `SubsessionDetailed` shape on every subsession
            in the array. Composition fields (`is_abstract`,
            `composition_status`, optional `composition`) are inherited from
            the parent session.

            Items are returned in the order set by the event admin
            (`subsession_order`), falling back to subsession number and then
            start time for subsessions that have never been reordered.
          items:
            oneOf:
              - $ref: '#/components/schemas/Subsession'
              - $ref: '#/components/schemas/SubsessionDetailed'
        admin_url:
          type: string
          format: uri
          nullable: true
          description: Deep link to this entity in the Sessionboard admin UI.
      examples:
        - id: "497f6eca-6276-4993-bfeb-53cbbbba6f08"
          friendly_id: "SESS-8"
          friendly_id_raw: 8
          title: "Networking"
          description: "<p>Networking session description</p>"
          status: "accepted"
          starts_at: "2024-03-15T09:00:00Z"
          ends_at: "2024-03-15T10:00:00Z"
          is_public: true
          created_at: "2024-01-10T14:30:00Z"
          updated_at: "2024-03-01T09:15:00Z"
          ceu_credits: 1.5
          capacity: 200
          language:
            id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
            event_id: 42
            name: "English"
            order: 0
            created_at: "2024-01-01T00:00:00Z"
            updated_at: "2024-01-01T00:00:00Z"
          track:
            id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
            event_id: 42
            name: "Artificial Intelligence & ML"
            color: "#329af0"
            order: 1
            created_at: "2024-01-01T00:00:00Z"
            updated_at: "2024-01-01T00:00:00Z"
          level:
            id: "c3d4e5f6-a7b8-9012-cdef-123456789012"
            name: "Beginner"
            order: 2
            created_at: "2024-01-01T00:00:00Z"
            updated_at: "2024-01-01T00:00:00Z"
          format:
            id: "d4e5f6a7-b8c9-0123-def0-234567890123"
            name: "Session"
          room:
            id: "e5f6a7b8-c9d0-1234-ef01-345678901234"
            name: "Workshop Room 1"
            order: 3
            capacity: 120
            created_at: "2024-01-01T00:00:00Z"
            updated_at: "2024-01-01T00:00:00Z"
          tags: []
          speakers: []
          content: []
          custom_fields: []
          admin_url: "https://app.sessionboard.com/event/42/sessions/497f6eca-6276-4993-bfeb-53cbbbba6f08"

    SessionStatus:
      type: object
      description: |
        A lightweight representation of a session's status, including
        soft-deleted sessions. Parent rows additionally include a
        `subsessions` array with the same minimal status-focused shape for
        each child subsession. Subsession rows that appear in the flat list
        return an empty `subsessions` array.
      properties:
        id:
          type: string
          format: uuid
        friendly_id:
          type: string
        friendly_id_raw:
          type: integer
        status:
          type: string
        custom_status_id:
          type: string
          format: uuid
          nullable: true
        custom_status:
          $ref: '#/components/schemas/SessionCustomStatus'
        is_abstract:
          type: boolean
        composition_status:
          $ref: '#/components/schemas/CompositionStatus'
        deleted_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        subsessions:
          type: array
          description: Empty for subsession rows. Populated with `SubsessionStatus` items for parent rows, in the admin-defined order (`subsession_order`, falling back to subsession number and start time).
          items:
            $ref: '#/components/schemas/SubsessionStatus'

    Contact:
      type: object
      description: A contact (speaker, attendee, or organizational contact) in Sessionboard.
      properties:
        id:
          type: string
        friendly_id:
          type: string
        full_name:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        created_at:
          type: string
        updated_at:
          type: string
        photo_url:
          type: string
        company_name:
          type: string
        title:
          type: string
        about:
          type: string
        phone_home:
          type: string
        phone_mobile:
          type: string
        address_line_1:
          type: string
        address_line_2:
          type: string
        address_city:
          type: string
        address_state:
          type: string
        address_postal_code:
          type: string
        address_country:
          type: string
        website_url:
          type: string
        linkedin_url:
          type: string
        twitter_url:
          type: string
        facebook_url:
          type: string
        honorific:
          type: string
        salutation:
          type: string
        pronouns:
          type: string
        gender:
          type: string
        past_companies:
          type: string
        educational_affiliation:
          type: string
        highest_level_of_education:
          type: string
        ethnicity:
          type: string
        languages:
          type: string
        speaker_score:
          type: string
        organization_contact:
          type: string
        speaker_fee:
          type: string
        availability:
          type: string
        preferred_session_format:
          type: string
        annual_revenue:
          type: string
        headcount:
          type: string
        years_in_operation:
          type: string
        organization_structure:
          type: string
        industry:
          type: string
        global_region:
          type: string
        audience_type:
          type: string
        target_age_range:
          type: string
        topic_expertise:
          type: string
        brand:
          type: string
        translated_fields:
          type: array
          items:
            $ref: '#/components/schemas/TranslatedField'
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomFieldValue'
        admin_url:
          type: string
          format: uri
          nullable: true
          description: Deep link to this entity in the Sessionboard admin UI.
      examples:
        - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          friendly_id: "SPK-42"
          full_name: "Dr. Jane Smith"
          first_name: "Jane"
          last_name: "Smith"
          email: "jane.smith@university.edu"
          created_at: "2023-11-01T10:00:00Z"
          updated_at: "2024-02-15T16:30:00Z"
          photo_url: "https://cdn.sessionboard.com/photos/jane-smith.jpg"
          company_name: "State University"
          title: "Professor of Computer Science"
          about: "Dr. Smith is a leading researcher in distributed systems."
          address_city: "Austin"
          address_state: "TX"
          address_country: "US"
          honorific: "Dr."
          pronouns: "she/her"
          topic_expertise: "Distributed Systems, Cloud Computing"
          custom_fields: []
          translated_fields: []

    SessionSpeaker:
      type: object
      description: |
        A contact embedded on a session (speaker, chairperson, moderator, or other
        program role). Standard contact profile fields (`photo_url`, `company_name`,
        `title`, `address_country`, etc.) are hydrated from the contact's module field
        values in the event's default language. No expand is required for these fields;
        use `expand=translated_fields` for other locales.
      properties:
        id:
          type: string
        participant_role:
          $ref: '#/components/schemas/ParticipantRole'
          description: |
            The event's configured program role for this participant (Sessions 2.0).
            `name` and `slug` reflect the configured `Session_Roles` record (for example
            Author, Panelist). `core_role` is the legacy junction category mapping only
            (`speaker`, `chairperson`, `moderator`) and must not be used as the display
            role label. Present on legacy `speakers`, `chairpersons`, and `moderators`
            entries when the event has `Session_Roles` configured.
        friendly_id:
          type: string
        full_name:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        created_at:
          type: string
        updated_at:
          type: string
        photo_url:
          type: string
        company_name:
          type: string
        title:
          type: string
        about:
          type: string
        phone_home:
          type: string
        phone_mobile:
          type: string
        address_line_1:
          type: string
        address_line_2:
          type: string
        address_city:
          type: string
        address_state:
          type: string
        address_postal_code:
          type: string
        address_country:
          type: string
        website_url:
          type: string
        linkedin_url:
          type: string
        twitter_url:
          type: string
        facebook_url:
          type: string
        instagram_url:
          type: string
        snapchat_username:
          type: string
        tiktok_username:
          type: string
        translated_fields:
          type: array
          items:
            $ref: '#/components/schemas/TranslatedField'
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomFieldValue'

    ParticipantRole:
      type: object
      description: |
        An event-configured session participant role (`Session_Roles`). Use `name` (or
        `slug` for programmatic matching) as the participant's role label. `core_role`
        is the legacy junction category only.
      properties:
        id:
          type: string
          format: uuid
          description: Session_Roles.id
        slug:
          type: string
          description: Stable programmatic identifier for the role within the event.
        name:
          type: string
          description: Display name (singular).
        name_plural:
          type: string
          description: Display name (plural).
        core_role:
          type: string
          enum:
            - speaker
            - chairperson
            - moderator
          description: Legacy junction category this role maps to for integrations.

    SessionParticipant:
      allOf:
        - $ref: '#/components/schemas/SessionSpeaker'
        - type: object
          description: |
            Sessions 2.0 participant row. Includes the same contact profile fields as
            `SessionSpeaker`. `participant_role` is always present (sourced from
            `Session_Roles`).
          properties:
            session_participant_id:
              type: string
              format: uuid
              description: Primary key of the `Session_Participants` row.
            acceptance_status:
              type: string
              enum:
                - pending
                - accepted
                - declined
                - not_accepted
              description: |
                The participant's answer to their acceptance invitation. Present only
                when the event has Participant Acceptance turned on
                (`features.speaker_acceptance`).
            acceptance_invite_sent_at:
              type:
                - string
                - 'null'
              format: date-time
              description: |
                When the acceptance invitation for this role was last sent, or `null`
                if it has not been sent. Present only when the event has Participant
                Acceptance turned on.

    ParticipantRoleAssignment:
      type: object
      description: |
        One role a participant holds on the event, with the sessions it holds
        it on. Returned inside `Participant.roles`.
      required:
        - roleId
        - roleName
        - slug
        - coreRole
        - isCustom
        - sessionIds
      properties:
        roleId:
          type:
            - string
            - 'null'
          format: uuid
          description: |
            The role's id — the value to pass in the `roleIds` filter and the
            `id` returned by `GET /v1/event/{eventId}/participant-roles`.
            `null` only for a standard role on an event whose roles have not
            been created yet (see the endpoint description).
        roleName:
          type: string
          description: Display name of the role (singular), e.g. `Speaker`, `Panelist`.
        slug:
          type: string
          description: Stable programmatic identifier for the role within the event.
        coreRole:
          type: string
          enum:
            - speaker
            - chairperson
            - moderator
          description: The standard category the role maps to. Not a display label.
        isCustom:
          type: boolean
          description: '`true` for a role the organizer added; `false` for Speaker, Chairperson and Moderator.'
        sessionIds:
          type: array
          items:
            type: string
            format: uuid
          description: Ids of the sessions (including subsessions) on which the contact holds this role.

    Participant:
      allOf:
        - $ref: '#/components/schemas/Contact'
        - type: object
          description: |
            A contact holding at least one session role on the event. Every
            `Contact` field is present with the same values `POST /speakers`
            returns; `roles` is appended after them.
          required:
            - roles
          properties:
            roles:
              type: array
              description: |
                Every role the contact holds on the event and the sessions it
                holds each one on. Always the contact's complete set — not
                narrowed by the request filters. Ordered by the role's
                configured sort order.
              items:
                $ref: '#/components/schemas/ParticipantRoleAssignment'
      examples:
        - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          friendly_id: "SPK-42"
          full_name: "Dr. Jane Smith"
          first_name: "Jane"
          last_name: "Smith"
          email: "jane.smith@university.edu"
          created_at: "2023-11-01T10:00:00Z"
          updated_at: "2024-02-15T16:30:00Z"
          company_name: "State University"
          title: "Professor of Computer Science"
          custom_fields: []
          roles:
            - roleId: "84bd12e5-d5ec-4c64-a604-4051e06c8dc2"
              roleName: "Speaker"
              slug: "speaker"
              coreRole: "speaker"
              isCustom: false
              sessionIds:
                - "04efb360-2b15-44db-b639-4035fae2772c"
            - roleId: "4efeb845-050f-4002-9496-cb586cdf10f2"
              roleName: "Moderator"
              slug: "moderator"
              coreRole: "moderator"
              isCustom: false
              sessionIds:
                - "26e465fc-1fb1-4c04-af52-f4d6314dfca4"
            - roleId: "52d60237-19db-4bda-a116-8b9fcb6750e9"
              roleName: "Panelist"
              slug: "panelist"
              coreRole: "speaker"
              isCustom: true
              sessionIds:
                - "140b8e82-41ca-444e-82ab-f7dd1011e95b"
                - "2c198cce-adff-4fb2-81e1-319b601d564b"

    EventParticipantRole:
      type: object
      description: |
        A participant role configured on an event, as returned by
        `GET /v1/event/{eventId}/participant-roles`.
      properties:
        id:
          type: string
          format: uuid
          description: Use as a `roleIds` filter value on `POST /v1/event/{eventId}/participants`.
        name:
          type: string
          description: Display name (singular).
        name_plural:
          type: string
          description: Display name (plural).
        slug:
          type: string
          description: Stable programmatic identifier within the event.
        core_role:
          type: string
          enum:
            - speaker
            - chairperson
            - moderator
          description: The standard category the role maps to. Not a display label.
        is_custom:
          type: boolean
          description: '`true` for a role the organizer added; `false` for the three standard roles.'
        sort_order:
          type: integer
          description: Position in the Sessions settings page; results are returned in this order.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Sponsor:
      type: object
      description: A sponsor associated with an event.
      properties:
        id:
          type: string
        friendly_id:
          type: string
        friendly_id_raw:
          type: integer
        name:
          type: string
        description:
          type: string
        logo_image_url:
          type: string
        banner_image_url:
          type: string
        phone:
          type: string
        address_line_1:
          type: string
        address_line_2:
          type: string
        address_city:
          type: string
        address_state:
          type: string
        address_postal_code:
          type: string
        address_coutnry:
          type: string
          description: 'Country (note: field name contains a known typo preserved for backwards compatibility).'
        facebook_url:
          type: string
        instagram_url:
          type: string
        linkedin_url:
          type: string
        snapchat_username:
          type: string
        tiktok_username:
          type: string
        twitter_url:
          type: string
        website_url:
          type: string
        created_at:
          type: string
        updated_at:
          type: string
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomFieldValue'
        translated_fields:
          type: array
          items:
            $ref: '#/components/schemas/TranslatedField'
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/AccountContact'
        admin_url:
          type: string
          format: uri
          nullable: true
          description: Deep link to this entity in the Sessionboard admin UI.
      examples:
        - id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
          friendly_id: "SPON-3"
          friendly_id_raw: 3
          name: "TechCorp Global"
          description: "Platinum sponsor and technology partner."
          logo_image_url: "https://cdn.sessionboard.com/logos/techcorp.png"
          phone: "+1-555-0100"
          address_city: "San Francisco"
          address_state: "CA"
          address_coutnry: "US"
          website_url: "https://www.techcorp.example.com"
          linkedin_url: "https://www.linkedin.com/company/techcorp"
          created_at: "2024-01-05T08:00:00Z"
          updated_at: "2024-02-20T12:00:00Z"
          custom_fields: []
          translated_fields: []
          contacts: []

    SessionSponsor:
      type: object
      description: A sponsor associated with a session. Same shape as Sponsor.
      properties:
        id:
          type: string
        friendly_id:
          type: string
        friendly_id_raw:
          type: integer
        name:
          type: string
        description:
          type: string
        logo_image_url:
          type: string
        banner_image_url:
          type: string
        phone:
          type: string
        address_line_1:
          type: string
        address_line_2:
          type: string
        address_city:
          type: string
        address_state:
          type: string
        address_postal_code:
          type: string
        address_coutnry:
          type: string
        facebook_url:
          type: string
        instagram_url:
          type: string
        linkedin_url:
          type: string
        snapchat_username:
          type: string
        tiktok_username:
          type: string
        twitter_url:
          type: string
        website_url:
          type: string
        created_at:
          type: string
        updated_at:
          type: string
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomFieldValue'
        translated_fields:
          type: array
          items:
            $ref: '#/components/schemas/TranslatedField'
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/AccountContact'

    Exhibitor:
      type: object
      description: An exhibitor associated with an event.
      properties:
        id:
          type: string
        friendly_id:
          type: string
        friendly_id_raw:
          type: integer
        name:
          type: string
        description:
          type: string
        logo_image_url:
          type: string
        banner_image_url:
          type: string
        phone:
          type: string
        address_line_1:
          type: string
        address_line_2:
          type: string
        address_city:
          type: string
        address_state:
          type: string
        address_postal_code:
          type: string
        address_coutnry:
          type: string
          description: 'Country (note: field name contains a known typo preserved for backwards compatibility).'
        facebook_url:
          type: string
        instagram_url:
          type: string
        linkedin_url:
          type: string
        snapchat_username:
          type: string
        tiktok_username:
          type: string
        twitter_url:
          type: string
        website_url:
          type: string
        created_at:
          type: string
        updated_at:
          type: string
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomFieldValue'
        translated_fields:
          type: array
          items:
            $ref: '#/components/schemas/TranslatedField'
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/AccountContact'
        admin_url:
          type: string
          format: uri
          nullable: true
          description: Deep link to this entity in the Sessionboard admin UI.
      examples:
        - id: "c3d4e5f6-a7b8-9012-cdef-123456789012"
          friendly_id: "EXH-7"
          friendly_id_raw: 7
          name: "InnovateTech Labs"
          description: "Showcasing next-generation event technology solutions."
          logo_image_url: "https://cdn.sessionboard.com/logos/innovatetech.png"
          address_city: "Chicago"
          address_state: "IL"
          address_coutnry: "US"
          website_url: "https://www.innovatetech.example.com"
          created_at: "2024-01-12T10:00:00Z"
          updated_at: "2024-03-01T14:00:00Z"
          custom_fields: []
          translated_fields: []
          contacts: []

    SessionExhibitor:
      type: object
      description: An exhibitor associated with a session. Same shape as Exhibitor.
      properties:
        id:
          type: string
        friendly_id:
          type: string
        friendly_id_raw:
          type: integer
        name:
          type: string
        description:
          type: string
        logo_image_url:
          type: string
        banner_image_url:
          type: string
        phone:
          type: string
        address_line_1:
          type: string
        address_line_2:
          type: string
        address_city:
          type: string
        address_state:
          type: string
        address_postal_code:
          type: string
        address_coutnry:
          type: string
        facebook_url:
          type: string
        instagram_url:
          type: string
        linkedin_url:
          type: string
        snapchat_username:
          type: string
        tiktok_username:
          type: string
        twitter_url:
          type: string
        website_url:
          type: string
        created_at:
          type: string
        updated_at:
          type: string
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomFieldValue'
        translated_fields:
          type: array
          items:
            $ref: '#/components/schemas/TranslatedField'
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/AccountContact'

    AccountContact:
      type: object
      description: A contact associated with a sponsor or exhibitor account.
      properties:
        id:
          type: string
        friendly_id:
          type: string
        friendly_id_raw:
          type: integer
        full_name:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        photo_url:
          type: string
        company_name:
          type: string
        title:
          type: string
        about:
          type: string
        is_primary:
          type: boolean
        contact_created_at:
          type: string
        contact_updated_at:
          type: string
        relationship_created_at:
          type: string
        relationship_updated_at:
          type: string

    CustomFieldValue:
      type: object
      description: A custom field value attached to a record.
      properties:
        id:
          type: string
        name:
          type: string
        value:
          type:
            - string
            - boolean
            - 'null'
          description: |
            The formatted field value, or `null` when the field is empty.
            Numeric field types (`number`, `currency`, `percent`, `quantity`,
            `formula`) serialize a stored zero as its formatted value (for
            example `"0"`) — `null` means the field has no value, not that
            the value is zero.

            `checkbox` fields serialize as the strings `"Yes"` (checked) and
            `"No"` (explicitly unchecked); a checkbox that was never set is
            `null`. Writes take boolean `true` / `false`.

            `datetime` fields serialize as ISO 8601. On event custom fields
            (`GET /v1/events?expand=custom_fields`) the value is 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 the value is the raw UTC instant.

            `date` (date-only) fields serialize as `M-DD-YYYY`: month without
            a leading zero, day with one, four-digit year (for example
            `9-22-2026` for 22 September 2026, `3-04-2026` for 4 March 2026).
            This is not ISO 8601; parse it with that exact pattern.
        type:
          type: string
        internal_name:
          type: string
        created_at:
          type: string

    TranslatedField:
      type: object
      description: A translated version of a field value.
      properties:
        id:
          type: string
        name:
          type: string
        type:
          type: string
        parent_field_id:
          type: string
        parent_field_source:
          type: string
        parent_field_name:
          type: string
        parent_field_internal_name:
          type: string
        translated_language:
          type: string
        value:
          type: string

    TranslatedFieldWrite:
      type: object
      description: |
        Write shape for `translated_fields` on create/update. Same identifying
        keys as the read `TranslatedField` object; extra read-only properties
        (`id`, `name`, `parent_field_id`, …) are ignored if sent.

        Validation (applies before anything is written — an invalid entry fails
        the whole request with `400` `VALIDATION_ERROR` and nothing is
        partially saved):
        - `translated_fields` must be an array of these objects.
        - Every entry must include a non-empty `translated_language`.
        - `translated_language` must be one of the event's enabled locales
          (the event default plus enabled language variants, e.g. `en_US`,
          `ar`). The error message lists the enabled locales.
      required:
        - parent_field_internal_name
        - translated_language
      properties:
        parent_field_internal_name:
          type: string
        translated_language:
          type: string
          description: |
            Locale code for the language variant (for example `fr` or `ar`).
            Must be enabled on the event; otherwise the request fails with `400`.
        value:
          description: Value for that locale. Same type as the parent field.

    Content:
      type: object
      description: |
        A content attachment (file, document, or media) associated with a session.

        **Approvals.** On events using Approvals (Early Access), a file can be held
        back until it is approved and its release time has passed. The Session Files
        endpoints still list a held file, so an integration knows it exists, but return
        `url: null` until it is released. Publish only files whose `approval_status`
        is `approved` or `not_required`. When a file you already published moves to
        any other status, remove it from your destination.
      properties:
        id:
          type: string
          format: uuid
        url:
          type: [string, 'null']
          format: uri
          description: |
            Download URL. `null` while Approvals holds the file back (see
            `approval_status`), so nothing built on the API can publish it early.
        title:
          type: string
        filename:
          type: string
        size:
          type: integer
        mimetype:
          type: string
        type:
          type: string
          nullable: true
          description: |
            Organizer-facing file type, e.g. `presentation`, `handout`,
            `poster`, or `holding_slide` for slides generated by Sessionboard.
            Filterable with `?type=` on the list endpoint.
          example: holding_slide
        label:
          type: string
          nullable: true
          description: Display label shown on the session's Files tab, e.g. `Holding Slide`.
          example: Holding Slide
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        assigned_participant_id:
          type: string
          format: uuid
          nullable: true
          description: Contact UUID of the session participant this file is assigned to.
        assigned_participant_email:
          type: string
          format: email
          nullable: true
        assigned_participant_name:
          type: string
          nullable: true
        content_slot_id:
          type: [string, 'null']
          format: uuid
          description: |
            The upload slot this file answers — see
            `GET /v1/event/{eventId}/content-slots`. Present only while the
            event uses upload slots; `null` for events on a single file
            request, and for files uploaded before slots existed or outside
            any slot.
        approval_status:
          type: string
          enum: [not_required, pending, approved, denied, superseded, embargoed]
          description: |
            Approval state, returned by the Session Files endpoints.
            - `not_required`: not under an approval workflow (no workflow, workflow off, or uploaded before it was turned on).
            - `pending`: waiting on a reviewer.
            - `approved`: approved and released.
            - `denied`: sent back to the uploader for changes.
            - `superseded`: a newer version replaced it before it was decided.
            - `embargoed`: approved (or not required) but held until a release time; see `embargo_until`.

            Only `approved` and `not_required` files have a `url`.
          example: approved
        embargo_until:
          type: [string, 'null']
          format: date-time
          description: |
            When an `embargoed` file is released (UTC). `null` when it is not
            embargoed, or when it waits for its session to be scheduled (release
            "when each file's session starts").

    ContentSlot:
      type: object
      description: |
        One file an event asks its speakers for (SB "upload slot"). Slots are
        configured under Program → Settings → Files; `session_filters` and
        role restrictions are internal and not exposed.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          description: The name speakers see, e.g. "Poster".
        content_type:
          type: string
          enum: [poster, presentation, handout, abstract, custom]
          description: What kind of file the slot collects. Poster galleries read `poster` slots.
        description:
          type: [string, 'null']
          description: Instructions shown to speakers above the drop zone.
        due_date:
          type: [string, 'null']
          format: date-time
          description: Deadline shown to speakers. `null` means no deadline.
        due_date_extended:
          type: [string, 'null']
          format: date-time
          description: Later date the drop zone stays open until, if the organizer set one.
        allowed_file_types:
          type: array
          items:
            type: string
          description: |
            Accepted type keys (`pdf`, `ppt`, `word`, `excel`, `image`, `png`,
            `jpeg`, `gif`, `svg`, `video`, `mp4`, `audio`, `zip`, `csv`,
            `text`). An empty array accepts any type.
        required:
          type: boolean
          description: Whether the slot counts as missing until a file is uploaded.
        min_files:
          type: [integer, 'null']
        max_files:
          type: [integer, 'null']
          description: Maximum files per session; `null` means unlimited.
        allow_after_deadline:
          type: boolean
          description: Whether uploads are still accepted after the deadline.
        order:
          type: integer
          description: Position in the list speakers see (0-based).
        is_active:
          type: boolean
          description: "`false` for archived slots (only returned with `include_archived=true`)."
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ContentSlotListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ContentSlot'

    Tag:
      type: object
      description: |
        An event taxonomy tag that can be applied to sessions.

        The list, create, and update endpoints return `type`, `color`, and
        `order`; tags nested inside a `Session` response and the search
        endpoint (`POST /tags`) return the identifying fields only.
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [tag]
        name:
          type: string
        color:
          type: string
          nullable: true
        order:
          type: integer
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string

    Track:
      type: object
      description: |
        A session track (e.g., a conference track or topic area).

        When nested inside a `Session` response, includes `event_id`. Standalone
        metadata list and search endpoints (`GET`/`POST /tracks`) omit `event_id`
        because the event is implicit from the URL.
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: integer
        name:
          type: string
        color:
          type: string
        order:
          type: integer
        created_at:
          type: string
        updated_at:
          type: string

    Language:
      type: object
      description: |
        A language configured for an event.

        When nested inside a `Session` response, includes `event_id`. Standalone
        metadata list and search endpoints (`GET`/`POST /languages`) omit
        `event_id` because the event is implicit from the URL.
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: integer
        name:
          type: string
        order:
          type: integer
        created_at:
          type: string
        updated_at:
          type: string

    Format:
      type: object
      description: A session format (e.g., Workshop, Panel, Keynote).
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: integer
        name:
          type: string
        order:
          type: integer
        created_at:
          type: string
        updated_at:
          type: string

    Level:
      type: object
      description: A session difficulty level (e.g., Beginner, Intermediate, Advanced).
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        order:
          type: integer
        created_at:
          type: string
        updated_at:
          type: string

    Room:
      type: object
      description: |
        A room or venue location for sessions. When nested inside a `Session`
        response, `room` is only populated when the session `location` is
        `room` (not for external URLs).
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        order:
          type: integer
        capacity:
          type: integer
          nullable: true
          description: |
            Maximum room capacity. Returned on nested session `room` objects and
            standalone room list/search endpoints.
        created_at:
          type: string
        updated_at:
          type: string

    Subsession:
      type: object
      description: |
        A subsession nested within a parent session. This is the default
        minimal shape returned inside `parent.subsessions[]` when the
        `subsession_details` expand value is not requested.

        To receive the full parent-shape parity (`status`, `custom_status`,
        `custom_fields`, `chairpersons`, `moderators`, `sponsors`,
        `exhibitors`, `tags`, `language`, `track`, `level`, `room`, etc.),
        pass `expand=subsession_details` on the GET or POST search request.
        See the `SubsessionDetailed` schema.

        `is_abstract`, `composition_status`, and optional `composition`
        (when `expand=composition`) are inherited from the parent session.

        Alternatively, call `GET /v1/event/{eventId}/sessions/{sessionId}`
        with the subsession's UUID directly — the endpoint always returns the
        full session shape at the top level.
      properties:
        id:
          type: string
          format: uuid
        friendly_id:
          type: string
          description: Human-readable id formatted as `<event-prefix>-<parent-friendly-id>-<subsession-number>` (or `<event-prefix>-<subsession-friendly-id>` when the event has `show_subsession_friendly_id` enabled).
        friendly_id_raw:
          type: integer
          description: Underlying numeric friendly id (the suffix of `friendly_id`).
        parent_session_friendly_id:
          type: string
          description: Human-readable friendly id of the parent session, e.g. `SESS-3370`.
        parent_session_friendly_id_raw:
          type: integer
          description: Numeric friendly id of the parent session.
        subsession_order:
          type: integer
          nullable: true
          description: Admin-defined position among the parent session's subsessions; subsessions are returned in this order, falling back to subsession number and start time. `null` when the parent's subsessions have never been reordered.
        title:
          type: string
        description:
          type: string
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        translated_fields:
          type: array
          description: Present only when `expand=translated_fields` is set and the event has language variants configured.
          items:
            type: object
        speakers:
          type: array
          items:
            $ref: '#/components/schemas/SessionSpeaker'
        participants:
          type: array
          description: Sessions 2.0 participants for this subsession (see `Session.participants`).
          items:
            $ref: '#/components/schemas/SessionParticipant'
        is_abstract:
          type: boolean
          description: Inherited from the parent session.
        composition_status:
          $ref: '#/components/schemas/CompositionStatus'
        format:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
        content:
          type: array
          items:
            $ref: '#/components/schemas/Content'

    SubsessionDetailed:
      type: object
      description: |
        Full-parity subsession returned inside `parent.subsessions[]` when the
        `subsession_details` expand value is set on the request. The shape
        mirrors `Session` (minus the recursive `subsessions` property, since
        subsessions cannot be nested) and adds the parent-friendly-id fields
        so callers can iterate the array and then call
        `GET /v1/event/{eventId}/sessions/{sessionId}` with each subsession
        id for the same shape at the top level.

        `is_abstract`, `composition_status`, and optional `composition`
        (when `expand=composition`) are inherited from the parent session.
      properties:
        id:
          type: string
          format: uuid
        friendly_id:
          type: string
        friendly_id_raw:
          type: integer
        parent_session_friendly_id:
          type: string
        parent_session_friendly_id_raw:
          type: integer
        subsession_order:
          type: integer
          nullable: true
          description: Admin-defined position among the parent session's subsessions; subsessions are returned in this order, falling back to subsession number and start time. `null` when the parent's subsessions have never been reordered.
        title:
          type: string
        description:
          type: string
        status:
          type: string
        custom_status_id:
          type: string
          format: uuid
          nullable: true
        custom_status:
          $ref: '#/components/schemas/SessionCustomStatus'
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        is_public:
          type: boolean
        is_abstract:
          type: boolean
          description: Inherited from the parent session.
        composition_status:
          $ref: '#/components/schemas/CompositionStatus'
        external_url:
          type: string
          nullable: true
        client_session_id:
          type: string
          nullable: true
        ceu_credits:
          type: number
          nullable: true
        capacity:
          type: integer
          nullable: true
        translated_fields:
          type: array
          description: Present only when `expand=translated_fields` is set and the event has language variants configured.
          items:
            type: object
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomFieldValue'
        speakers:
          type: array
          items:
            $ref: '#/components/schemas/SessionSpeaker'
        chairpersons:
          type: array
          items:
            $ref: '#/components/schemas/SessionSpeaker'
        moderators:
          type: array
          items:
            $ref: '#/components/schemas/SessionSpeaker'
        participants:
          type: array
          description: Sessions 2.0 participants for this subsession (see `Session.participants`).
          items:
            $ref: '#/components/schemas/SessionParticipant'
        sponsors:
          type: array
          items:
            $ref: '#/components/schemas/Sponsor'
        exhibitors:
          type: array
          items:
            $ref: '#/components/schemas/Exhibitor'
        content:
          type: array
          items:
            $ref: '#/components/schemas/Content'
        tags:
          type: array
          items:
            $ref: '#/components/schemas/Tag'
        language:
          allOf:
            - $ref: '#/components/schemas/Language'
          description: Same shape as `Session.language` when assigned; `{}` when unassigned.
        track:
          allOf:
            - $ref: '#/components/schemas/Track'
          description: Same shape as `Session.track` when assigned; `{}` when unassigned.
        level:
          allOf:
            - $ref: '#/components/schemas/Level'
          description: Same shape as `Session.level` when assigned; `{}` when unassigned.
        format:
          type: object
          description: Nested format includes only `id` and `name`.
          properties:
            id:
              type: string
            name:
              type: string
        room:
          allOf:
            - $ref: '#/components/schemas/Room'
          description: |
            Same shape as `Session.room` when `location` is `room`; `{}` when
            location is external or unset.
        source:
          type: string
      examples:
        - id: "ff14c2e4-2abf-4610-8267-0d2935f19576"
          friendly_id: "SESS-3370-3"
          friendly_id_raw: 3052
          parent_session_friendly_id: "SESS-3370"
          parent_session_friendly_id_raw: 3370
          title: "Imagining a better tomorrow"
          status: "accepted"
          is_public: true
          custom_status: null
          custom_fields:
            - id: "749c5737-cde4-44cc-acf6-15a6ccf74a5f"
              name: "Co-authors"
              type: "textarea"
              internal_name: "co_authors"
              value: "Mohamed Tarik, Mar..."
              created_at: "2026-01-21T13:15:57.710Z"
          chairpersons: []
          moderators: []
          sponsors: []
          exhibitors: []
          tags: []
          speakers: []
          content: []
          language:
            id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
            event_id: 72
            name: "English"
            order: 0
            created_at: "2025-01-01T00:00:00Z"
            updated_at: "2025-02-01T00:00:00Z"
          track:
            id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
            event_id: 72
            name: "Artificial Intelligence & ML"
            color: "#329af0"
            order: 1
            created_at: "2025-01-01T00:00:00Z"
            updated_at: "2025-02-01T00:00:00Z"
          level:
            id: "c3d4e5f6-a7b8-9012-cdef-123456789012"
            name: "Beginner"
            order: 2
            created_at: "2025-01-01T00:00:00Z"
            updated_at: "2025-02-01T00:00:00Z"
          format:
            id: "d4e5f6a7-b8c9-0123-def0-234567890123"
            name: "Technical Programme Forum Paper"
          room:
            id: "e5f6a7b8-c9d0-1234-ef01-345678901234"
            name: "Workshop Room 1"
            order: 3
            capacity: 120
            created_at: "2025-01-01T00:00:00Z"
            updated_at: "2025-02-01T00:00:00Z"
          source: "Manual"

    SubsessionStatus:
      type: object
      description: A lightweight representation of a subsession's status, included as part of the parent SessionStatus response.
      properties:
        id:
          type: string
          format: uuid
        friendly_id:
          type: string
        friendly_id_raw:
          type: integer
        parent_session_friendly_id:
          type: string
        parent_session_friendly_id_raw:
          type: integer
        subsession_order:
          type: integer
          nullable: true
          description: Admin-defined position among the parent session's subsessions; subsessions are returned in this order, falling back to subsession number and start time. `null` when the parent's subsessions have never been reordered.
        status:
          type: string
          nullable: true
          description: |
            The subsession's own approval status from the database. The API does
            not inherit the parent session's status. May be null when the
            subsession has no explicit status set.
        custom_status_id:
          type: string
          format: uuid
          nullable: true
        custom_status:
          $ref: '#/components/schemas/SessionCustomStatus'
        deleted_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    SessionCustomStatus:
      type: object
      description: A custom session status definition configured for an event.
      properties:
        id:
          type: string
        name:
          type: string
        status:
          type: string
          description: |
            Approval state this custom label maps onto. Stored NOT NULL, so it
            is always present on a status created through this API.
          enum:
            - pending
            - accepted
            - declined
            - accept_queue
            - decline_queue
        status_name:
          type: string
          nullable: true
        color:
          type: string
        order:
          type: integer
        is_custom:
          type: boolean
        created_at:
          type: string
          nullable: true
        created_by_user_id:
          type: integer
          nullable: true
        created_by_user_name:
          type: string
          nullable: true

    GDPRRequest:
      type: object
      description: A GDPR data access or erasure request, and the record of what it did.
      properties:
        id:
          type: string
          description: Identifier of the stored request. Quote this when evidencing a DSAR.
        org_id:
          type: string
        request_type:
          type: string
          enum:
            - erasure
            - access
        identity_type:
          type: string
          enum:
            - email
        identity_value:
          type: string
        status:
          type: string
          enum:
            - complete
            - no_records_found
            - pending
          description: |
            Outcome of the request.

            - `complete` — at least one record matched and was processed.
            - `no_records_found` — nothing in this organization matched the
              email, so nothing was erased.
            - `pending` — the request is stored but has not finished
              processing.
        matching_user_exists:
          type: boolean
          description: True when the email matched at least one record of any kind.
        response:
          type: object
          description: >-
            What the request matched and what happened to it. `people`,
            `users_scrubbed`, `users_retained` and `counts` are present on
            requests processed recently and absent on older records; an absent
            field means "not recorded", not "zero".
          properties:
            users:
              type: array
              description: User accounts identified for this email.
              items:
                type: integer
            org_users:
              type: array
              description: Organization membership records removed.
              items:
                type: string
            event_users:
              type: array
              description: Event participation records removed.
              items:
                type: integer
            contact_records:
              type: array
              description: Contact records marked deleted.
              items:
                type: string
            people:
              type: array
              description: Person records whose personal data was scrubbed in place.
              items:
                type: string
            users_scrubbed:
              type: array
              description: User accounts whose personal data was de-identified.
              items:
                type: integer
            users_retained:
              type: array
              description: >-
                User accounts left intact because they still belong to another
                organization, with the reason recorded.
              items:
                type: object
            counts:
              type: object
              description: Per-category totals for the arrays above.
              properties:
                org_users:
                  type: integer
                event_users:
                  type: integer
                contact_records:
                  type: integer
                people:
                  type: integer
                users_identified:
                  type: integer
                users_scrubbed:
                  type: integer
                users_retained:
                  type: integer
        created_at:
          type: string
        updated_at:
          type: string

    Field:
      type: object
      description: A field definition (standard or custom) for an event module.
      properties:
        id:
          type: string
        internal_name:
          type: string
        public_name:
          type: string
        field_type:
          type: string
        field_source:
          type: string
        contains_pii:
          type: boolean
          description: Whether this field contains personally identifiable information (PII).
        scope:
          type: string
        createdAt:
          type: string
        updatedAt:
          type: string

    # ────────────────────────────────────────────
    # Bulk operations
    # ────────────────────────────────────────────
    BulkOperations:
      type: object
      description: |
        Request body for bulk create/update/delete operations.

        Operations are applied in request order, each in its own transaction,
        and `results[i]` describes `operations[i]`. A failed operation is
        rolled back whole and does not stop the ones after it.
      required:
        - operations
      properties:
        operations:
          type: array
          maxItems: 100
          description: Array of operations to execute (max 100).
          items:
            type: object
            required:
              - action
            properties:
              action:
                type: string
                enum:
                  - create
                  - update
                  - delete
                description: The operation to perform.
              id:
                type: string
                format: uuid
                description: Entity ID. Required for `update` and `delete` actions.
              data:
                type: object
                description: |
                  Entity-specific fields. Required for `create` and `update` actions.
                  Session create supports `is_abstract` (boolean, default false).
                  `is_abstract` cannot be changed on update.

                  `data` takes the same fields as the matching single-record
                  create/update endpoint, including `custom_fields` and
                  `translated_fields`. A `translated_fields` entry for a
                  language the event has not enabled fails that operation with
                  `400` and rolls it back — it is no longer accepted and
                  discarded.

                  `custom_fields` is honoured on the contacts, sessions,
                  exhibitors and sponsors bulk endpoints. An unknown
                  `internal_name`, a value of the wrong type, or a field owned
                  by an integration fails **that operation only** — the
                  operations around it in the same batch still commit.

                  **Exception — draft sessions.** `POST
                  /event/{eventId}/agenda-drafts/{draftId}/sessions/bulk` does
                  not accept `custom_fields`, and neither does its
                  single-record sibling. A draft session is a scheduling
                  placement of an existing session inside a draft agenda, not a
                  record of its own, so it has no custom fields to write. Set
                  custom fields on the underlying session through
                  `/event/{eventId}/sessions`.

    BulkResponse:
      type: object
      description: Response body for bulk operations, including per-item results.
      properties:
        batch_id:
          type: string
          format: uuid
          description: Unique identifier for this bulk batch.
        results:
          type: array
          description: Per-operation results in the same order as the request.
          items:
            type: object
            properties:
              index:
                type: integer
                description: Zero-based index of the operation in the request array.
              action:
                type: string
                description: The action that was performed (create, update, delete).
              status:
                type: string
                enum:
                  - success
                  - created
                  - associated
                  - updated
                  - deleted
                  - error
                description: |
                  Whether this individual operation succeeded or failed.

                  Most bulk endpoints report `success` or `error`. The contacts
                  bulk endpoint instead names the outcome: `created`,
                  `associated` (the email already existed in the organization
                  and was linked to this event), `updated`, `deleted`, or
                  `error`. Treat anything other than `error` as a success.
              id:
                type: string
                format: uuid
                description: The ID of the created or affected entity.
              error:
                description: |
                  Error details, present only when `status` is `error`.

                  An object with `code` and `message` on most bulk endpoints.
                  On the contacts bulk endpoint `error` is a plain string and
                  the code is returned alongside it as `error_code`; that
                  difference is longstanding and is kept so existing callers
                  reading the string keep working.
                oneOf:
                  - type: object
                    properties:
                      code:
                        type: string
                        description: Machine-readable error code.
                      message:
                        type: string
                        description: Human-readable error description.
                      valid_options:
                        type: array
                        items:
                          type: string
                        description: |
                          Present only when `code` is `TYPE_MISMATCH` on a
                          `dropdown` field: every value the field accepts,
                          one array entry per option, in the field's option
                          order. An option label can itself contain a comma
                          (`"Banana, Chocolate"`), so read this array rather
                          than splitting the list in `message`. Absent on
                          every other error.
                    example:
                      code: TYPE_MISMATCH
                      message: >-
                        Field 'product_type' value 'Banana' is not a valid
                        option. Valid options: ["1", "Banana, Chocolate",
                        "Blue, Green, Violet", "10"]
                      valid_options:
                        - '1'
                        - Banana, Chocolate
                        - Blue, Green, Violet
                        - '10'
                  - type: string
                    description: Human-readable error description (contacts bulk).
              error_code:
                type: string
                description: >-
                  Machine-readable error code. Returned by the contacts bulk
                  endpoint alongside the string `error`.
        summary:
          type: object
          description: >-
            Aggregate counts for the batch. This page previously named the
            object `stats`; every bulk endpoint has always returned it as
            `summary`.
          properties:
            total:
              type: integer
              description: Total number of operations in the batch.
            succeeded:
              type: integer
              description: >-
                Number of operations that succeeded — every row whose `status`
                is not `error`, including the contacts bulk endpoint's
                `created`, `associated`, `updated` and `deleted`.
            failed:
              type: integer
              description: Number of operations that failed.

    AgendaDraft:
      type: object
      description: An agenda draft workspace for planning session schedule changes before committing them to the live event.
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: integer
        name:
          type: string
        description:
          type: string
        status:
          type: string
          enum:
            - draft
            - committed
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        admin_url:
          type: string
          format: uri
          nullable: true
          description: Deep link to this entity in the Sessionboard admin UI.

    DraftSession:
      type: object
      description: A session placement within an agenda draft.
      properties:
        id:
          type: string
          format: uuid
        draft_id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        room_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    EventRule:
      type: object
      description: A scheduling constraint or validation rule for an event.
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: integer
        name:
          type: string
        type:
          type: string
        config:
          type: object
          additionalProperties: true
        enabled:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Persona:
      type: object
      description: An evaluation persona representing an attendee type for schedule evaluation.
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: integer
        name:
          type: string
        description:
          type: string
        config:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    TranscriptionResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Transcription'

    TranscriptionListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Transcription'
        pagination:
          $ref: '#/components/schemas/Pagination'

    TranscriptionElement:
      type: object
      properties:
        value:
          type: string
        speaker:
          type: object
          properties:
            id:
              type: string
            label:
              type: string
            name:
              type: string
              nullable: true
        start_time:
          type: number
        end_time:
          type: number
        fragment_ids:
          type: array
          items:
            type: string
            format: uuid

    ContentItem:
      type: object
      description: Typed item inside a session or event content pack.
      properties:
        id:
          oneOf:
            - type: string
              format: uuid
            - type: string
            - type: 'null'
        type:
          type: string
          enum: [transcription, summary, insight, podcast, document, recap]
        session_id:
          type: string
          format: uuid
        text:
          type: string
          description: Full joined transcript (transcription items).
        elements:
          type: array
          items:
            $ref: '#/components/schemas/TranscriptionElement'
        fragment_count:
          type: integer
        interval_sec:
          type: integer
          nullable: true
        language_code:
          type: string
        summary_type:
          type: string
        provider:
          type: string
        content:
          type: object
        topics:
          type: array
          items:
            type: object
        insight_type:
          type: string
        name:
          type: string
        metadata:
          type: object
          nullable: true
        status:
          type: string
        url:
          type: string
          nullable: true
        duration_sec:
          type: integer
          nullable: true
        generated_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        document_type:
          type: string
          enum: [summary_pdf, summary_card, event_report]
        content_type:
          type: string
        scope:
          type: string
        quotes:
          type: array
          items:
            type: object
        summaries:
          type: array
          items:
            type: object
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    SessionContent:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Session id
        event_id:
          type: integer
        title:
          type: string
          nullable: true
        starts_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
        speakers:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              label:
                type: string
              name:
                type: string
                nullable: true
        has_transcription:
          type: boolean
        has_summary:
          type: boolean
        has_podcast:
          type: boolean
        items:
          type: array
          items:
            $ref: '#/components/schemas/ContentItem'

    SessionContentResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/SessionContent'

    SessionContentListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SessionContent'
        pagination:
          type: object
          properties:
            current_page:
              type: integer
            page_size:
              type: integer
            total_pages:
              type: integer
            total_results:
              type: integer

    EventContent:
      type: object
      properties:
        id:
          type: integer
          description: Event id
        updated_at:
          type: string
          format: date-time
        items:
          type: array
          items:
            $ref: '#/components/schemas/ContentItem'

    EventContentResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/EventContent'

    ContentItemResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/ContentItem'

    Transcription:
      type: object
      description: Polymorphic transcription artifact. Fields vary by type.
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [fragment, summary, insight, translation]
        session_id:
          type: string
          format: uuid
        transcription:
          type: string
        speaker:
          type: string
        is_partial:
          type: boolean
        provider:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    TranscriptionCreateRequest:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [fragment, summary, insight, translation]
        transcription:
          type: string

    Recording:
      type: object
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          example: recording
        session_id:
          type: string
          format: uuid
        s3_bucket:
          type: string
        s3_key:
          type: string
          description: After complete, non-PCM uploads are stored as `.pcm` keys.
        encoding:
          type: string
          example: linear16
        sample_rate:
          type: integer
          example: 16000
        channels:
          type: integer
          example: 1
        status:
          type: string
          enum: [recording, completed]
        duration_seconds:
          type: number
        file_size_bytes:
          type: integer
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        mp3_url:
          type: string
          format: uri
          nullable: true
          description: >
            6-hour presigned HTTPS URL for an MP3 sibling of the original PCM
            take. Null while the recording is in progress or if transcode
            has not succeeded yet. Download and store this file — do not hotlink
            it as a permanent CDN URL. Session PDFs and user-uploaded files stay
            on `/content` and `/files`.
        mp3_expires_at:
          type: string
          format: date-time
          nullable: true
          description: When `mp3_url` stops working. Request the recording again to refresh the URL.

    RecordingResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Recording'

    RecordingCreateResponse:
      type: object
      properties:
        data:
          allOf:
            - $ref: '#/components/schemas/Recording'
            - type: object
              properties:
                upload:
                  type: object
                  properties:
                    url:
                      type: string
                    method:
                      type: string
                      example: PUT
                    headers:
                      type: object
                      properties:
                        Content-Type:
                          type: string

    MediaItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          example: media_item
        title:
          type: string
        platform:
          type: string
        kind:
          type: string
        event_id:
          type: integer
        session_id:
          type: string
          format: uuid
        media_s3_key:
          type: string
        transcript_status:
          type: string
          enum: [queued, processing, ready, failed]
        duration_seconds:
          type: number
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    MediaItemResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/MediaItem'

    SessionFileCreateRequest:
      type: object
      required: [filename, size_bytes]
      properties:
        filename:
          type: string
          description: Original filename including extension (e.g. `slides.pptx`, `handout.pdf`).
          example: keynote-slides.pptx
        size_bytes:
          type: integer
          minimum: 1
          maximum: 524288000
          description: Exact file size in bytes. Maximum 500 MB (524288000) for direct-to-storage upload. Use `POST .../files/upload` for files up to 50 MB without specifying size.
        content_type:
          type: string
          description: MIME type. Optional; inferred from the filename extension when omitted.
          example: application/vnd.openxmlformats-officedocument.presentationml.presentation
        mimetype:
          type: string
          description: Alias for `content_type`.
        title:
          type: string
          description: Display title. Defaults to `filename`.
        assigned_participant_id:
          type: string
          format: uuid
          nullable: true
          description: Contact UUID of a session participant to assign this file to.
        content_slot_id:
          type: string
          format: uuid
          nullable: true
          description: |
            The [upload slot](/api-reference/session-files/list-upload-slots)
            this file answers. Optional — API callers act as the organization,
            so no slot deadline applies and a file may be attached without a
            slot. When set, the slot must be active, apply to the session
            (`GET .../content-slots?session_id=`), accept the file's declared
            type and have room under its `max_files`; otherwise `400` names the
            slot. The file's `type` is derived from the slot's `content_type`.
            Ignored on events that still use a single file request.

    SessionFileUpdateRequest:
      type: object
      properties:
        title:
          type: string
          nullable: true
        assigned_participant_id:
          type: string
          format: uuid
          nullable: true

    SessionFileResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Content'

    SessionFileListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Content'

    SessionFileCreateResponse:
      type: object
      properties:
        data:
          allOf:
            - $ref: '#/components/schemas/Content'
            - type: object
              properties:
                upload:
                  type: object
                  properties:
                    url:
                      type: string
                      format: uri
                      description: Presigned S3 URL — PUT the file bytes here.
                    method:
                      type: string
                      example: PUT
                    headers:
                      type: object
                      properties:
                        Content-Type:
                          type: string

    # ────────────────────────────────────────────
    # Registration & Expo
    # ────────────────────────────────────────────
    RegistrationApiError:
      type: object
      description: Error body of the Registration and Expo endpoints.
      required:
        - error
        - message
      properties:
        error:
          type: string
          enum:
            - VALIDATION_ERROR
            - NOT_FOUND
            - FORBIDDEN
            - CONFLICT
            - SCOPE_REQUIRED
            - EVENT_NOT_ALLOWED
            - EVENT_ARCHIVED
            - INTERNAL_ERROR
        message:
          type: string
        details:
          type: object
          description: Present when there is a machine-readable reason.
          properties:
            reason:
              type: string
              description: |
                Why the request was refused, for example `duplicate`, `sold_out`, `too_many`,
                `ticket_unavailable`, `not_on_sale`, `registrant_type_unavailable`,
                `answers_invalid`, `discount_invalid`, `wrong_status`, `account_required` or
                `booth_not_found`.
            status:
              type: string
              description: For `wrong_status` on Check someone in, the registration's current status.
            errors:
              type: object
              additionalProperties: true
              description: More detail for some reasons — the email for `duplicate`, the `ticket_type_id` for `sold_out`, the problems per answer for `answers_invalid`.

    RegistrationForm:
      type: object
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: integer
        name:
          type: string
        slug:
          type: string
        kind:
          type: string
          enum: [attendee, group, presenter, press, exhibitor_staff, custom]
        status:
          type: string
          enum: [draft, open, closed, archived]
        is_open_now:
          type: boolean
          description: '`true` when `status` is `open` and now is between `opens_at` and `closes_at`.'
        opens_at:
          type: [string, 'null']
          description: Local date and time in the form's `timezone`, without an offset (for example `2026-09-01T09:00:00`).
        closes_at:
          type: [string, 'null']
          description: Local date and time in the form's `timezone`, without an offset.
        timezone:
          type: string
          description: The event's IANA time zone; `UTC` when the event has none.
        audience:
          type: string
          enum: [public, invite_code, invite_list, hidden]
        invite_code:
          type: [string, 'null']
          description: The code registrants enter when `audience` is `invite_code`.
        requires_code:
          type: boolean
        ticket_type_ids:
          type: array
          items:
            type: string
            format: uuid
          description: The ticket types sold on this form.
        registrant_type_ids:
          type: array
          items:
            type: string
            format: uuid
          description: The registrant types (for example Member, Non-member) this form offers.
        questions:
          type: array
          items:
            type: object
            additionalProperties: true
          description: The form's questions, in order.
        languages:
          type: array
          items:
            type: string
        translations:
          type: object
          additionalProperties: true
        settings:
          type: object
          additionalProperties: true
        counts:
          type: object
          properties:
            registrations:
              type: integer
              description: Registrations placed through the form, excluding canceled and transferred.
        sort_order:
          type: [integer, 'null']
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    TicketType:
      type: object
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: integer
        type:
          type: string
          enum: [ticket, addon, booth, sponsorship, donation, bundle, session_fee]
        name:
          type: string
        description:
          type: [string, 'null']
        price_cents:
          type: integer
          description: Base price in the smallest currency unit.
        currency:
          type: string
          description: ISO 4217 code.
        capacity:
          type: [integer, 'null']
          description: '`null` means unlimited.'
        sold:
          type: integer
          description: Seats on placed orders.
        available:
          type: [integer, 'null']
          description: '`capacity - sold`, never below `0`; `null` when `capacity` is `null`.'
        min_per_order:
          type: [integer, 'null']
        max_per_order:
          type: [integer, 'null']
        sales_start_at:
          type: [string, 'null']
          format: date-time
        sales_end_at:
          type: [string, 'null']
          format: date-time
        visibility:
          type: string
          enum: [public, hidden, code]
        status:
          type: string
          enum: [active, archived]
        sort_order:
          type: [integer, 'null']
        settings:
          type: object
          additionalProperties: true
          description: |
            Stored snake_case: `form_ids`, `session_id`, `audience`, `category`, `deadline`,
            `late_price_cents`, `taxable`, `gl_code`, `bundle_items`, `booth`, `sponsorship`,
            `pricing`. `{}` when none are set.

    TicketTypeWrite:
      type: object
      description: |
        Body of Create a ticket type (where `name` is required) and Update a ticket type (where
        every field is optional). Unknown keys are ignored.
      properties:
        name:
          type: string
          maxLength: 255
          description: Required on create. Longer names are cut to 255 characters.
        type:
          type: string
          enum: [ticket, addon, booth, sponsorship, donation, bundle, session_fee]
        description:
          type: [string, 'null']
        price_cents:
          type: [integer, 'null']
          minimum: 0
        currency:
          type: string
          description: ISO 4217 code; upper-cased and cut to 3 characters.
        capacity:
          type: [integer, 'null']
          minimum: 0
          description: '`null` for unlimited.'
        min_per_order:
          type: [integer, 'null']
          minimum: 0
        max_per_order:
          type: [integer, 'null']
          minimum: 0
        sort_order:
          type: [integer, 'null']
          minimum: 0
        visibility:
          type: string
          enum: [public, hidden, code]
        status:
          type: string
          enum: [active, archived]
        sales_start_at:
          type: [string, 'null']
          format: date-time
        sales_end_at:
          type: [string, 'null']
          format: date-time
        settings:
          type: object
          description: |
            Ticket type settings. Keys may be snake_case or camelCase; unknown keys are dropped.
            On update, each top-level key you send replaces the stored one.
          properties:
            form_ids:
              type: array
              items:
                type: string
                format: uuid
            session_id:
              type: [string, 'null']
              description: For a `session_fee`, the session it pays for.
            audience:
              type: string
              enum: [attendee, exhibitor, sponsor]
            category:
              type: [string, 'null']
            deadline:
              type: [string, 'null']
            late_price_cents:
              type: [integer, 'null']
            taxable:
              type: boolean
            gl_code:
              type: [string, 'null']
            bundle_items:
              type: array
              items:
                type: object
                properties:
                  ticket_type_id:
                    type: string
                    format: uuid
                  quantity:
                    type: integer
                    minimum: 1
            booth:
              type: object
              properties:
                booth_type:
                  type: string
                size:
                  type: string
                floor_map_id:
                  type: string
                  format: uuid
                booth_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                staff_badges:
                  type: integer
                lead_licenses:
                  type: integer
            sponsorship:
              type: object
              properties:
                tier_id:
                  type: string
                staff_badges:
                  type: integer
                lead_licenses:
                  type: integer
                deliverables:
                  type: array
                  items:
                    type: object
                    properties:
                      key:
                        type: string
                      title:
                        type: string
                      due_days_before:
                        type: [integer, 'null']
            pricing:
              type: object
              properties:
                date_tier_mode:
                  type: [string, 'null']
                  enum: [cutoffs, windows, null]
                date_tiers:
                  type: array
                  description: Each tier needs `ends_at` and `price` (cents).
                  items:
                    type: object
                    required: [ends_at, price]
                    properties:
                      id:
                        type: string
                      label:
                        type: [string, 'null']
                      price:
                        type: integer
                      starts_at:
                        type: string
                      ends_at:
                        type: string
                registrant_prices:
                  type: array
                  items:
                    type: object
                    properties:
                      registrant_type_id:
                        type: string
                        format: uuid
                      price_cents:
                        type: integer
                      date_tiers:
                        type: array
                        items:
                          type: object
                          additionalProperties: true
                rules:
                  type: array
                  items:
                    type: object
                    properties:
                      field_id:
                        type: string
                      operator:
                        type: string
                      value: {}
                      fee_action:
                        type: string
                      amount_cents:
                        type: integer

    Registration:
      type: object
      description: One person's registration for the event.
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: integer
        status:
          type: string
          enum: [incomplete, pending_approval, waitlisted, confirmed, checked_in, canceled, transferred, declined, no_show]
        first_name:
          type: [string, 'null']
        last_name:
          type: [string, 'null']
        email:
          type: string
        company:
          type: [string, 'null']
        job_title:
          type: [string, 'null']
        ticket_type:
          type: [object, 'null']
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
        registrant_type:
          type: [object, 'null']
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
        form:
          type: [object, 'null']
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            kind:
              type: string
        order_id:
          type: [string, 'null']
          format: uuid
        account_id:
          type: [string, 'null']
          format: uuid
          description: The exhibitor or sponsor account the person registered under (booth staff).
        contact_id:
          type: [string, 'null']
          description: The person's contact in Sessionboard.
        badge_id:
          type: [string, 'null']
        answers:
          type: object
          additionalProperties: true
          description: Answers to the form's questions beyond name, email, company and job title, keyed by the question's field `internal_name`.
        session_ids:
          type: array
          items:
            type: string
          description: Sessions the person reserved or paid for.
        membership:
          type: [object, 'null']
          additionalProperties: true
          description: The membership check made at registration, when the event verifies membership.
        awaiting_assignment:
          type: boolean
          description: '`true` for a seat bought by a group purchaser and not yet assigned to a person.'
        checked_in_at:
          type: [string, 'null']
          format: date-time
        badge:
          type: object
          properties:
            printed:
              type: boolean
            print_count:
              type: integer
            last_printed_at:
              type: [string, 'null']
              format: date-time
        approval:
          type: [object, 'null']
          description: The approval request for this registration, when the event's approval workflow opened one.
          properties:
            id:
              type: string
            status:
              type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          description: When the registration last changed. `updated_since` on List registrations filters on it.

    RegistrationCreateRequest:
      type: object
      required: [form_id, payment, attendees]
      properties:
        form_id:
          type: string
          format: uuid
          description: The registration form to place the order through. Its ticket types, registrant types and questions apply.
        payment:
          type: string
          enum: [comp, invoice, paid_externally, cash, check]
          description: How the order is paid. See the table above.
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 128
          description: Your unique key for this order. A repeat with the same key returns the original order with `200`.
        external_reference:
          type: string
          maxLength: 200
          description: Your reference for the order (for example your order number). Stored as the order's note.
        discount_code:
          type: string
          description: A promo code configured on the event.
        purchaser:
          type: object
          description: Who placed the order. Defaults to the first attendee.
          properties:
            email:
              type: string
              format: email
            first_name:
              type: string
            last_name:
              type: string
            company:
              type: string
        attendees:
          type: array
          minItems: 1
          maxItems: 100
          description: One entry per person, at most 100 per order.
          items:
            $ref: '#/components/schemas/RegistrationAttendeeInput'

    RegistrationAttendeeInput:
      type: object
      required: [email, ticket_type_id]
      properties:
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        company:
          type: string
        job_title:
          type: string
        ticket_type_id:
          type: string
          format: uuid
          description: Must be sold on the form.
        registrant_type_id:
          type: [string, 'null']
          format: uuid
          description: Must be offered on the form. Decides the price when the ticket type prices by registrant type.
        addon_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Add-on ticket types for this person.
        session_ids:
          type: array
          items:
            type: string
          description: Sessions to reserve or pay for (session fees).
        answers:
          type: object
          additionalProperties: true
          description: Answers to the form's questions, keyed by the question's field `internal_name` or field ID.
        consent:
          type: boolean
          default: true

    RegistrationCreateResult:
      type: object
      properties:
        order:
          $ref: '#/components/schemas/Order'
        registrations:
          type: array
          items:
            $ref: '#/components/schemas/Registration'

    RegistrationUpdateRequest:
      type: object
      properties:
        first_name:
          type: string
        last_name:
          type: string
        company:
          type: [string, 'null']
        job_title:
          type: [string, 'null']
        action:
          type: string
          enum: [approve, decline, cancel]
        note:
          type: string
          description: Recorded with an `approve` or `decline` decision.

    Order:
      type: object
      description: An order and what it bought. Amounts are in the smallest currency unit.
      properties:
        id:
          type: string
          format: uuid
        number:
          type: [string, 'null']
          description: The order number shown to the purchaser and organizers.
        event_id:
          type: integer
        status:
          type: string
          enum: [draft, pending_payment, awaiting_invoice, paid, partially_refunded, refunded, canceled, expired]
        purchaser:
          type: object
          properties:
            type:
              type: [string, 'null']
              enum: [contact, account, attendee, guest, null]
            id:
              type: [string, 'null']
            email:
              type: [string, 'null']
            name:
              type: [string, 'null']
            company:
              type: [string, 'null']
        currency:
          type: string
        subtotal_cents:
          type: integer
        discount_cents:
          type: integer
        tax_cents:
          type: integer
        total_cents:
          type: integer
        paid_cents:
          type: integer
        balance_cents:
          type: integer
          description: '`total_cents - paid_cents`, never below `0`.'
        payment_method:
          type: [string, 'null']
          description: '`card` for orders paid online; `comp`, `invoice`, `external`, `cash` or `check` for orders placed by organizers or through this API.'
        discount:
          type: [object, 'null']
          properties:
            id:
              type: string
            code:
              type: string
        lines:
          type: array
          items:
            $ref: '#/components/schemas/OrderLine'
        registrations:
          type: array
          description: The registrations the order bought. Empty in the `order` returned by Create a registration — read `registrations` beside it.
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              status:
                type: string
              first_name:
                type: [string, 'null']
              last_name:
                type: [string, 'null']
              email:
                type: string
              awaiting_assignment:
                type: boolean
        invoice:
          type: [object, 'null']
          additionalProperties: true
          description: The invoice, for orders paid by invoice.
        form_id:
          type: [string, 'null']
          format: uuid
        account_id:
          type: [string, 'null']
          format: uuid
        payment_mode:
          type: [string, 'null']
        created_at:
          type: string
          format: date-time

    RegistrationSummary:
      type: object
      description: The event's registration numbers (the Registration Overview page). Amounts in cents.
      properties:
        totals:
          type: object
          properties:
            registrations: { type: integer, description: 'Live registrations: incomplete, pending approval, waitlisted, confirmed, checked in.' }
            confirmed: { type: integer }
            pending_approval: { type: integer }
            waitlisted: { type: integer }
            checked_in: { type: integer }
            canceled: { type: integer }
        revenue:
          type: object
          properties:
            currency: { type: string, description: The event's registration currency. }
            gross_cents: { type: integer, description: Succeeded charges. }
            refunded_cents: { type: integer }
            outstanding_invoices_cents: { type: integer }
        by_day:
          type: array
          description: Registrations per day in the event time zone (canceled and transferred excluded).
          items:
            type: object
            properties:
              date: { type: string, format: date }
              registrations: { type: integer }
        by_ticket_type:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              name: { type: string }
              type: { type: string }
              sold: { type: integer }
              capacity: { type: [integer, 'null'] }
              revenue_cents: { type: integer }
        by_registrant_type:
          type: array
          items:
            type: object
            properties:
              id: { type: [string, 'null'], format: uuid }
              name: { type: string }
              count: { type: integer }
        by_form:
          type: array
          items:
            type: object
            properties:
              id: { type: [string, 'null'], format: uuid }
              name: { type: string }
              count: { type: integer }
        membership:
          type: object
          properties:
            member: { type: integer }
            lapsed: { type: integer }
            non_member: { type: integer }
            joined: { type: integer, description: People who joined through the form. }
        signals:
          type: object
          additionalProperties: { type: integer }
        orders:
          type: object
          properties:
            by_status:
              type: array
              items:
                type: object
                properties:
                  status: { type: string }
                  count: { type: integer }
                  total_cents: { type: integer }
                  paid_cents: { type: integer }
        outstanding_invoices:
          type: object
          properties:
            count: { type: integer, description: Open or partly paid invoices. }
            overdue: { type: integer, description: Of those, past their due date. }
            balance_cents: { type: integer }
        capacity_pools:
          type: array
          items:
            $ref: '#/components/schemas/CapacityPool'
    Payment:
      type: object
      description: |
        A payment record of a registration order. Never carries a card number, a payment or
        gateway token, or a gateway credential.
      properties:
        id: { type: string, format: uuid }
        order_id: { type: string, format: uuid }
        order_number: { type: [string, 'null'] }
        type:
          type: string
          enum: [charge, refund, authorize, capture, void]
        status:
          type: string
          enum: [pending, processing, succeeded, failed, refunded, canceled]
          description: '`pending` with no verdict from the gateway yet (a bank transfer settles days later).'
        amount_cents: { type: integer }
        currency: { type: string }
        method:
          type: string
          description: How it was paid as recorded (`card`, `check`, `cash`, `external`, `comp`, `invoice`, …).
        method_type:
          type: string
          enum: [card, bank_account, offline, comp]
        processor:
          type: [string, 'null']
          enum: [spreedly, null]
          description: '`spreedly` for money moved through your payment gateway; `null` for money recorded offline.'
        gateway_type:
          type: [string, 'null']
          description: The kind of payment gateway (for example `stripe`, `authorize_net`, `test`).
        test_mode: { type: [boolean, 'null'], description: Whether the gateway is in test mode. }
        card_brand: { type: [string, 'null'] }
        last_four: { type: [string, 'null'], description: Last four digits of the card or bank account, when stored. }
        installment: { type: [integer, 'null'], description: Which installment of an invoice plan this pays (0-based). }
        parent_payment_id: { type: [string, 'null'], format: uuid, description: For a refund, the charge it refunds. }
        reference: { type: [string, 'null'], description: Check number or wire reference typed by an admin. }
        failure:
          type: [object, 'null']
          description: Why a failed attempt failed.
          properties:
            code: { type: [string, 'null'] }
            message: { type: [string, 'null'] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    RegistrationMembership:
      type: object
      properties:
        registration_id: { type: string, format: uuid }
        status: { type: string }
        email: { type: string }
        first_name: { type: [string, 'null'] }
        last_name: { type: [string, 'null'] }
        registrant_type_id: { type: [string, 'null'], format: uuid }
        membership:
          type: string
          enum: [member, lapsed, none]
          description: What the membership check answered when the person registered.
        member_id: { type: [string, 'null'] }
        created_at: { type: string, format: date-time }
    MembershipLookup:
      type: object
      properties:
        email: { type: string }
        membership_enabled: { type: boolean }
        result:
          type: string
          enum: [member, lapsed, none]
        applied:
          type: string
          enum: [member, lapsed, non_member]
          description: The pricing that applies.
        registrant_type_id: { type: [string, 'null'], format: uuid, description: The attendee type the form assigns. }
        status_value: { type: [string, 'null'], description: The member-status value that decided it. }
        member_id: { type: [string, 'null'] }
        registrations:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              status: { type: string }
              membership: { type: string, enum: [member, lapsed, none] }
              created_at: { type: string, format: date-time }
    CapacityPool:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        capacity: { type: integer }
        sold: { type: integer }
        left: { type: integer }
        percent: { type: integer, description: Percent full (0–100). }
        tickets:
          type: array
          items:
            type: object
            properties:
              ticket_type_id: { type: string, format: uuid }
              name: { type: [string, 'null'] }
              status: { type: string, description: '`missing` when the ticket type was deleted.' }
              own_capacity: { type: [integer, 'null'] }
              sold: { type: integer }
    FeeGroup:
      type: object
      properties:
        name: { type: string }
        ticket_types:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              name: { type: string }
              type: { type: string }
              status: { type: string }
        promo_codes:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              code: { type: string }
              kind: { type: string, enum: [percent, fixed, comp] }
              value: { type: integer, description: Percent points, or cents for `fixed`. }
              status: { type: string }
    DonationFundTotals:
      type: object
      properties:
        gifts: { type: integer }
        raised_cents: { type: integer, description: On paid orders. }
        pledged_cents: { type: integer, description: On orders still to pay. }
    DonationFee:
      type: object
      properties:
        ticket_type_id: { type: string, format: uuid }
        name: { type: string }
        status: { type: string }
        currency: { type: string }
        open_amount: { type: boolean, description: The attendee chooses the amount. }
        min_cents: { type: integer }
        max_cents: { type: integer }
        suggested_cents: { type: array, items: { type: integer } }
        funds:
          type: array
          items:
            allOf:
              - type: object
                properties:
                  id: { type: string }
                  name: { type: string }
                  gl_code: { type: [string, 'null'] }
              - $ref: '#/components/schemas/DonationFundTotals'
        unassigned:
          $ref: '#/components/schemas/DonationFundTotals'
    WorkshopRules:
      type: object
      properties:
        include_free_sessions: { type: boolean, description: Offer sessions that have no fee. }
        general_session_ids: { type: array, items: { type: string, format: uuid }, description: Sessions everyone attends; shown, not picked. }
        overlap_mode: { type: string, enum: ['off', warn, block] }
        slots:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              label: { type: string }
              session_ids: { type: array, items: { type: string, format: uuid } }
              min_picks: { type: [integer, 'null'] }
              max_picks: { type: [integer, 'null'] }
        session_rules:
          type: array
          items:
            type: object
            properties:
              session_id: { type: string, format: uuid }
              registrant_type_ids: { type: array, items: { type: string, format: uuid } }
              ticket_type_ids: { type: array, items: { type: string, format: uuid } }
              hidden: { type: boolean }
        self_service:
          type: object
          properties:
            enabled: { type: boolean, description: Attendees may change paid picks from their manage link. }
            until: { type: [string, 'null'], format: date-time }
    BadgePrintQueue:
      type: object
      properties:
        stations:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              stock: { type: [string, 'null'] }
              template_id: { type: [string, 'null'], format: uuid }
              offset_x_in: { type: [number, 'null'] }
              offset_y_in: { type: [number, 'null'] }
              queued: { type: integer }
              printing: { type: integer }
              failed: { type: integer }
        jobs:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              station_id: { type: string }
              status: { type: string, enum: [queued, printing, printed, failed, canceled] }
              attendee_id: { type: string, format: uuid, description: The registration printed. }
              first_name: { type: [string, 'null'] }
              last_name: { type: [string, 'null'] }
              company: { type: [string, 'null'] }
              attempts: { type: integer }
              error: { type: [string, 'null'] }
              created_at: { type: string, format: date-time }
              claimed_at: { type: [string, 'null'], format: date-time }
              printed_at: { type: [string, 'null'], format: date-time }
    OrderLine:
      type: object
      properties:
        key:
          type: string
        line_item_id:
          type: string
          format: uuid
        ticket_type_id:
          type: [string, 'null']
          format: uuid
        type:
          type: [string, 'null']
          description: The ticket type's `type` (`ticket`, `addon`, …).
        label:
          type: [string, 'null']
        tier_label:
          type: [string, 'null']
          description: The price tier applied (for example Early bird).
        quantity:
          type: integer
        unit_price_cents:
          type: integer
        discount_cents:
          type: integer
        tax_cents:
          type: integer
        total_cents:
          type: integer
        attendee_index:
          type: [integer, 'null']
          description: Which entry of the order's attendees this line is for.
        session_id:
          type: [string, 'null']
        gl_code:
          type: [string, 'null']

    Attendee:
      type: object
      description: Short registration shape used by List attendees, List check-ins and Check someone in.
      properties:
        id:
          type: string
          format: uuid
          description: The registration ID.
        status:
          type: string
        first_name:
          type: [string, 'null']
        last_name:
          type: [string, 'null']
        email:
          type: string
        company:
          type: [string, 'null']
        job_title:
          type: [string, 'null']
        ticket_type_id:
          type: [string, 'null']
          format: uuid
        registrant_type_id:
          type: [string, 'null']
          format: uuid
        account_id:
          type: [string, 'null']
          format: uuid
        checked_in_at:
          type: [string, 'null']
          format: date-time
        updated_at:
          type: string
          format: date-time

    CheckIn:
      allOf:
        - $ref: '#/components/schemas/Attendee'
        - type: object
          properties:
            already_checked_in:
              type: boolean
              description: '`true` when the person was already checked in and nothing changed.'

    CheckInRequest:
      type: object
      description: Send one of `registration_id`, `email` or `badge_code`.
      properties:
        registration_id:
          type: string
          format: uuid
          description: Takes precedence over `email` and `badge_code`. A value that is not a UUID is ignored.
        email:
          type: string
          description: Used when there is no `registration_id`. Case-insensitive.
        badge_code:
          type: string
          description: The scanned code — `sb-checkin:<registration id>` from the confirmation email, or a printed or in-app badge code. Used when neither of the others is sent.
        checked_in_at:
          type: string
          format: date-time
          description: When the person arrived, for scans uploaded later. Defaults to now; ignored if it is not a date.

    Booth:
      type: object
      properties:
        id:
          type: string
          format: uuid
        floor_map_id:
          type: string
          format: uuid
        hall_id:
          type: string
          format: uuid
        number:
          type: string
        status:
          type: string
          enum: [available, hold, reserved, sold, blocked]
          description: The status now — an expired `hold` reads `available`.
        stored_status:
          type: string
          enum: [available, hold, reserved, sold, blocked]
          description: The status as last set.
        booth_type:
          type: string
          description: For example `linear`.
        list_price_cents:
          type: [integer, 'null']
          description: The booth's own price, when set; overrides its package price.
        price_cents:
          type: [integer, 'null']
          description: '`list_price_cents`, or else the booth package price.'
        currency:
          type: string
        hold_until:
          type: [string, 'null']
          format: date-time
        geom:
          type: object
          additionalProperties: true
          description: The booth's shape and position on the floor map.
        fill_color:
          type: [string, 'null']
        product:
          type: [object, 'null']
          description: The booth package (a ticket type of type `booth`) that sells this booth.
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            price_cents:
              type: integer
        account:
          type: [object, 'null']
          description: The exhibitor or sponsor holding the booth.
          properties:
            id:
              type: string
              format: uuid
            name:
              type: [string, 'null']
        application_id:
          type: [string, 'null']
          description: The exhibitor booth application this booth was assigned through, if any.
        order_id:
          type: [string, 'null']
          description: The order of that application, if any.

    BoothUpdateRequest:
      type: object
      properties:
        status:
          type: string
          enum: [available, hold, reserved, sold, blocked]
        account_id:
          type: [string, 'null']
          format: uuid
          description: An exhibitor or sponsor of this event; `null` removes the account.
        hold_until:
          type: [string, 'null']
          format: date-time
          description: Until when a `hold` lasts; `null` clears it.
        list_price_cents:
          type: [integer, 'null']
          minimum: 0
          description: The booth's own price in cents; `null` falls back to its package price.
        product_id:
          type: [string, 'null']
          format: uuid
          description: The booth package (a ticket type of type `booth`) to sell this booth in; `null` removes it from every package.

  responses:
    RegistrationUnauthorized:
      description: Missing, invalid or expired token, or the event does not belong to your organization.
    RegistrationForbidden:
      description: |
        - `SCOPE_REQUIRED` — the token lacks the scope (`read:registrations` for reads, `write:registrations` for writes). A write scope does not grant reads, and legacy tokens with no scopes cannot write.
        - `EVENT_NOT_ALLOWED` — the event is outside the token's event allow-list.
        - `EVENT_ARCHIVED` — the event is archived.
        - `FORBIDDEN` — Registration is not turned on for this event.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RegistrationApiError'
          examples:
            feature_off:
              summary: Registration is off for the event
              value:
                error: FORBIDDEN
                message: Registration is not enabled for this event
            scope_required:
              summary: Token lacks the write scope
              value:
                error: SCOPE_REQUIRED
                message: "This operation requires the 'write:registrations' scope."
    ExpoForbidden:
      description: |
        - `SCOPE_REQUIRED` — the token lacks the scope (`read:expo` for reads, `write:expo` for writes). A write scope does not grant reads, and legacy tokens with no scopes cannot write.
        - `EVENT_NOT_ALLOWED` — the event is outside the token's event allow-list.
        - `EVENT_ARCHIVED` — the event is archived.
        - `FORBIDDEN` — Expo is not turned on for this event.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RegistrationApiError'
          example:
            error: FORBIDDEN
            message: Expo is not enabled for this event
    RegistrationTooManyRequests:
      description: |
        The token's request limit (100 requests per 15 minutes per bucket — see
        [Rate Limiting](/rate-limiting)). Wait `Retry-After` seconds.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
