{
  "openapi": "3.1.0",
  "info": {
    "title": "Bisque API",
    "version": "1.0.0",
    "x-api-version": "1",
    "x-versioning": {
      "scheme": "path",
      "current": "/api/v1",
      "unversionedAlias": "/api",
      "deprecationHeaders": [
        "Deprecation",
        "Sunset"
      ],
      "minimumNoticeDays": 90,
      "policy": "https://bisque.today/docs/api/versioning"
    },
    "x-lifecycle": {
      "stability": "stable",
      "currentVersion": "1",
      "versionedBy": "path",
      "pathPrefixes": {
        "presentations": "/api/v1/presentations",
        "presentationsUnversionedAlias": "/api/presentations",
        "tools": "/v1"
      },
      "compatibleChanges": [
        "new optional request fields",
        "new response fields",
        "new endpoints",
        "new enum values on fields documented as open"
      ],
      "breakingChanges": {
        "shipsAs": "a new path prefix; the old one keeps serving",
        "minimumNoticeDays": 90,
        "signals": {
          "Deprecation": "RFC 9745 — the date the old path was deprecated",
          "Sunset": "RFC 8594 — the date the old path stops serving",
          "Link": "rel=\"successor-version\" — the new path"
        }
      },
      "deprecated": [],
      "changelog": "https://bisque.today/docs/api/versioning"
    },
    "x-sandbox": {
      "kind": "dry-run",
      "description": "POST /api/presentations/create with `dryRun: true` (or the MCP tool create_presentation with dryRun) runs every check a real publish runs — format, slug and org conflicts, tier cap, credit preflight — against live data and writes nothing. The 200 reply carries status `validated` and the URL a real create would publish at. Reads (/api/presentations/context) need no credential and touch nothing.",
      "request": {
        "path": "/api/presentations/create",
        "field": "dryRun"
      },
      "documentation": "https://bisque.today/docs/api#sandbox"
    },
    "summary": "Publish narrated presentations from an agent, and read any shared presentation as text.",
    "description": "Bisque turns an HTML file an agent wrote into a narrated presentation on a shareable watch link (`/p/{handle}/{slug}`), and lets an agent read any shared presentation back as a transcript.\n\n**Start here:** `GET /api/presentations/spec` returns the authoring format; `GET /api/presentations/context` reads a presentation and needs no credentials. `POST /api/presentations/create` publishes and needs a bearer credential — see `/auth.md` for how an agent obtains one.\n\nThe same tools are available over MCP (Streamable HTTP) at `/mcp`; the docs are queryable over MCP at `/docs/mcp` with no credentials.\n\n**Retries:** send an `Idempotency-Key` header on every POST. A retry with the same key and body replays the first response (marked `Idempotent-Replayed: true`); the same key with a different body is a 422.\n\n**Rate limits:** responses carry `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (seconds) and `RateLimit-Policy`; a 429 adds `Retry-After`.\n\n**Versioning:** this is version 1 of the presentations API, addressed by path (`/api/presentations/...`; the agent tool API is `/v1/...`). Fields are only added, never removed or retyped, within a version. A breaking change ships under a new path prefix and the old one keeps serving for at least 90 days, during which its responses carry `Deprecation` and `Sunset` headers (RFC 9745 / RFC 8594) naming the date. `info.x-lifecycle` states the same policy field by field.\n\n**Sandbox:** `POST /api/presentations/create` with `dryRun: true` runs every check a real publish runs and writes nothing — the reply is a 200 with status `validated` and the URL a real create would publish at. Use it to test an integration without touching a channel.\n\n**Batch:** `POST /api/presentations/context` (alias `/api/presentations/context/batch`) reads up to 20 presentations in one request; each item succeeds or fails on its own.",
    "termsOfService": "https://bisque.cloud/terms",
    "contact": {
      "name": "Bisque",
      "email": "hello@bisque.today",
      "url": "https://bisque.today/docs/api"
    },
    "x-logo": {
      "url": "https://bisque.today/favicon-512x512.png"
    }
  },
  "externalDocs": {
    "description": "API guide",
    "url": "https://bisque.today/docs/api"
  },
  "servers": [
    {
      "url": "https://bisque.today",
      "description": "Public host (bisque.today). Every path below resolves here."
    },
    {
      "url": "https://bisque.cloud",
      "description": "App host (bisque.cloud). Same API; also the OAuth issuer."
    }
  ],
  "tags": [
    {
      "name": "Read",
      "description": "Public reads. No credential needed."
    },
    {
      "name": "Publish",
      "description": "Create and check presentations. Needs a bearer credential."
    },
    {
      "name": "MCP",
      "description": "JSON-RPC 2.0 over Streamable HTTP (MCP 2025-06-18)."
    },
    {
      "name": "OAuth",
      "description": "OAuth 2.1 authorization server on bisque.cloud; see /auth.md."
    },
    {
      "name": "Discovery",
      "description": "Machine-readable site descriptions."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/presentations/spec": {
      "get": {
        "tags": [
          "Read"
        ],
        "operationId": "getPresentationSpec",
        "summary": "The html-presentation/v1 authoring format",
        "description": "Markdown. Fetch this before writing a presentation; never write the format from memory. Modules are addressable at `/api/presentations/spec/{part}`.",
        "security": [],
        "responses": {
          "200": {
            "description": "The format contract.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/presentations/spec/{part}": {
      "get": {
        "tags": [
          "Read"
        ],
        "operationId": "getPresentationSpecPart",
        "summary": "One module of the authoring format",
        "security": [],
        "parameters": [
          {
            "name": "part",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Module name, with or without `.md`. The index lists the modules."
          }
        ],
        "responses": {
          "200": {
            "description": "The module.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown module.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "part": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/presentations/context": {
      "get": {
        "tags": [
          "Read"
        ],
        "operationId": "getPresentationContext",
        "summary": "Read a presentation as text",
        "description": "Metadata, the per-slide narration transcript with timestamps, and the bundle's `context.md`. Public and unlisted presentations resolve without credentials; send a bearer credential to read your own private ones. Anonymous callers get 60 requests per 10 minutes per IP, authenticated callers 300.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "A watch URL, `https://bisque.today/p/{handle}/{slug}`. Either this or `handle` + `slug`.",
            "example": "https://bisque.today/p/siderakis/claude-system-card"
          },
          {
            "name": "handle",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Channel handle (leading @ tolerated)."
          },
          {
            "name": "slug",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The presentation.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresentationContext"
                }
              }
            }
          },
          "400": {
            "description": "Neither `url` nor `handle` + `slug` identified a presentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "A credential was sent and is unusable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No presentation you can see at that address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (`CONTEXT_RATE_LIMITED`). Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the window frees up."
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Read"
        ],
        "operationId": "getPresentationContextBatch",
        "summary": "Read up to 20 presentations in one request",
        "description": "The batch form of the GET. Send `{\"urls\": [...]}`, `{\"items\": [{\"url\"} | {\"handle\",\"slug\"}, ...]}`, or a bare JSON array of watch URLs; each item resolves on its own, so one bad address fails its row and not the batch. Every item costs one slot of the same per-IP window as a GET (60 per 10 minutes anonymous, 300 with a credential); items past the window come back as `CONTEXT_RATE_LIMITED` rows rather than failing the request.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "x-batch": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PresentationContextBatchRequest"
              },
              "examples": {
                "urls": {
                  "value": {
                    "urls": [
                      "https://bisque.today/p/siderakis/claude-system-card",
                      "https://bisque.today/p/siderakis/bun-1-3"
                    ]
                  }
                },
                "items": {
                  "value": {
                    "items": [
                      {
                        "url": "https://bisque.today/p/siderakis/claude-system-card"
                      },
                      {
                        "handle": "siderakis",
                        "slug": "bun-1-3"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One row per item, in request order.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresentationContextBatchResult"
                }
              }
            }
          },
          "400": {
            "description": "The body was not one of the three accepted shapes, was empty, or named more than 20 presentations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "A credential was sent and is unusable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The window had no room for even the first item. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/presentations/context/batch": {
      "post": {
        "tags": [
          "Read"
        ],
        "operationId": "getPresentationContextBatchAlias",
        "summary": "Read up to 20 presentations in one request",
        "description": "The batch form of the GET. Send `{\"urls\": [...]}`, `{\"items\": [{\"url\"} | {\"handle\",\"slug\"}, ...]}`, or a bare JSON array of watch URLs; each item resolves on its own, so one bad address fails its row and not the batch. Every item costs one slot of the same per-IP window as a GET (60 per 10 minutes anonymous, 300 with a credential); items past the window come back as `CONTEXT_RATE_LIMITED` rows rather than failing the request.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "x-batch": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PresentationContextBatchRequest"
              },
              "examples": {
                "urls": {
                  "value": {
                    "urls": [
                      "https://bisque.today/p/siderakis/claude-system-card",
                      "https://bisque.today/p/siderakis/bun-1-3"
                    ]
                  }
                },
                "items": {
                  "value": {
                    "items": [
                      {
                        "url": "https://bisque.today/p/siderakis/claude-system-card"
                      },
                      {
                        "handle": "siderakis",
                        "slug": "bun-1-3"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One row per item, in request order.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresentationContextBatchResult"
                }
              }
            }
          },
          "400": {
            "description": "The body was not one of the three accepted shapes, was empty, or named more than 20 presentations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "A credential was sent and is unusable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The window had no room for even the first item. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/oembed": {
      "get": {
        "tags": [
          "Read"
        ],
        "operationId": "getOEmbed",
        "summary": "oEmbed for a watch URL",
        "security": [],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "oEmbed `video` payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Not a Bisque watch URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No public presentation there.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/presentations": {
      "get": {
        "tags": [
          "Publish"
        ],
        "operationId": "listPresentations",
        "summary": "Your presentations, newest first, cursor-paginated",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The `nextCursor` from the previous page. Opaque."
          },
          {
            "name": "visibility",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "public",
                "unlisted",
                "private",
                "org"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresentationPage"
                }
              }
            }
          },
          "400": {
            "description": "Bad `visibility` or a cursor this endpoint did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/presentations/create": {
      "post": {
        "tags": [
          "Publish"
        ],
        "operationId": "createPresentation",
        "summary": "Publish a presentation from HTML",
        "description": "Send the `html-presentation/v1` file. Bisque parses it, narrates each slide server-side, and publishes to `webUrl`. The reply is immediate (202); narration finishes in the background — poll `statusUrl`. Re-send with the same `slug` to republish. With `dryRun: true` the same checks run and nothing is written: the reply is a 200 with status `validated` (the sandbox).",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePresentationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dry run passed (`dryRun: true`): every check succeeded and nothing was published. `webUrl` is where a real create would publish.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePresentationResult"
                }
              }
            }
          },
          "202": {
            "description": "Accepted; narration queued.",
            "headers": {
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePresentationResult"
                }
              }
            }
          },
          "400": {
            "description": "The HTML or a field failed validation; `error.message` names the problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A create for the same presentation is still running (`CREATE_IN_PROGRESS`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/presentations/status": {
      "get": {
        "tags": [
          "Publish"
        ],
        "operationId": "getPresentationStatus",
        "summary": "Narration and publish progress",
        "parameters": [
          {
            "name": "presentationId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresentationStatus"
                }
              }
            }
          },
          "400": {
            "description": "`presentationId` missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not one of your presentations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/presentations": {
      "post": {
        "tags": [
          "Publish"
        ],
        "operationId": "createPresentationRun",
        "summary": "Create a presentation and put its slides up now",
        "description": "Start a creation: send the HTML and the assets, and the slides are viewable at the returned `webUrl` straight away — silent until narration arrives. The response names the slides that still need narrating (`needsAudio`) and the ones whose audio carried forward from last time (`reused`), with an upload target for each asset and each slide. Narrate each slide with the `bisque-voice` CLI, PUT the MP3 to its target, then PATCH the slide. While the creation is open it can take more slides (`PATCH /v1/presentations/{presentationId}`): the presentation document carries `live: { revision, startedAt }`, `revision` rises on every append, and the watch page appends the new slides in place. API-key callers only.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublishNarratedRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The creation is open and the slides are live.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/presentations/{presentationId}": {
      "get": {
        "tags": [
          "Publish"
        ],
        "operationId": "getLivePresentation",
        "summary": "Read a live creation to resume it",
        "description": "The open creation as it stands, for its owner picking it up from a new agent session: every slide published so far as `indexHtml`, which slides have narration, the voice and speed they were narrated with, the assets it holds, the owner's answer to every survey so far, and `live.revision`. Send that revision back as `baseRevision` on the next append; an append written against an older one is refused with `LIVE_REVISION_CONFLICT`. Reading changes nothing. API-key callers only.",
        "parameters": [
          {
            "name": "presentationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The live creation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Another account opened this creation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not a presentation you can change, or not live: finalized or never opened live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Another publish superseded this creation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Publish"
        ],
        "operationId": "appendPresentationSlides",
        "summary": "Add slides to a live creation",
        "description": "Send the whole HTML again with new sections appended. Existing slides keep their keys and order (`LIVE_APPEND_ONLY` otherwise); unchanged slides carry their audio forward, assets the body leaves out are kept, and the response names the slides that need narrating with an upload target for each, exactly as create does. Add `baseRevision`, the `live.revision` the slides were written against, and an append that fell behind is refused (`LIVE_REVISION_CONFLICT`) rather than overwriting slides it never saw; an append waits up to 20 seconds for another append or narration upload in progress, and is refused with `LIVE_APPEND_IN_PROGRESS` if it is still going. `live.revision` on the presentation rises by one and anyone watching sees the new slides without a reload. API-key callers only.",
        "parameters": [
          {
            "name": "presentationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublishNarratedRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The slides are appended and live.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "An existing slide was removed or reordered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No creation is open for this presentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Another publish superseded this creation, another append got there first, or one is being written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/presentations/{presentationId}/surveys/{surveyId}/response": {
      "get": {
        "tags": [
          "Publish"
        ],
        "operationId": "waitForSurveyResponse",
        "summary": "Wait for the owner's answer to a survey",
        "description": "Returns the caller's own answer to one survey on one of their presentations. With `wait`, the request is held on a listener for up to that many seconds (at most 25) and answers the moment the survey is complete, or with `until=submit` the moment the viewer presses Continue (answers save as they are typed, so a complete text answer can still be half-written). On timeout it returns the answer as it stands with 200, and the caller polls again. This is the wait an agent already does for a deploy or a CI run, shaped like `gh run watch`. API-key callers only.",
        "parameters": [
          {
            "name": "presentationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "surveyId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `id` in the slide's `data-survey-spec`."
          },
          {
            "name": "wait",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 25
            },
            "description": "Seconds to hold the request before answering with the answer as it stands. Default 0."
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "complete",
                "submit"
              ]
            },
            "description": "`complete` (default) answers once every required question has an answer. `submit` waits for the viewer to press Continue, or play on the survey's slide."
          }
        ],
        "responses": {
          "200": {
            "description": "`{ presentationId, surveyId, complete, submitted, answers?, submittedAt?, updatedAt? }`; `answers` maps question id to value when complete.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "`wait` is not a number of seconds, or `until` is not `complete` or `submit`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not one of your presentations, or no such survey.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/presentations/{presentationId}/slides/{slideKey}": {
      "patch": {
        "tags": [
          "Publish"
        ],
        "operationId": "addPresentationSlideAudio",
        "summary": "Give one slide its narration",
        "description": "PUT the MP3 to the upload target the creation returned, then send its word timings, duration and digest here. The slide starts playing for anyone already watching, and `audioProgress` on the presentation advances. API-key callers only.",
        "parameters": [
          {
            "name": "presentationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "slideKey",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "e.g. `slide-04`, as the creation named it."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Narration counts after this slide.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "The timings do not match the slide's words.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No creation is open for this presentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/presentations/{presentationId}:finalize": {
      "post": {
        "tags": [
          "Publish"
        ],
        "operationId": "finalizePresentation",
        "summary": "Finish the creation",
        "description": "Claims the version number and marks the presentation ready. Slides still without audio stay silent and are named in `silentSlides`. API-key callers only.",
        "parameters": [
          {
            "name": "presentationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Finished; this is the version.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No creation is open for this presentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/presentations/publish-narrated": {
      "post": {
        "tags": [
          "Publish"
        ],
        "operationId": "publishNarratedPresentation",
        "deprecated": true,
        "summary": "Publish with audio you synthesized locally (deprecated)",
        "description": "Superseded by `POST /v1/presentations`, which puts the slides up before narrating them. This one publishes nothing until every slide is narrated, so the watch URL does not work until the whole run finishes. It keeps working. Narrate each slide with the `bisque-voice` CLI, then post the HTML plus per-slide MP3s and word timings; slides you omit from `audio` carry their audio forward when their narration text is unchanged. API-key callers only.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublishNarratedRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted; upload the bytes to `uploadUrls`, then POST `completeUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishNarratedResult"
                }
              }
            }
          },
          "400": {
            "description": "Audio or HTML failed validation; the message names the slide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": [
          "MCP"
        ],
        "operationId": "presentationsMcp",
        "summary": "Presentations MCP server",
        "description": "Streamable HTTP, stateless. Tools: `get_presentation_spec`, `create_presentation`, `publish_narrated_presentation`, `get_presentation_status`, `list_presentations`, `get_presentation_analytics`, `get_presentation_context`, plus org administration for bisque.team admins. Prompt `present`; resource `bisque://presentations/format-spec`; MCP Apps widget `ui://bisque/presentation-player.html` linked from tools via `_meta.ui.resourceUri`. Canonical URL `https://bisque.cloud/presentations/mcp`. A 401 carries `WWW-Authenticate: Bearer resource_metadata=...` for OAuth discovery.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "presentations"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonRpcRequest"
              },
              "examples": {
                "initialize": {
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "initialize",
                    "params": {
                      "protocolVersion": "2025-06-18",
                      "capabilities": {},
                      "clientInfo": {
                        "name": "example",
                        "version": "1.0"
                      }
                    }
                  }
                },
                "listTools": {
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 2,
                    "method": "tools/list"
                  }
                }
              }
            }
          },
          "description": "A JSON-RPC 2.0 request or notification."
        },
        "responses": {
          "200": {
            "description": "JSON-RPC result or error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "202": {
            "description": "Notification accepted."
          },
          "401": {
            "description": "Unauthorized; see `WWW-Authenticate`.",
            "headers": {
              "WWW-Authenticate": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/ask": {
      "post": {
        "tags": [
          "Read"
        ],
        "operationId": "ask",
        "summary": "Ask a question (NLWeb 0.55)",
        "description": "Microsoft's NLWeb protocol over the docs and public presentations. Send `{\"query\": \"...\"}` (or the structured 0.55 form with `query.text`, `context.prev`, `prefer.streaming`) and get a ranked list of schema.org items. With `prefer.streaming: true` or `Accept: text/event-stream` the reply is an SSE stream of `start`, `result` and `complete` events. GET with `?query=` works the same way. No credential; 120 requests per 10 minutes per IP.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AskRequest"
              },
              "examples": {
                "flat": {
                  "value": {
                    "query": "how do I publish from a script"
                  }
                },
                "structured": {
                  "value": {
                    "query": {
                      "text": "how do I publish from a script"
                    },
                    "prefer": {
                      "streaming": true
                    },
                    "meta": {
                      "version": "0.55"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer: ranked items, or the same as an SSE stream.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskResponse"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "`event: start` with `_meta`; one `event: result` per item as `{index, item}`; `event: complete` with `_meta`."
                }
              }
            }
          },
          "400": {
            "description": "No query text.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskError"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Read"
        ],
        "operationId": "askGet",
        "summary": "Ask a question (NLWeb 0.55), GET form",
        "security": [],
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "streaming",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "1",
                "0"
              ]
            },
            "description": "Stream the reply as SSE. Off unless set."
          },
          {
            "name": "prev",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated previous queries."
          }
        ],
        "responses": {
          "200": {
            "description": "Same as POST.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskResponse"
                }
              }
            }
          }
        }
      }
    },
    "/docs/mcp": {
      "post": {
        "tags": [
          "MCP"
        ],
        "operationId": "docsMcp",
        "summary": "Documentation MCP server",
        "description": "Streamable HTTP, stateless, no credentials. Tools: `list_docs`, `read_doc`, `search_docs`, `get_openapi_spec`; each docs page is also a `bisque://docs/{slug}` resource. Answers 'how does Bisque work' questions from the same markdown the site renders.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonRpcRequest"
              },
              "examples": {
                "initialize": {
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "initialize",
                    "params": {
                      "protocolVersion": "2025-06-18",
                      "capabilities": {},
                      "clientInfo": {
                        "name": "example",
                        "version": "1.0"
                      }
                    }
                  }
                },
                "listTools": {
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 2,
                    "method": "tools/list"
                  }
                }
              }
            }
          },
          "description": "A JSON-RPC 2.0 request or notification."
        },
        "responses": {
          "200": {
            "description": "JSON-RPC result or error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                }
              }
            }
          },
          "202": {
            "description": "Notification accepted."
          }
        }
      }
    },
    "/oauth/register": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "operationId": "oauthRegister",
        "summary": "Dynamic client registration (RFC 7591)",
        "servers": [
          {
            "url": "https://bisque.cloud"
          }
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "redirect_uris"
                ],
                "properties": {
                  "client_name": {
                    "type": "string"
                  },
                  "redirect_uris": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uri"
                    },
                    "description": "https, or http on loopback."
                  },
                  "token_endpoint_auth_method": {
                    "type": "string",
                    "enum": [
                      "none",
                      "client_secret_post",
                      "client_secret_basic"
                    ],
                    "default": "client_secret_basic"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The client.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "client_id": {
                      "type": "string"
                    },
                    "client_secret": {
                      "type": "string"
                    },
                    "client_name": {
                      "type": "string"
                    },
                    "redirect_uris": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "token_endpoint_auth_method": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad registration.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/token": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "operationId": "oauthToken",
        "summary": "Token endpoint (authorization_code + PKCE, refresh_token)",
        "servers": [
          {
            "url": "https://bisque.cloud"
          }
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type",
                  "client_id"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "authorization_code",
                      "refresh_token"
                    ]
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "client_secret": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string"
                  },
                  "code_verifier": {
                    "type": "string"
                  },
                  "redirect_uri": {
                    "type": "string",
                    "format": "uri"
                  },
                  "refresh_token": {
                    "type": "string"
                  },
                  "resource": {
                    "type": "string",
                    "format": "uri",
                    "description": "Must be https://bisque.cloud/presentations/mcp when sent."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A token pair.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string",
                      "description": "`bisque_oat_…`, 1 hour."
                    },
                    "token_type": {
                      "type": "string",
                      "const": "Bearer"
                    },
                    "expires_in": {
                      "type": "integer"
                    },
                    "refresh_token": {
                      "type": "string",
                      "description": "`bisque_ort_…`, 90 days, rotates on use."
                    },
                    "scope": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad grant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "401": {
            "description": "Client authentication failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/revoke": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "operationId": "oauthRevoke",
        "summary": "Revoke a token (RFC 7009)",
        "description": "Revoking a refresh token also revokes the access tokens issued with it. Unknown tokens return 200.",
        "servers": [
          {
            "url": "https://bisque.cloud"
          }
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "token",
                  "client_id"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "token_type_hint": {
                    "type": "string",
                    "enum": [
                      "access_token",
                      "refresh_token"
                    ]
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "client_secret": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The token is no longer usable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "`token` missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "401": {
            "description": "Client authentication failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/userinfo": {
      "get": {
        "tags": [
          "OAuth"
        ],
        "operationId": "oauthUserInfo",
        "summary": "OIDC-style userinfo for openid-scoped tokens",
        "servers": [
          {
            "url": "https://bisque.cloud"
          }
        ],
        "security": [
          {
            "oauth2": [
              "openid"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "`sub` and, with the email scope, `email`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sub": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Token lacks the openid scope or is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-authorization-server": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "oauthAuthorizationServerMetadata",
        "summary": "RFC 8414 authorization-server metadata, with the auth.md `agent_auth` block",
        "security": [],
        "responses": {
          "200": {
            "description": "Metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "oauthProtectedResourceMetadata",
        "summary": "RFC 9728 protected-resource metadata for the MCP server",
        "security": [],
        "responses": {
          "200": {
            "description": "Metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/mcp.json": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "mcpManifest",
        "summary": "MCP manifest: the presentations server's address, transport, tools, and the widget each tool renders",
        "security": [],
        "responses": {
          "200": {
            "description": "`name`, `kind`, `url`, `transport`, `capabilities`, `tools[]` (with `ui` for MCP Apps tools), `trustManifest`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/did.json": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "didDocument",
        "summary": "DID document for did:web:bisque.today — the identity the ARD catalog's trustManifest cites",
        "security": [],
        "responses": {
          "200": {
            "description": "W3C DID document: `id`, `verificationMethod` (the Web Bot Auth Ed25519 key), and `service` entries for the discovery documents.",
            "content": {
              "application/did+json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/http-message-signatures-directory": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "webBotAuthDirectory",
        "summary": "Web Bot Auth key directory (Ed25519 JWKs) for requests Bisque's agents sign",
        "security": [],
        "responses": {
          "200": {
            "description": "`keys` array of OKP/Ed25519 JWKs with `kid`, `nbf`, `exp`.",
            "content": {
              "application/http-message-signatures-directory+json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/auth.md": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "authMd",
        "summary": "How an agent obtains, uses, and revokes a credential",
        "security": [],
        "responses": {
          "200": {
            "description": "Markdown walkthrough.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "llmsTxt",
        "summary": "What this site is, when to use it, and where the docs are",
        "security": [],
        "responses": {
          "200": {
            "description": "Plain text (llmstxt.org).",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/presentations/spec": {
      "get": {
        "tags": [
          "Read"
        ],
        "operationId": "getPresentationSpecV1",
        "summary": "The html-presentation/v1 authoring format",
        "description": "Versioned address of `/api/presentations/spec` — the same endpoint and contract. Markdown. Fetch this before writing a presentation; never write the format from memory. Modules are addressable at `/api/presentations/spec/{part}`.",
        "security": [],
        "responses": {
          "200": {
            "description": "The format contract.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/presentations/spec/{part}": {
      "get": {
        "tags": [
          "Read"
        ],
        "operationId": "getPresentationSpecPartV1",
        "summary": "One module of the authoring format",
        "security": [],
        "parameters": [
          {
            "name": "part",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Module name, with or without `.md`. The index lists the modules."
          }
        ],
        "responses": {
          "200": {
            "description": "The module.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown module.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "part": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "Versioned address of `/api/presentations/spec/{part}` — the same endpoint and contract."
      }
    },
    "/api/v1/presentations/context": {
      "get": {
        "tags": [
          "Read"
        ],
        "operationId": "getPresentationContextV1",
        "summary": "Read a presentation as text",
        "description": "Versioned address of `/api/presentations/context` — the same endpoint and contract. Metadata, the per-slide narration transcript with timestamps, and the bundle's `context.md`. Public and unlisted presentations resolve without credentials; send a bearer credential to read your own private ones. Anonymous callers get 60 requests per 10 minutes per IP, authenticated callers 300.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "A watch URL, `https://bisque.today/p/{handle}/{slug}`. Either this or `handle` + `slug`.",
            "example": "https://bisque.today/p/siderakis/claude-system-card"
          },
          {
            "name": "handle",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Channel handle (leading @ tolerated)."
          },
          {
            "name": "slug",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The presentation.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresentationContext"
                }
              }
            }
          },
          "400": {
            "description": "Neither `url` nor `handle` + `slug` identified a presentation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "A credential was sent and is unusable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No presentation you can see at that address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (`CONTEXT_RATE_LIMITED`). Wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the window frees up."
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Read"
        ],
        "operationId": "getPresentationContextBatchV1",
        "summary": "Read up to 20 presentations in one request",
        "description": "Versioned address of `/api/presentations/context` — the same endpoint and contract. The batch form of the GET. Send `{\"urls\": [...]}`, `{\"items\": [{\"url\"} | {\"handle\",\"slug\"}, ...]}`, or a bare JSON array of watch URLs; each item resolves on its own, so one bad address fails its row and not the batch. Every item costs one slot of the same per-IP window as a GET (60 per 10 minutes anonymous, 300 with a credential); items past the window come back as `CONTEXT_RATE_LIMITED` rows rather than failing the request.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "x-batch": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PresentationContextBatchRequest"
              },
              "examples": {
                "urls": {
                  "value": {
                    "urls": [
                      "https://bisque.today/p/siderakis/claude-system-card",
                      "https://bisque.today/p/siderakis/bun-1-3"
                    ]
                  }
                },
                "items": {
                  "value": {
                    "items": [
                      {
                        "url": "https://bisque.today/p/siderakis/claude-system-card"
                      },
                      {
                        "handle": "siderakis",
                        "slug": "bun-1-3"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One row per item, in request order.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresentationContextBatchResult"
                }
              }
            }
          },
          "400": {
            "description": "The body was not one of the three accepted shapes, was empty, or named more than 20 presentations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "A credential was sent and is unusable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The window had no room for even the first item. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/presentations/context/batch": {
      "post": {
        "tags": [
          "Read"
        ],
        "operationId": "getPresentationContextBatchAliasV1",
        "summary": "Read up to 20 presentations in one request",
        "description": "Versioned address of `/api/presentations/context/batch` — the same endpoint and contract. The batch form of the GET. Send `{\"urls\": [...]}`, `{\"items\": [{\"url\"} | {\"handle\",\"slug\"}, ...]}`, or a bare JSON array of watch URLs; each item resolves on its own, so one bad address fails its row and not the batch. Every item costs one slot of the same per-IP window as a GET (60 per 10 minutes anonymous, 300 with a credential); items past the window come back as `CONTEXT_RATE_LIMITED` rows rather than failing the request.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "x-batch": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PresentationContextBatchRequest"
              },
              "examples": {
                "urls": {
                  "value": {
                    "urls": [
                      "https://bisque.today/p/siderakis/claude-system-card",
                      "https://bisque.today/p/siderakis/bun-1-3"
                    ]
                  }
                },
                "items": {
                  "value": {
                    "items": [
                      {
                        "url": "https://bisque.today/p/siderakis/claude-system-card"
                      },
                      {
                        "handle": "siderakis",
                        "slug": "bun-1-3"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One row per item, in request order.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresentationContextBatchResult"
                }
              }
            }
          },
          "400": {
            "description": "The body was not one of the three accepted shapes, was empty, or named more than 20 presentations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "A credential was sent and is unusable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The window had no room for even the first item. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/presentations": {
      "get": {
        "tags": [
          "Publish"
        ],
        "operationId": "listPresentationsV1",
        "summary": "Your presentations, newest first, cursor-paginated",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The `nextCursor` from the previous page. Opaque."
          },
          {
            "name": "visibility",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "public",
                "unlisted",
                "private",
                "org"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresentationPage"
                }
              }
            }
          },
          "400": {
            "description": "Bad `visibility` or a cursor this endpoint did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Versioned address of `/api/presentations` — the same endpoint and contract."
      }
    },
    "/api/v1/presentations/create": {
      "post": {
        "tags": [
          "Publish"
        ],
        "operationId": "createPresentationV1",
        "summary": "Publish a presentation from HTML",
        "description": "Versioned address of `/api/presentations/create` — the same endpoint and contract. Send the `html-presentation/v1` file. Bisque parses it, narrates each slide server-side, and publishes to `webUrl`. The reply is immediate (202); narration finishes in the background — poll `statusUrl`. Re-send with the same `slug` to republish. With `dryRun: true` the same checks run and nothing is written: the reply is a 200 with status `validated` (the sandbox).",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePresentationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dry run passed (`dryRun: true`): every check succeeded and nothing was published. `webUrl` is where a real create would publish.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePresentationResult"
                }
              }
            }
          },
          "202": {
            "description": "Accepted; narration queued.",
            "headers": {
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePresentationResult"
                }
              }
            }
          },
          "400": {
            "description": "The HTML or a field failed validation; `error.message` names the problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A create for the same presentation is still running (`CREATE_IN_PROGRESS`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/presentations/status": {
      "get": {
        "tags": [
          "Publish"
        ],
        "operationId": "getPresentationStatusV1",
        "summary": "Narration and publish progress",
        "parameters": [
          {
            "name": "presentationId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresentationStatus"
                }
              }
            }
          },
          "400": {
            "description": "`presentationId` missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not one of your presentations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Versioned address of `/api/presentations/status` — the same endpoint and contract."
      }
    },
    "/api/v1/presentations/publish-narrated": {
      "post": {
        "tags": [
          "Publish"
        ],
        "operationId": "publishNarratedPresentationV1",
        "deprecated": true,
        "summary": "Publish with audio you synthesized locally (deprecated)",
        "description": "Versioned address of `/api/presentations/publish-narrated` — the same endpoint and contract. Superseded by `POST /v1/presentations`, which puts the slides up before narrating them. This one publishes nothing until every slide is narrated, so the watch URL does not work until the whole run finishes. It keeps working. Narrate each slide with the `bisque-voice` CLI, then post the HTML plus per-slide MP3s and word timings; slides you omit from `audio` carry their audio forward when their narration text is unchanged. API-key callers only.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublishNarratedRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted; upload the bytes to `uploadUrls`, then POST `completeUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishNarratedResult"
                }
              }
            }
          },
          "400": {
            "description": "Audio or HTML failed validation; the message names the slide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/oembed": {
      "get": {
        "tags": [
          "Read"
        ],
        "operationId": "getOEmbedV1",
        "summary": "oEmbed for a watch URL",
        "security": [],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "oEmbed `video` payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Not a Bisque watch URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No public presentation there.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "Versioned address of `/api/oembed` — the same endpoint and contract."
      }
    }
  },
  "components": {
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "A unique key per logical request (a UUID). A retry with the same key and body replays the first response with `Idempotent-Replayed: true` instead of publishing again; the same key with a different body is a 422 `IDEMPOTENCY_KEY_REUSED`. Keys last 24 hours."
      }
    },
    "headers": {
      "RateLimit-Limit": {
        "schema": {
          "type": "integer"
        },
        "description": "Requests allowed in the window."
      },
      "RateLimit-Remaining": {
        "schema": {
          "type": "integer"
        },
        "description": "Requests left in the window."
      },
      "RateLimit-Reset": {
        "schema": {
          "type": "integer"
        },
        "description": "Seconds until the window resets."
      },
      "RateLimit-Policy": {
        "schema": {
          "type": "string"
        },
        "description": "`<limit>;w=<window seconds>`."
      },
      "Deprecation": {
        "schema": {
          "type": "string"
        },
        "description": "Present on a deprecated endpoint: the date it was deprecated (RFC 9745)."
      },
      "Sunset": {
        "schema": {
          "type": "string"
        },
        "description": "Present on a deprecated endpoint: the date it stops serving (RFC 8594), at least 90 days out."
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A Bisque API key (`bisque_live_…`, from https://bisque.cloud/settings/keys or the `bisque login` device flow) or an OAuth access token (`bisque_oat_…`). Send as `Authorization: Bearer <credential>`. Walkthrough: /auth.md."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.1 with PKCE (S256) and dynamic client registration. Issuer https://bisque.cloud.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://bisque.cloud/oauth/authorize",
            "tokenUrl": "https://bisque.cloud/oauth/token",
            "refreshUrl": "https://bisque.cloud/oauth/token",
            "scopes": {
              "presentations": "Publish, list, and read presentations as the user.",
              "openid": "Read the user's subject identifier.",
              "email": "Read the user's email address."
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every error is JSON. `error.code` is stable and upper-snake-case; `error.message` is for people; `requestId` is what to quote in a bug report.",
        "required": [
          "error"
        ],
        "properties": {
          "requestId": {
            "type": "string",
            "format": "uuid"
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "examples": [
                  "UNAUTHORIZED",
                  "INVALID_ACCESS_TOKEN",
                  "INVALID_PRESENTATION_ID",
                  "CONTEXT_RATE_LIMITED",
                  "METHOD_NOT_ALLOWED",
                  "NOT_FOUND"
                ]
              },
              "message": {
                "type": "string"
              },
              "hint": {
                "type": "string",
                "description": "Where to look next, when there is one."
              }
            }
          }
        }
      },
      "OAuthError": {
        "type": "object",
        "description": "RFC 6749 §5.2 error body.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "examples": [
              "invalid_request",
              "invalid_client",
              "invalid_grant",
              "unsupported_grant_type"
            ]
          },
          "error_description": {
            "type": "string"
          }
        }
      },
      "JsonRpcRequest": {
        "type": "object",
        "required": [
          "jsonrpc",
          "method"
        ],
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "method": {
            "type": "string"
          },
          "params": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "JsonRpcResponse": {
        "type": "object",
        "required": [
          "jsonrpc",
          "id"
        ],
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "result": {},
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              },
              "data": {}
            }
          }
        }
      },
      "CreatePresentationRequest": {
        "type": "object",
        "required": [
          "indexHtml"
        ],
        "properties": {
          "indexHtml": {
            "type": "string",
            "description": "The whole `html-presentation/v1` file."
          },
          "title": {
            "type": "string",
            "description": "Overrides the title parsed from the HTML."
          },
          "slug": {
            "type": "string",
            "description": "URL slug under your channel. Reuse it to republish."
          },
          "presentationId": {
            "type": "string",
            "description": "Republish an existing presentation by id instead of slug."
          },
          "handle": {
            "type": "string",
            "description": "Which of your channels to publish on; defaults to your account's."
          },
          "dryRun": {
            "type": "boolean",
            "default": false,
            "description": "Sandbox: run every check a real create runs (format, slug and org conflicts, tier cap, credit preflight) and write nothing. The reply is a 200 with status `validated`."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "unlisted",
              "private",
              "org"
            ],
            "default": "unlisted",
            "description": "Who can open the watch link. Unlisted unless you say otherwise; `org` publishes into the bisque.team organization named by `org`."
          },
          "org": {
            "type": "string",
            "description": "bisque.team organization slug; publishes into the company space."
          },
          "contextMd": {
            "type": "string",
            "description": "Agent-facing background shipped with the bundle as `context.md`."
          },
          "designMd": {
            "type": "string",
            "description": "Design notes shipped with the bundle."
          },
          "voiceId": {
            "type": "string",
            "description": "Narration voice tag; defaults to the account's saved voice."
          },
          "madeWith": {
            "type": "array",
            "maxItems": 8,
            "items": {
              "type": "string",
              "maxLength": 80
            },
            "description": "Models credited with making this presentation — e.g. [\"Claude Opus 5\", \"Kokoro af_heart\"]. Free text, shown to viewers in the watch page's ⋯ menu. Omit to keep what a previous publish recorded; send `[]` to clear it."
          },
          "description": {
            "type": "string",
            "maxLength": 5000,
            "description": "Plain-text description shown under the player on the watch page, with web URLs linked — a summary, then the sources. Omit to keep what a previous publish sent; send an empty string to clear it."
          },
          "speechSpeed": {
            "type": "number",
            "description": "Narration speed. 1.0 is normal."
          },
          "voiceStability": {
            "type": "number",
            "description": "How closely the voice keeps to its reference, for voices that take it. Omit for the voice's default."
          },
          "assets": {
            "type": "object",
            "additionalProperties": true,
            "description": "Page assets (fonts, images) keyed by bundle path."
          }
        }
      },
      "PresentationPage": {
        "type": "object",
        "required": [
          "requestId",
          "items",
          "nextCursor",
          "hasMore"
        ],
        "properties": {
          "requestId": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PresentationSummary"
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page; null on the last page."
          },
          "hasMore": {
            "type": "boolean"
          }
        }
      },
      "PresentationSummary": {
        "type": "object",
        "required": [
          "presentationId",
          "title",
          "slug",
          "visibility",
          "publishStatus",
          "webUrl"
        ],
        "properties": {
          "presentationId": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "unlisted",
              "private",
              "org"
            ]
          },
          "publishStatus": {
            "type": "string"
          },
          "webUrl": {
            "type": "string",
            "format": "uri"
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "CreatePresentationResult": {
        "type": "object",
        "required": [
          "requestId",
          "presentationId",
          "webUrl",
          "status",
          "statusUrl"
        ],
        "properties": {
          "requestId": {
            "type": "string"
          },
          "presentationId": {
            "type": "string"
          },
          "webUrl": {
            "type": "string",
            "format": "uri",
            "description": "The watch link, e.g. https://bisque.today/p/{handle}/{slug}."
          },
          "status": {
            "type": "string",
            "enum": [
              "ready",
              "queued",
              "validated"
            ],
            "description": "`queued`: narration running, poll `statusUrl`. `ready`: live now. `validated`: a dry run passed and nothing was written."
          },
          "statusUrl": {
            "type": "string",
            "format": "uri"
          },
          "narration": {
            "type": "object",
            "additionalProperties": true
          },
          "dryRun": {
            "type": "boolean",
            "description": "Present and true only on a dry-run reply."
          }
        }
      },
      "PresentationContextBatchRequest": {
        "description": "Up to 20 presentations to read. Any of the three shapes.",
        "oneOf": [
          {
            "type": "object",
            "required": [
              "urls"
            ],
            "properties": {
              "urls": {
                "type": "array",
                "minItems": 1,
                "maxItems": 20,
                "items": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          {
            "type": "object",
            "required": [
              "items"
            ],
            "properties": {
              "items": {
                "type": "array",
                "minItems": 1,
                "maxItems": 20,
                "items": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "handle": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          {
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "items": {
              "type": "string",
              "format": "uri"
            }
          }
        ]
      },
      "PresentationContextBatchResult": {
        "type": "object",
        "required": [
          "requestId",
          "items"
        ],
        "properties": {
          "requestId": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "description": "One row per requested item, in request order.",
            "items": {
              "type": "object",
              "required": [
                "ok",
                "input"
              ],
              "properties": {
                "ok": {
                  "type": "boolean"
                },
                "input": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "The item as sent, normalized to an object."
                },
                "presentation": {
                  "$ref": "#/components/schemas/PresentationContext"
                },
                "error": {
                  "type": "object",
                  "required": [
                    "code",
                    "message"
                  ],
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "PresentationStatus": {
        "type": "object",
        "required": [
          "requestId",
          "presentationId",
          "publishStatus",
          "ready"
        ],
        "properties": {
          "requestId": {
            "type": "string"
          },
          "presentationId": {
            "type": "string"
          },
          "webUrl": {
            "type": "string",
            "format": "uri"
          },
          "publishStatus": {
            "type": "string"
          },
          "ready": {
            "type": "boolean"
          },
          "visibility": {
            "type": "string"
          },
          "narration": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "PublishNarratedRequest": {
        "type": "object",
        "required": [
          "indexHtml"
        ],
        "description": "`CreatePresentationRequest` plus per-slide narration metadata. The MP3 bytes are NOT in this body: the reply returns one signed `uploadUrls[]` entry per file, you PUT the bytes there, then POST `completeUrl` with the echoed `files`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/CreatePresentationRequest"
          }
        ],
        "properties": {
          "audio": {
            "type": "array",
            "description": "One entry per narrated slide you synthesized. Omit a slide to carry its last-published audio forward when its narration text is unchanged; omit the array to carry every slide forward.",
            "items": {
              "type": "object",
              "required": [
                "words",
                "durationMs",
                "size",
                "hash"
              ],
              "properties": {
                "slideKey": {
                  "type": "string",
                  "description": "The slide's key from the narration plan (the MP3 filename stem). Either this or `slideIndex`."
                },
                "slideIndex": {
                  "type": "integer",
                  "description": "0-based index of the slide in the HTML."
                },
                "words": {
                  "type": "array",
                  "description": "Word timings from the synthesizer, in narration order; must match the slide's narration word for word.",
                  "items": {
                    "type": "object",
                    "required": [
                      "word",
                      "start",
                      "end"
                    ],
                    "properties": {
                      "word": {
                        "type": "string"
                      },
                      "start": {
                        "type": "number",
                        "description": "Seconds."
                      },
                      "end": {
                        "type": "number",
                        "description": "Seconds."
                      }
                    }
                  }
                },
                "durationMs": {
                  "type": "integer",
                  "description": "Length of the MP3."
                },
                "size": {
                  "type": "integer",
                  "description": "Encoded MP3 bytes."
                },
                "hash": {
                  "type": "string",
                  "description": "sha256 of the MP3 bytes; verified after upload."
                },
                "contentType": {
                  "type": "string",
                  "const": "audio/mpeg",
                  "description": "Optional; anything but MP3 is refused."
                }
              }
            }
          },
          "assets": {
            "type": "array",
            "description": "Page assets (fonts, images) as file descriptors; bytes go through `uploadUrls` like the audio.",
            "items": {
              "type": "object",
              "required": [
                "path",
                "size",
                "hash"
              ],
              "properties": {
                "path": {
                  "type": "string",
                  "description": "Bundle path under assets/."
                },
                "size": {
                  "type": "integer"
                },
                "hash": {
                  "type": "string",
                  "description": "sha256 of the bytes."
                }
              }
            }
          }
        }
      },
      "PublishNarratedResult": {
        "type": "object",
        "required": [
          "requestId",
          "presentationId",
          "uploadUrls",
          "completeUrl",
          "files"
        ],
        "properties": {
          "requestId": {
            "type": "string"
          },
          "presentationId": {
            "type": "string"
          },
          "webUrl": {
            "type": "string",
            "format": "uri"
          },
          "uploadUrls": {
            "type": "array",
            "description": "One signed PUT target per file whose bytes the server does not already have.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "completeUrl": {
            "type": "string",
            "format": "uri",
            "description": "POST here with `files` once every upload succeeded."
          },
          "files": {
            "type": "array",
            "description": "Echo back verbatim as the `files` of the complete call.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        }
      },
      "AskRequest": {
        "type": "object",
        "description": "Either the flat form (`query` is a string) or the NLWeb 0.55 structured form (`query` is an object).",
        "properties": {
          "query": {
            "oneOf": [
              {
                "type": "string",
                "maxLength": 200
              },
              {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "site": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "site": {
            "type": "string"
          },
          "prev": {
            "type": "string",
            "description": "Comma-separated previous queries."
          },
          "streaming": {
            "type": "boolean",
            "default": false
          },
          "mode": {
            "type": "string",
            "enum": [
              "list",
              "summarize",
              "generate"
            ],
            "default": "list"
          },
          "query_id": {
            "type": "string"
          },
          "context": {
            "type": "object",
            "properties": {
              "prev": {
                "type": "string"
              }
            }
          },
          "prefer": {
            "type": "object",
            "properties": {
              "streaming": {
                "type": "boolean"
              },
              "mode": {
                "type": "string"
              }
            }
          },
          "meta": {
            "type": "object",
            "properties": {
              "version": {
                "type": "string",
                "const": "0.55"
              }
            }
          }
        }
      },
      "AskItem": {
        "type": "object",
        "required": [
          "url",
          "name",
          "site",
          "score",
          "description",
          "schema_object"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "name": {
            "type": "string"
          },
          "site": {
            "type": "string"
          },
          "score": {
            "type": "number"
          },
          "description": {
            "type": "string"
          },
          "schema_object": {
            "type": "object",
            "additionalProperties": true,
            "description": "A schema.org object: TechArticle for a docs page, VideoObject for a presentation."
          }
        }
      },
      "AskResponse": {
        "type": "object",
        "required": [
          "_meta",
          "query_id",
          "query",
          "results"
        ],
        "properties": {
          "_meta": {
            "type": "object",
            "required": [
              "version",
              "response_type"
            ],
            "properties": {
              "version": {
                "type": "string",
                "const": "0.55"
              },
              "response_type": {
                "type": "string",
                "const": "answer"
              },
              "mode": {
                "type": "string"
              },
              "site": {
                "type": "string"
              }
            }
          },
          "query_id": {
            "type": "string"
          },
          "query": {
            "type": "string"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AskItem"
            }
          }
        }
      },
      "AskError": {
        "type": "object",
        "required": [
          "_meta",
          "error"
        ],
        "properties": {
          "_meta": {
            "type": "object",
            "properties": {
              "version": {
                "type": "string"
              },
              "error": {
                "type": "boolean",
                "const": true
              }
            }
          },
          "error": {
            "type": "string"
          }
        }
      },
      "PresentationContext": {
        "type": "object",
        "required": [
          "requestId",
          "presentationId",
          "title",
          "handle",
          "slug",
          "webUrl",
          "visibility",
          "transcript"
        ],
        "properties": {
          "requestId": {
            "type": "string"
          },
          "presentationId": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "handle": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "webUrl": {
            "type": "string",
            "format": "uri"
          },
          "visibility": {
            "type": "string"
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "transcript": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "slideIndex": {
                  "type": "integer"
                },
                "startSec": {
                  "type": "number"
                },
                "title": {
                  "type": "string"
                },
                "text": {
                  "type": "string",
                  "description": "The slide's narration."
                }
              }
            }
          },
          "transcriptTruncatedSlides": {
            "type": "integer"
          },
          "contextMd": {
            "type": [
              "string",
              "null"
            ]
          },
          "contextTruncated": {
            "type": "boolean"
          }
        }
      }
    }
  }
}