---
title: "Bisque API"
description: "Publish and read presentations from any program; OpenAPI at /openapi.json."
canonical: https://bisque.cloud/docs/api
---

# API

Publish and read presentations from any program; OpenAPI at /openapi.json.

Page: https://bisque.cloud/docs/api

Publish a presentation from any program, and read any shared presentation
back as text. The API is what the `/present` skill calls; you can call it
without the skill. Two hosts serve it, `https://bisque.today` and
`https://bisque.cloud`, and every path on this page works on both.

The whole surface is described in OpenAPI 3.1 at
[`/openapi.json`](/openapi.json). The same tools are available over MCP —
see [the MCP page](/docs/mcp). Agents reading this site start at
[`/llms.txt`](/llms.txt).

## Read a presentation, no sign-in

`GET /api/presentations/context` returns a presentation's title, its
narration transcript slide by slide with timestamps, and the `context.md`
its author shipped for agents. Public and unlisted presentations need no
credential.

```bash
curl -s "https://bisque.today/api/presentations/context?url=https://bisque.today/p/siderakis/claude-system-card"
```

```json
{
  "requestId": "…",
  "presentationId": "…",
  "title": "…",
  "handle": "siderakis",
  "slug": "claude-system-card",
  "webUrl": "https://bisque.today/p/siderakis/claude-system-card",
  "visibility": "public",
  "transcript": [{ "slideIndex": 0, "startSec": 0, "title": "…", "text": "…" }],
  "contextMd": "…"
}
```

Anonymous callers get 60 requests per 10 minutes per IP; send a credential
for 300, and to read your own private presentations.

## Get a credential

Every write needs `Authorization: Bearer <credential>`.

