4 / 6 · Build on Bisque · 11 min read

Bisque API

The REST API the /present skill calls, callable without the skill.

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. The same tools are available over MCP — see the MCP page. Agents reading this site start at /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.

curl -s "https://bisque.today/api/presentations/context?url=https://bisque.today/p/siderakis/claude-system-card"
{
  "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, 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.

Publish

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

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

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

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"}')"
{
  "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.

EndpointWhat it does
GET /api/presentations/specThe html-presentation/v1 format, as markdown. Public.
GET /api/presentations/contextA presentation as text. Public for public and unlisted presentations.
POST /api/presentations/contextUp to 20 presentations as text in one request.
POST /api/presentations/createPublish; Bisque narrates. Returns 202 and a status URL. dryRun: true = sandbox.
GET /api/presentations/statusNarration and publish progress by presentationId.
POST /api/presentations/publish-narratedPublish with audio you synthesized locally (what /present does). API keys only.
GET /api/oembedoEmbed for a watch URL.
POST /mcpThe presentations MCP server.
POST /docs/mcpThe 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/....

EndpointWhat it does
POST /v1/presentationsStarts 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}:finalizeEnds 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:

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"}')"
{
  "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:

curl -s https://bisque.today/v1/presentations/office-hours-3f9a1c \
  -H "Authorization: Bearer $BISQUE_API_KEY"
{
  "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:

curl -s "https://bisque.today/v1/presentations/office-hours-3f9a1c/surveys/scope/response?wait=25" \
  -H "Authorization: Bearer $BISQUE_API_KEY"
{
  "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.

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.

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"]}'
{
  "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.

npm install @bisquecloud/sdk
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.

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:

{
  "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 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.