Wispr AI Meetings API

The Meetings API from Wispr AI — 7 operation(s) for meetings.

OpenAPI Specification

wispr-ai-meetings-api-openapi.yml Raw ↑
openapi: 3.0.3
info:
  title: Wispr Backend Analytics Meetings API
  description: Wispr Backend API
  version: 0.5.2
tags:
- name: Meetings
paths:
  /api/v1/meetings/sync:
    post:
      tags:
      - Meetings
      summary: Sync Meetings
      description: "Reconcile the client's meetings against the server and return the delta.\n\nGated on cloud-sync via the ``require_cloud_sync`` dependency. Acks the\nclient's pushed meetings, pulls the server-side changes the client is\nmissing, and echoes the sync watermark the client should resume from.\n\nArgs:\n    sync_request:     The client's pushed meetings, watermark, and cursor.\n    current_user:     The authenticated caller.\n    meetings_service: Service that performs the reconciliation.\n    storage:          Backing store for meeting transcripts.\n\nReturns:\n    The acked meetings, the pulled delta, the next watermark, and the\n    pagination cursor.\n\nRaises:\n    HTTPException:\n        401: the authenticated token carries no user id.\n        422: a pushed field exceeds its size cap.\n        500: reconciliation fails unexpectedly (the original error is\n            logged with a truncated message and full traceback, then\n            wrapped)."
      operationId: sync_meetings
      security:
      - ApiKeyHeaderPatched: []
      parameters:
      - name: content-length
        in: header
        required: false
        schema:
          anyOf:
          - type: integer
          - type: 'null'
          title: Content-Length
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MeetingSyncRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeetingSyncResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/meetings/{meeting_id}:
    delete:
      tags:
      - Meetings
      summary: Delete Meeting
      description: 'Soft-delete a meeting and reap its transcript blobs from S3.


        Flips ``is_deleted=true``, then best-effort-deletes everything under

        ``users/{user_id}/meetings/transcripts/{meeting_id}/`` plus the legacy

        flat key. Bumps the sync timestamp so other devices observe the

        deletion on their next pull.


        Returns 204 on both first-delete and idempotent re-delete. Returns

        404 for unknown meetings and meetings owned by another user (we do

        not leak existence to non-owners).'
      operationId: delete_meeting
      security:
      - ApiKeyHeaderPatched: []
      parameters:
      - name: meeting_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Meeting Id
      responses:
        '204':
          description: Successful Response
        '404':
          description: Meeting not found or owned by another user
        '500':
          description: S3 transcript cleanup failed; the meeting row may already be marked deleted. Retry is safe — the cleanup leg is idempotent.
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/meetings/{meeting_id}/upload-url:
    post:
      tags:
      - Meetings
      summary: Create Meeting Upload Url
      description: 'Mint a presigned S3 POST for a meeting''s refine audio + create the job.


        The ``uploaded`` pipeline: the desktop uploads the encoded audio directly

        to S3 with the returned presigned POST, then deletes its local copy. The

        reconciliation poller picks the object up (by ``audio_s3_key`` on the

        job), submits it to Soniox, and drives completion. Re-minting for a

        meeting still awaiting its first upload refreshes the same job row (the

        desktop''s pre-upload retry); it won''t disturb an in-flight or completed

        job.


        Errors map as:


        * 404 — meeting doesn''t exist for this user (or is deleted).

        * 413 — ``size_bytes`` exceeds :data:`REFINE_MAX_AUDIO_BYTES`.

        * 415 — unsupported audio content type (only ``audio/ogg`` / ``audio/wav``).'
      operationId: create_meeting_upload_url
      security:
      - ApiKeyHeaderPatched: []
      parameters:
      - name: meeting_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Meeting Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MeetingUploadUrlRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeetingUploadUrlResponse'
        '429':
          description: Too Many Requests
        '503':
          description: Refine start temporarily unavailable — retry
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/meetings/{meeting_id}/status:
    get:
      tags:
      - Meetings
      summary: Get Meeting Status
      description: 'Report a meeting''s refine progress and inline what the client is missing.


        The desktop polls this as a recovery channel parallel to sync, passing the

        refine status it currently believes (``transcribing`` or ``summarizing``).

        The server returns its own job status and inlines the refined transcript

        and/or content only when it is ahead of the client. A 404 means the meeting

        doesn''t exist on the server, so the client re-pushes it via sync.'
      operationId: get_meeting_status
      security:
      - ApiKeyHeaderPatched: []
      parameters:
      - name: meeting_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Meeting Id
      - name: client_status
        in: query
        required: true
        schema:
          $ref: '#/components/schemas/RefineJobStatus'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeetingStatusResponse'
        '404':
          description: Meeting not found for this user
        '429':
          description: Too Many Requests
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/meetings/{meeting_id}/refined-transcript:
    get:
      tags:
      - Meetings
      summary: Get Refined Transcript Download Url
      description: "Mint a presigned S3 GET for a meeting's refined-transcript NDJSON.\n\nThe desktop calls this when a pull row's ``refined_uploaded_at`` stamp\nbeats its locally persisted fetched-through stamp, then downloads\n``refined.ndjson`` straight from S3 — the bytes never transit the API.\nPresign-only: no ``meeting_refine_jobs`` side effects (same contract as\nthe live-transcript / speaker-observations presigns).\n\nArgs:\n    meeting_id: Meeting whose refined transcript to fetch.\n    user:       Authenticated caller; the row lookup is scoped to their id.\n    db:         Request-scoped executor (replica read).\n\nReturns:\n    The presigned GET URL and its absolute expiry.\n\nRaises:\n    HTTPException:\n        403: Cloud Sync is disabled for the user.\n        404: Meeting missing/foreign/deleted, transcript erased, or no\n            ``refined_uploaded_at`` stamp yet."
      operationId: get_refined_transcript_download_url
      security:
      - ApiKeyHeaderPatched: []
      parameters:
      - name: meeting_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Meeting Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeetingRefinedTranscriptUrlResponse'
        '403':
          description: Cloud Sync must be enabled
        '404':
          description: Meeting not found, deleted, transcript erased, or no refined transcript yet
        '429':
          description: Too Many Requests
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/meetings/{meeting_id}/speaker-observations/upload-url:
    post:
      tags:
      - Meetings
      summary: Create Speaker Observations Upload Url
      description: 'Mint a presigned S3 POST for a meeting''s speaker-observation log.


        Sibling to :func:`create_meeting_upload_url` but with no job side

        effects: it never touches ``meeting_refine_jobs`` (the audio route''s

        upsert revives failed/abandoned jobs and clobbers the audio key — a

        stuck observation artifact must not delay or disturb the transcript).

        The key is server-composed via :func:`speaker_observations_key`, never

        derived from client input. The client PUTs the artifact directly to S3;

        Phase-1 transcript persistence reads it from S3 on demand (no confirm leg).


        Errors map as:


        * 404 — meeting doesn''t exist for this user (or is deleted).

        * 413 — ``size_bytes`` exceeds :data:`SPEAKER_OBS_MAX_BYTES`.

        * 415 — content type is not ``application/x-ndjson``.'
      operationId: create_speaker_observations_upload_url
      security:
      - ApiKeyHeaderPatched: []
      parameters:
      - name: meeting_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Meeting Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpeakerObservationsUploadUrlRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeetingUploadUrlResponse'
        '429':
          description: Too Many Requests
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/meetings/{meeting_id}/live-transcript/upload-url:
    post:
      tags:
      - Meetings
      summary: Create Live Transcript Upload Url
      description: 'Mint a presigned S3 POST for a meeting''s live-transcript NDJSON.


        Sibling to :func:`create_speaker_observations_upload_url`: the desktop

        PUTs ``live.ndjson`` directly to S3 instead of inlining it in the sync

        body. Same no-job-side-effects contract — it never touches

        ``meeting_refine_jobs``. The key is server-composed via

        :func:`live_ndjson_key`, never derived from client input.


        Errors map as:


        * 404 — meeting doesn''t exist for this user (or is deleted).

        * 413 — ``size_bytes`` exceeds :data:`LIVE_TRANSCRIPT_MAX_BYTES`.

        * 415 — content type is not ``application/x-ndjson``.'
      operationId: create_live_transcript_upload_url
      security:
      - ApiKeyHeaderPatched: []
      parameters:
      - name: meeting_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Meeting Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LiveTranscriptUploadUrlRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeetingUploadUrlResponse'
        '429':
          description: Too Many Requests
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
components:
  schemas:
    SpeakerPerson:
      properties:
        name:
          type: string
          title: Name
        origin:
          type: string
          enum:
          - platform
          - calendar
          - user
          - merged_cal_platform
          - self
          - llm
          title: Origin
        platform:
          anyOf:
          - type: string
            maxLength: 120
          - type: 'null'
          title: Platform
        participantId:
          anyOf:
          - type: string
            maxLength: 64
          - type: 'null'
          title: Participantid
      additionalProperties: false
      type: object
      required:
      - name
      - origin
      title: SpeakerPerson
      description: 'One person in :attr:`SpeakerMap.people`, keyed by a plain uuid4 string.


        ``platform`` is immutable after mint; ``origin`` changes only via the

        calendar-merge upgrade (``platform`` → ``merged_cal_platform``); a rename

        (any origin, including a machine-minted person) changes only ``name``.

        ``origin="self"`` represents the meeting owner ("you").

        ``participantId`` is the server-derived stable calendar-attendee key —

        deliberately NOT an email (nothing PII-bearing rides the map) and NOT the

        ``calendar_participant`` row id (those rows are replaced on every calendar

        re-sync); present only on ``calendar`` / ``merged_cal_platform`` people.'
    MeetingStatusResponse:
      properties:
        refine_status:
          anyOf:
          - $ref: '#/components/schemas/RefineJobStatus'
          - type: 'null'
        refined_transcript:
          anyOf:
          - type: string
          - type: 'null'
          title: Refined Transcript
        speaker_map:
          anyOf:
          - $ref: '#/components/schemas/SpeakerMap'
          - type: 'null'
        content:
          anyOf:
          - type: string
          - type: 'null'
          title: Content
        summary:
          anyOf:
          - type: string
          - type: 'null'
          title: Summary
        notes:
          anyOf:
          - type: string
          - type: 'null'
          title: Notes
      type: object
      required:
      - refine_status
      title: MeetingStatusResponse
      description: 'Refine-progress snapshot the desktop pulls as a backstop to sync.


        The client polls ``GET /meetings/{id}/status`` with the refine

        status it currently believes, and the server inlines whatever the

        client is missing. ``refine_status`` is the server''s job status, or

        ``None`` when no refine job row exists for the meeting (the client

        then has nothing to wait for). ``RefineJobStatus`` is a ``StrEnum``,

        so it serializes to its string value on the wire.


        ``refined_transcript`` / ``content`` are inlined only when the

        server is ahead of the client (see ``MeetingsService.get_meeting_status``);

        both stay ``None`` otherwise, on a deleted meeting, and whenever the

        refined blob is absent, undecodable, or empty.'
    MeetingUploadUrlRequest:
      properties:
        content_type:
          type: string
          title: Content Type
          description: Audio MIME type; one of audio/ogg, audio/wav
        size_bytes:
          type: integer
          exclusiveMinimum: 0.0
          title: Size Bytes
          description: Encoded audio size in bytes
        context:
          anyOf:
          - type: object
          - type: 'null'
          title: Context
          description: Client meeting context (dictionary, participants, meeting metadata) reused server-side to enrich the refined transcript + summary. The same envelope the client sends on the live transcription WS.
        include_google_contact_names:
          type: boolean
          title: Include Google Contact Names
          description: 'Feature payload option: add per-user Google Contacts display names as weak refine terms.'
          default: false
        include_workspace_directory_names:
          type: boolean
          title: Include Workspace Directory Names
          description: 'Feature payload option: add per-user workspace directory display names as weak refine terms.'
          default: false
      type: object
      required:
      - content_type
      - size_bytes
      title: MeetingUploadUrlRequest
      description: Request for a presigned S3 slot to upload a meeting's refine audio.
    RefineJobStatus:
      type: string
      enum:
      - awaiting_audio
      - transcribing
      - summarizing
      - complete
      - failed
      - abandoned
      title: RefineJobStatus
    MeetingSyncRequest:
      properties:
        last_sync_time:
          anyOf:
          - type: string
          - type: 'null'
          title: Last Sync Time
          description: Unix timestamp in ms of the most recent server change the client has seen
        hard_refresh:
          type: boolean
          title: Hard Refresh
          description: When true, ignore last_sync_time and return up to 1000 meetings from server regardless of modified_at
          default: false
        cursor:
          anyOf:
          - type: string
          - type: 'null'
          title: Cursor
          description: Opaque pagination cursor from the previous response's next_cursor. Refetch with the same last_sync_time and this cursor until next_cursor is null.
        meetings:
          items:
            $ref: '#/components/schemas/SyncMeeting-Input'
          type: array
          title: Meetings
      type: object
      required:
      - meetings
      title: MeetingSyncRequest
      description: Request model for meeting synchronization.
    SyncMeeting-Input:
      properties:
        id:
          type: string
          format: uuid
          title: Id
        title:
          type: string
          title: Title
        created_at:
          type: string
          title: Created At
        modified_at:
          type: string
          title: Modified At
        content:
          type: string
          title: Content
        is_deleted:
          type: boolean
          title: Is Deleted
          default: false
        finalized:
          type: boolean
          title: Finalized
          default: false
        calendar_external_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Calendar External Id
        transcript_deleted_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Transcript Deleted At
        refined_transcript:
          anyOf:
          - type: string
          - type: 'null'
          title: Refined Transcript
          readOnly: true
        refine_status:
          anyOf:
          - $ref: '#/components/schemas/RefineJobStatus'
          - type: 'null'
          readOnly: true
        refined_uploaded_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Refined Uploaded At
          readOnly: true
        slug:
          anyOf:
          - type: string
          - type: 'null'
          title: Slug
          readOnly: true
        visibility:
          anyOf:
          - $ref: '#/components/schemas/NoteVisibility'
          - type: 'null'
          readOnly: true
        notes:
          anyOf:
          - type: string
          - type: 'null'
          title: Notes
        summary:
          anyOf:
          - type: string
          - type: 'null'
          title: Summary
        live_transcript_uploaded:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Live Transcript Uploaded
        calendar_event_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Calendar Event Id
        ended_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Ended At
        speaker_map:
          anyOf:
          - $ref: '#/components/schemas/SpeakerMap'
          - type: 'null'
        import_source:
          anyOf:
          - type: string
          - type: 'null'
          title: Import Source
      type: object
      required:
      - id
      - title
      - created_at
      - modified_at
      - content
      title: SyncMeeting
      description: 'Model for meeting data in sync requests.


        The live transcript is uploaded out-of-band via a presigned S3 PUT;

        the client then sets ``live_transcript_uploaded`` on its next sync and

        the server stamps the server-internal ``live_uploaded_at`` watermark

        without the bytes. The live transcript is never inlined on the wire —

        cross-device live view is unsupported.


        ``refined_transcript`` is pull-only. The server inlines the

        refined NDJSON body when ``refined_uploaded_at`` advanced past the

        client''s watermark. The payload is one ``TranscriptEntry`` per line; the

        field is named without a content-type suffix because the client treats it as

        "refined transcript", not "refined JSON". Skipped on hard refresh —

        a 100-meeting backlog × 200 KB each would blow up the response when

        a device catches up after a long offline window.


        ``refined_uploaded_at`` rides every pull (hard refresh included,

        which skips the inline bytes) as the client''s reconciliation

        signal: when the row''s stamp beats the client''s locally persisted

        fetched-through stamp, it downloads the bytes out-of-band via

        ``GET /meetings/{id}/refined-transcript``. Response-only — a push

        carrying it never writes the column. ``live_uploaded_at`` stays

        deliberately NOT on the wire: cross-device live view is

        unsupported, so clients have no use for that watermark.'
    MeetingRefinedTranscriptUrlResponse:
      properties:
        url:
          type: string
          pattern: ^https?://
          format: uri
          title: Url
        expires_at:
          type: string
          format: date-time
          title: Expires At
          description: Absolute UTC expiry of the presigned GET
      type: object
      required:
      - url
      - expires_at
      title: MeetingRefinedTranscriptUrlResponse
      description: 'Presigned S3 GET for a meeting''s refined-transcript NDJSON.


        The download twin of :class:`MeetingUploadUrlResponse` (same shape as the

        notes ``NoteImageKey`` payload): the desktop calls

        ``GET /meetings/{id}/refined-transcript`` when a pull row''s

        ``refined_uploaded_at`` stamp beats its locally persisted fetched-through

        stamp, then downloads ``refined.ndjson`` straight from S3 — the bytes

        never transit the API.'
    SyncMeeting-Output:
      properties:
        id:
          type: string
          format: uuid
          title: Id
        title:
          type: string
          title: Title
        created_at:
          type: string
          title: Created At
        modified_at:
          type: string
          title: Modified At
        content:
          type: string
          title: Content
        is_deleted:
          type: boolean
          title: Is Deleted
          default: false
        finalized:
          type: boolean
          title: Finalized
          default: false
        calendar_external_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Calendar External Id
        transcript_deleted_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Transcript Deleted At
        refined_transcript:
          anyOf:
          - type: string
          - type: 'null'
          title: Refined Transcript
          readOnly: true
        refine_status:
          anyOf:
          - $ref: '#/components/schemas/RefineJobStatus'
          - type: 'null'
          readOnly: true
        refined_uploaded_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Refined Uploaded At
          readOnly: true
        slug:
          anyOf:
          - type: string
          - type: 'null'
          title: Slug
          readOnly: true
        visibility:
          anyOf:
          - $ref: '#/components/schemas/NoteVisibility'
          - type: 'null'
          readOnly: true
        notes:
          anyOf:
          - type: string
          - type: 'null'
          title: Notes
        summary:
          anyOf:
          - type: string
          - type: 'null'
          title: Summary
        live_transcript_uploaded:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Live Transcript Uploaded
        calendar_event_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Calendar Event Id
        ended_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Ended At
        speaker_map:
          anyOf:
          - $ref: '#/components/schemas/SpeakerMap'
          - type: 'null'
        import_source:
          anyOf:
          - type: string
          - type: 'null'
          title: Import Source
      type: object
      required:
      - id
      - title
      - created_at
      - modified_at
      - content
      title: SyncMeeting
      description: 'Model for meeting data in sync requests.


        The live transcript is uploaded out-of-band via a presigned S3 PUT;

        the client then sets ``live_transcript_uploaded`` on its next sync and

        the server stamps the server-internal ``live_uploaded_at`` watermark

        without the bytes. The live transcript is never inlined on the wire —

        cross-device live view is unsupported.


        ``refined_transcript`` is pull-only. The server inlines the

        refined NDJSON body when ``refined_uploaded_at`` advanced past the

        client''s watermark. The payload is one ``TranscriptEntry`` per line; the

        field is named without a content-type suffix because the client treats it as

        "refined transcript", not "refined JSON". Skipped on hard refresh —

        a 100-meeting backlog × 200 KB each would blow up the response when

        a device catches up after a long offline window.


        ``refined_uploaded_at`` rides every pull (hard refresh included,

        which skips the inline bytes) as the client''s reconciliation

        signal: when the row''s stamp beats the client''s locally persisted

        fetched-through stamp, it downloads the bytes out-of-band via

        ``GET /meetings/{id}/refined-transcript``. Response-only — a push

        carrying it never writes the column. ``live_uploaded_at`` stays

        deliberately NOT on the wire: cross-device live view is

        unsupported, so clients have no use for that watermark.'
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
            - type: string
            - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
      - loc
      - msg
      - type
      title: ValidationError
    SpeakerObservationsUploadUrlRequest:
      properties:
        content_type:
          type: string
          title: Content Type
          description: Observation-log MIME type; must be application/x-ndjson
        size_bytes:
          type: integer
          exclusiveMinimum: 0.0
          title: Size Bytes
          description: NDJSON body size in bytes
      type: object
      required:
      - content_type
      - size_bytes
      title: SpeakerObservationsUploadUrlRequest
      description: 'Request for a presigned S3 slot to upload a meeting''s speaker-observation log.


        Distinct from :class:`MeetingUploadUrlRequest`: NDJSON not audio, an

        8 MiB cap not 500 MiB, and no ``context`` passthrough — this artifact

        carries observations the server reduces into canonical speakers, not

        refine/summary context.'
    SpeakerMap:
      properties:
        people:
          additionalProperties:
            $ref: '#/components/schemas/SpeakerPerson'
          type: object
          maxProperties: 512
          title: People
        assignments:
          additionalProperties:
            $ref: '#/components/schemas/SpeakerAssignment'
          type: object
          maxProperties: 512
          title: Assignments
      additionalProperties: false
      type: object
      title: SpeakerMap
      description: 'The ``meetings.speaker_map`` blob (nullable JSONB on the meeting row).


        ``people`` is keyed by person id (a plain uuid4 string, minted by whoever

        creates the person); ``assignments`` is keyed by ``refined_id`` (the

        transcript ``speaker.id``, serialized as a string key).'
    NoteVisibility:
      type: string
      enum:
      - anyone_with_link
      - domain
      - invited_only
      title: NoteVisibility
    MeetingUploadUrlResponse:
      properties:
        url:
          type: string
          title: Url
        fields:
          additionalProperties:
            type: string
          type: object
          title: Fields
        expires_at:
          type: string
          title: Expires At
          description: ISO-8601 UTC expiry of the presigned POST
      type: object
      required:
      - url
      - fields
      - expires_at
      title: MeetingUploadUrlResponse
      description: 'Presigned S3 POST the desktop uses to upload refine audio directly.


        The S3 object key is carried inside ``fields[''key'']`` per the S3

        presigned-POST contract, so it is not duplicated as a top-level field.'
    MeetingSyncResponse:
      properties:
        acked:
          items:
            type: string
            format: uuid
          type: array
          title: Acked
        pull:
          items:
            $ref: '#/components/schemas/SyncMeeting-Output'
          type: array
          title: Pull
        sync_time:
          type: string
          title: Sync Time
        next_cursor:
          anyOf:
          - type: string
          - type: 'null'
          title: Next Cursor
          description: Non-null when more changed meetings remain; refetch with it and the same last_sync_time.
      type: object
      required:
      - acked
      - pull
      - sync_time
      title: MeetingSyncResponse
      description: Response model for meeting synchronization.
    LiveTranscriptUploadUrlRequest:
      properties:
        content_type:
          type: string
          title: Content Type
          description: Live-transcript MIME type; must be application/x-ndjson
        size_bytes:
          type: integer
          exclusiveMinimum: 0.0
          title: Size Bytes
          description: NDJSON body size in bytes
      type: object
      required:
      - content_type
      - size_bytes
      title: LiveTranscriptUploadUrlRequest
      description: 'Request for a presigned S3 slot to upload a meeting''s live-transcript NDJSON.


        Sibling to :class:`SpeakerObservationsUploadUrlRequest`: same NDJSON

        content type, but the 64 MiB ``LIVE_TRANSCRIPT_MAX_BYTES`` cap and a

        distinct server-composed key.'
    SpeakerAssignment:
      properties:
        consensus:
          anyOf:
          - type: string
            maxLength: 64
          - type: 'null'
          title: Consensus
        user:
          anyOf:
          - type: string
            maxLength: 64
          - type: 'null'
          title: User
        mic:
          anyOf:
          - type: string
            maxLength: 64
          - type: 'null'
          title: Mic
        dom:
          anyOf:
          - type: string
            maxLength: 64
          - type: 

# --- truncated at 32 KB (33 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/wispr-ai/refs/heads/main/openapi/wispr-ai-meetings-api-openapi.yml