Every API here is available over the APIs.io API and to AI agents over MCP.
{
"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
# --- truncated at 32 KB (116 KB total) ---
# Full source: https://artifacts.apis.io/bisque-cloud/openapi/_original/bisque-cloud-api-openapi.json