- **API key** (`bisque_live_…`) — for scripts, CLIs, and coding agents on
  your machine. Create one at
  [bisque.cloud/setup/keys](https://bisque.cloud/setup/keys), or run
  `bisque login` and approve the pairing code in the browser.
- **OAuth** (`bisque_oat_…`) — for hosted chat products where nobody can
  paste a key. Dynamic client registration, PKCE, refresh, revocation.

The step-by-step for agents, in the order they run it, is
[`/auth.md`](/auth.md).

## Publish

Fetch the authoring format first — it is served live and never written
from memory:

```bash
curl -s https://bisque.today/api/presentations/spec
```

Write one HTML file in that format, then post it:

```bash
curl -s -X POST https://bisque.today/api/presentations/create \
  -H "Authorization: Bearer $BISQUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html index.html '{indexHtml: $html, slug: "my-first-presentation", visibility: "unlisted"}')"
```

```json
{
  "requestId": "…",
  "presentationId": "…",
  "webUrl": "https://bisque.today/p/you/my-first-presentation",
  "status": "queued",
  "statusUrl": "https://bisque.today/api/presentations/status?presentationId=…"
}
```

The reply is immediate. Narration runs in the background; poll `statusUrl`
until `ready` is true. Post again with the same `slug` to republish.

| Endpoint                                   | What it does                                                                      |
| ------------------------------------------ | --------------------------------------------------------------------------------- |
| `GET /api/presentations/spec`              | The `html-presentation/v1` format, as markdown. Public.                           |
| `GET /api/presentations/context`           | A presentation as text. Public for public and unlisted presentations.             |
| `POST /api/presentations/context`          | Up to 20 presentations as text in one request.                                    |
| `POST /api/presentations/create`           | Publish; Bisque narrates. Returns 202 and a status URL. `dryRun: true` = sandbox. |
| `GET /api/presentations/status`            | Narration and publish progress by `presentationId`.                               |
| `POST /api/presentations/publish-narrated` | Publish with audio you synthesized locally (what `/present` does). API keys only. |
| `GET /api/oembed`                          | oEmbed for a watch URL.                                                           |
| `POST /mcp`                                | The presentations MCP server.                                                     |
| `POST /docs/mcp`                           | The documentation MCP server. No credential.                                      |

## Add slides while someone watches, and wait for their answer

A live presentation is one you keep adding slides to while a person watches
it. The slides appear in their player in place: nothing reloads, and they
stay on the slide they are on. A slide can carry a survey, and you can wait
for the answer the same way you wait for a CI run. These routes take an API
key and answer at the root, `https://bisque.today/v1/presentations/...`.

| Endpoint                                                       | What it does                                                                    |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `POST /v1/presentations`                                       | Starts a creation. Send the first slides to keep it open for more slides.       |
| `GET /v1/presentations/{id}`                                   | The live creation as it stands, to resume it from a new session.                |
| `PATCH /v1/presentations/{id}`                                 | Appends slides to a live creation.                                              |
| `PATCH /v1/presentations/{id}/slides/{slideKey}`               | Gives one slide its narration, after you PUT the MP3 to its upload URL.         |
| `POST /v1/presentations/{id}:finalize`                         | Ends the creation. The presentation becomes an ordinary one and `live` is gone. |
| `GET /v1/presentations/{id}/surveys/{surveyId}/response?wait=` | Your answer to one survey, held open up to `wait` seconds for it.               |

Start one by posting the first slides, exactly as any creation starts:

```bash
curl -s -X POST https://bisque.today/v1/presentations \
  -H "Authorization: Bearer $BISQUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html index.html '{indexHtml: $html, slug: "office-hours", visibility: "private"}')"
```

```json
{
  "presentationId": "office-hours-3f9a1c",
  "creationId": "…",
  "webUrl": "https://bisque.today/p/you/office-hours",
  "viewable": true,
  "live": { "revision": 1 },
  "needsAudio": ["slide-00", "slide-01"],
  "reused": [],
  "carriedAssets": [],
  "assetUploads": [
    { "path": "assets/logo.png", "url": "…", "method": "PUT", "headers": {} }
  ],
  "audioUploads": [
    {
      "slideKey": "slide-00",
      "path": "audio/slide-00.mp3",
      "url": "…",
      "method": "PUT",
      "headers": {}
    }
  ]
}
```

`viewable: true` means the slides are on the watch page now, silent until
their narration arrives. `live` is only in the reply when you asked for it.

To add slides, send the whole presentation again to
`PATCH /v1/presentations/{id}`, with the new slides after the ones you
already sent. The body is the same as the create body. Leave out `slug`,
`handle`, `org` and `visibility` and the ones the creation started with are
used; send a different one and you get a 409 `LIVE_TARGET_CHANGED`. The reply
has the same shape as the create reply, with `live.revision` one higher. Only
slides that still need narration are in `needsAudio`: a slide whose narration
text did not change keeps its audio.

A live presentation only grows. Every narrated slide you already sent has to
come back first, in the same order. You may change a slide's narration text,
which puts that slide back in `needsAudio`. Removing, reordering or inserting
a slide before another is a 400 `LIVE_APPEND_ONLY`, and the message names the
first slide that moved. Other errors: 404 `NO_OPEN_CREATION` when nothing is
open for that id, and 409 `CREATION_SUPERSEDED` when another publish of the same
presentation replaced it.

Send `baseRevision`, the `live.revision` your slides were written against,
and an append from anything that fell behind is refused instead of landing
over slides it never saw. Slide keys are positions (`slide-05`), so two
sessions that each add "the next slide" would otherwise both pass the
append-only check, and the second would replace the first. Appends and narration uploads
take turns: one that arrives while another is being written waits for it, up
to 20 seconds, and is a 409 `LIVE_APPEND_IN_PROGRESS` if it is still going. A
stale revision is a 409 `LIVE_REVISION_CONFLICT`. Either way nothing was
written. Resume, then append after what is there. An append with no
`baseRevision` still waits its turn, but is not checked for staleness.
Assets an append leaves out are kept, so the slides already published keep
theirs.

While it is live, the presentation document carries
`live: { revision, startedAt, agentSeenAt }`. The revision goes up by one on
every append, and the player fetches the slides again each time it rises.
`agentSeenAt` is when an agent last appended, sent narration or waited on an
answer, and reading the presentation to resume it leaves it alone; the watch
page says the presentation is waiting for the agent to come back when it is
more than five minutes old. Finalizing removes the field.

### Resume a live presentation from a new session

A live presentation outlives the session that opened it. When that session
ends before finalizing, because the agent's harness exited or lost its login,
the creation stays open under its id. Any session of the same account picks
it up by reading it:

```bash
curl -s https://bisque.today/v1/presentations/office-hours-3f9a1c \
  -H "Authorization: Bearer $BISQUE_API_KEY"
```

```json
{
  "presentationId": "office-hours-3f9a1c",
  "webUrl": "https://bisque.today/p/you/office-hours",
  "live": { "revision": 3, "startedAt": "…", "agentSeenAt": "…" },
  "voice": { "voiceId": "kokoro:af_heart", "speechSpeed": 1 },
  "slides": [{ "slideKey": "slide-00", "audio": "ready" }],
  "indexHtml": "<!doctype html>…",
  "assets": ["assets/logo.png"],
  "surveys": [
    {
      "surveyId": "scope",
      "complete": true,
      "answers": { "mode": "expansion" }
    }
  ]
}
```

`indexHtml` is every slide published so far: append after it, and send
`live.revision` back as `baseRevision`. `surveys` is your own answer to each
question so far, so a question already answered is not asked again. Narrate new
slides with the same voice and speed, or the earlier slides' audio goes stale.
Reading changes nothing, so any number of sessions may resume at once; the
revision decides whose append lands. A presentation you cannot change is a
404 `PRESENTATION_NOT_FOUND`, a finalized one is a 404 `NO_OPEN_CREATION`,
and one another publish replaced is a 409. The present skill wraps this as
`resume --presentation <id> --dir <dir>`, and takes the watch URL in place of
the id for a presentation started without one.

To wait for an answer, ask for your own response to the survey and say how
long the server may hold the request:

```bash
curl -s "https://bisque.today/v1/presentations/office-hours-3f9a1c/surveys/scope/response?wait=25" \
  -H "Authorization: Bearer $BISQUE_API_KEY"
```

```json
{
  "presentationId": "office-hours-3f9a1c",
  "surveyId": "scope",
  "complete": true,
  "answers": { "mode": "expansion", "notes": "Start with the team plan" },
  "updatedAt": "2026-09-25T18:04:11.000Z"
}
```

`wait` is in seconds, from 0 to 25, and defaults to 0, which answers at
once. The server returns the moment every required question has an answer.
If the time runs out first you get a 200 with `complete: false` and no
`answers`, so call again. The answer read back is the one you gave, signed
in, on the watch page: a live presentation has one respondent, the account
that owns it. An answer given to different wording of the question does not
count until it is given again. A presentation you cannot read, or a survey id
it does not have, is a 404.

## Sandbox

Add `dryRun: true` to the create body and the same checks run — the
format gate, slug and org conflicts, the tier cap, the credit preflight —
against your real account, and nothing is written. The reply is a `200`
with `status: "validated"`, the `webUrl` a real create would publish at,
and the narration counts, so an integration can be tested end to end
without a presentation appearing on the channel. The `create_presentation`
MCP tool takes the same flag. Reads never need a sandbox: the context
endpoint below touches nothing and needs no credential.

```bash
curl -s -X POST https://bisque.today/api/presentations/create \
  -H "Authorization: Bearer $BISQUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html index.html '{indexHtml: $html, slug: "smoke-test", dryRun: true}')"
```

## Read many at once

`POST /api/presentations/context` (or its alias
`/api/presentations/context/batch`) reads up to 20 presentations in one
request. Send `{"urls": [...]}`, `{"items": [{"url"} | {"handle","slug"}]}`,
or a bare JSON array of watch URLs. Each item resolves on its own — a bad
address fails its row, not the batch — and each costs one slot of the same
per-IP window as a GET.

```bash
curl -s -X POST https://bisque.today/api/presentations/context \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://bisque.today/p/siderakis/claude-system-card", "https://bisque.today/p/siderakis/bun-1-3"]}'
```

```json
{
  "requestId": "…",
  "items": [
    {
      "ok": true,
      "input": { "url": "…" },
      "presentation": { "presentationId": "…", "transcript": [] }
    },
    {
      "ok": false,
      "input": { "url": "…" },
      "error": { "code": "PRESENTATION_NOT_FOUND", "message": "…" }
    }
  ]
}
```

## List your presentations

`GET /api/presentations` returns your presentations newest first, 25 per
page (`limit` up to 100). Pass the `nextCursor` from one page as `cursor`
on the next; `hasMore` is false on the last page. Filter with
`visibility=public|unlisted|private|org`.

## SDK

`@bisquecloud/sdk` wraps every endpoint on this page in TypeScript, with typed
results, `BisqueError` for failures, an `Idempotency-Key` on every POST, and
retries that honour `Retry-After`. Zero dependencies; Node 18+, Bun, Deno.

```sh
npm install @bisquecloud/sdk
```

```ts
import { Bisque } from "@bisquecloud/sdk";

const bisque = new Bisque({ apiKey: process.env.BISQUE_API_KEY });
const spec = await bisque.presentations.spec();
const created = await bisque.presentations.create({ indexHtml });
const status = await bisque.presentations.waitUntilReady(
  created.presentationId,
);
console.log(status.webUrl);
```

Source: [github.com/bisque-cloud/presenter/tree/main/sdk](https://github.com/bisque-cloud/presenter/tree/main/sdk).

## Retries and idempotency

Send an `Idempotency-Key` header (a UUID) on every POST. If the network
drops after the request went through, retry with the same key and body and
you get the first response back, marked `Idempotent-Replayed: true`, instead
of a second presentation. The same key with a different body is a 422
`IDEMPOTENCY_KEY_REUSED`. Keys last 24 hours.

## Rate limits

Every response from `/api/presentations/context` carries `RateLimit-Limit`,
`RateLimit-Remaining`, `RateLimit-Reset` (seconds) and `RateLimit-Policy`
(`60;w=600` for anonymous callers, `300;w=600` with a credential). A 429
adds `Retry-After`. Publishing endpoints have no fixed limit.

## Versioning and deprecation

This is version 1 of the presentations API, and the version is in the URL:
every endpoint on this page also answers at `/api/v1/...` — for example
`/api/v1/presentations/create` — serving exactly what the unversioned path
serves. The versioned address is the durable one to integrate against; the
unversioned `/api/...` form stays as an alias. (The agent tool API is
`/v1/...` at the root, a separate surface.) Within a version fields are only
added, never removed or retyped. A breaking change ships
under a new path prefix (`/api/v2/...`) while v1 keeps serving; a retired
prefix keeps serving for at least 90 days, and
during that time its responses carry `Deprecation` and `Sunset` headers
(RFC 9745 and RFC 8594) with the dates, plus a `Link` header with
`rel="successor-version"` naming the new path. The OpenAPI document's
`info.x-versioning` and `info.x-lifecycle` state the same policy for
machines; `x-lifecycle.deprecated` lists every path currently in its
notice period (none today).

## Errors

Every error is JSON with a stable code, a message for people, and a
`requestId` to quote:

```json
{
  "requestId": "…",
  "error": {
    "code": "INVALID_PRESENTATION_ID",
    "message": "presentationId query parameter is required."
  }
}
```

`401` means the credential is missing, expired, or revoked; `429` on the
context endpoint means the per-IP limit; a `404` from any `/api` path that
does not exist is also JSON, with a `hint` pointing at this page.

## Where it fits

The [`/present`](/docs/present) skill wraps this API: it writes the HTML,
narrates locally, and calls `publish-narrated`. Reach for the API directly
when you are building your own tool, or a pipeline that publishes without an
agent in the loop.
