API Reference

Put listening into your product

Your users are reading walls of AI output. Send that text here and get back a summary, spoken audio, or the structure hiding inside it — the same engine the Mac app runs on.

Quick start

  1. Create a key on your developer page. It is shown once — store it somewhere safe.
  2. Send it as a bearer token on every request.
  3. Call an endpoint. You are metered per call at the real provider cost.
curl -X POST https://ideal-newt-976.convex.site/v1/actions \
  -H "Authorization: Bearer wb_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "content": "paste a long agent conversation here" }'

Authentication

Keys begin with wb_live_. Only a hash is stored, so a lost key cannot be recovered — revoke it and mint another.

Authorization: Bearer wb_live_YOUR_KEY

Each key carries scopessummarize, speak, actions, prompt — and a monthly spend cap. A key without the scope for an endpoint gets 403; a key past its cap gets 402.

Keys are for server-to-server use. Anything you ship to a browser or an app bundle is readable, so call the API from your backend and never from client code.

Endpoints

POST/v1/summarizescope: summarize

Summarize

Send text and say how you want it summarized — verbosity, focus, tone, even a language to translate into. The prompt is built server-side, so you get the same summarization the Mac app runs, without writing a system prompt yourself.

FieldTypeRequiredNotes
textstringyesWhat to summarize.
verbositystringauto · brief · balanced · detailed · talkback. Default balanced. auto adapts to input length — a short selection is read back almost verbatim, a long one gets a tight TL;DR. talkback reads the whole text aloud faithfully — every point, nothing condensed — instead of summarizing it.
focusstringoutcome · keyChanges · walkthrough. Default outcome.
tonestringneutral · warm · professional · energetic · calm. Default neutral.
languagestringA language to translate into (english, spanish, french, german, italian, portuguese, hindi, japanese, korean, chinese, arabic, russian, dutch — see GET /v1/prompt-catalog for the live list). Default auto keeps the source language.
expressivestringnone · gemini · cartesia. Default none. Writes performance cues into the summary text itself ([laughs], <emotion value="happy"/>) for voices that can act them out.
sourceAppstringWhere the text came from, e.g. "Xcode" — used for attribution ("Xcode says…"). Truncated to 40 characters.
streambooleanServer-sent events when true. Default false — a non-streaming call returns one buffered JSON response.

Returns. The buffered chat-completions response — `choices[0].message.content` is the summary — when `stream` is false, the default. An SSE stream of `delta` chunks in the same shape when `stream` is true.

curl -X POST https://ideal-newt-976.convex.site/v1/summarize \
  -H "Authorization: Bearer wb_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "<the agent output you want summarized>",
    "verbosity": "brief",
    "focus": "outcome",
    "sourceApp": "IMAI"
  }'

Response

{
  "choices": [
    { "message": { "role": "assistant", "content": "Fixed the retry so uploads no longer stall." } }
  ],
  "usage": { "cost": 0.000114 }
}

Already sending `model` and `messages`? That form still works unchanged — it's the original chat-completions proxy this endpoint was; a body containing `messages` always takes that path, even if it also has `text`. Add "stream": true to either shape for a server-sent-events response instead of one buffered call: data: {"choices":[{"delta":{"content":"..."}}]} lines, ending in data: [DONE]. One thing worth knowing before you rely on these: a misspelled `verbosity`, `focus`, `tone`, or `expressive` value does NOT error — it's silently replaced with that field's default, and an unrecognized `language` name just produces no translation (same as `auto`). Nothing here fails loudly the way `/v1/speak`'s `voice` does (a bad voice key 400s by name) — a typo in any of these fields degrades quietly instead.

POST/v1/speakscope: speak

Speak

Turn text into audio using one of the curated voices. You send text and (optionally) a voice key — the server resolves the model, so you can't be billed for one you didn't ask for.

FieldTypeRequiredNotes
textstringyesThe text to speak. 4000 characters max — over that, 413 text_too_long.
voicestringAny enabled key returned by GET /v1/presets. The service key is billed under its own API pricing and cap; app everyday/premium labels do not restrict API callers. Defaults to the first enabled preset.
speednumber0.5 to 3.0. Default 1. Honored on the Grok voices (eve, leo, ara). Silently ignored on the Gemini voices (aoede, kore, puck, zephyr), including the default — you won't get an error, the audio just comes back at normal speed.
response_formatstring"wav" or "pcm". Only takes effect on the Grok voices, defaulting to wav there. The Gemini voices always return pcm regardless of what you send. The gpt-audio voices ignore this field — the format is whatever OpenRouter returns for that call.

Returns. Raw audio bytes. `X-Audio-Format` tells you what you actually got — wav, pcm, or provider-chosen for the gpt-audio voices — since Content-Type alone isn't reliable enough to build a player around. When the format is pcm, `X-Audio-Sample-Rate` gives the sample rate. The default voice (aoede) always returns pcm: headerless 24 kHz mono 16-bit audio that no player will open until you wrap it.

curl -X POST https://ideal-newt-976.convex.site/v1/speak \
  -H "Authorization: Bearer wb_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Fixed the retry so uploads no longer stall." }' \
  --output reply.pcm

# The default voice (aoede) returns headerless PCM — reply.pcm won't open in anything yet.
# The response headers told you the format: X-Audio-Format: pcm, X-Audio-Sample-Rate: 24000.
# Wrap it into a real, playable file with that:
ffmpeg -f s16le -ar 24000 -ac 1 -i reply.pcm reply.wav

Response

(raw audio bytes)

Response headers:
X-Audio-Format: pcm
X-Audio-Sample-Rate: 24000

Want a file you can --output straight to something playable? Pick a Grok voice instead — { "text": "...", "voice": "eve" } returns a real wav, headers and all, no unwrapping needed. Already sending model and input? That form still works unchanged — a body with model always takes that path, even if it also has text.

POST/v1/actionsscope: actions

Extract actions

Pull the structured items out of a conversation: what to do next, what was decided, and what is still open. Useful for turning a long agent thread into a task list.

FieldTypeRequiredNotes
contentstringyesThe conversation or document.

Returns. Three string arrays. Empty categories come back as [].

curl -X POST https://ideal-newt-976.convex.site/v1/actions \
  -H "Authorization: Bearer wb_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "content": "<the full chat transcript>" }'

Response

{
  "actions": ["Add a retry to the upload step", "Cover it with a unit test"],
  "decisions": ["Cap retries at three attempts"],
  "openQuestions": ["Is a 30s total timeout enough on slow networks?"]
}
POST/v1/promptscope: prompt

Draft a prompt

Turn a conversation plus an optional goal into one complete, self-contained prompt a coding agent can act on, with a one-line gist of what it asks for.

FieldTypeRequiredNotes
contentstringyesThe conversation or document.
goalstringWhat you want to happen next.

Returns. The drafted prompt and a plain-sentence gist.

curl -X POST https://ideal-newt-976.convex.site/v1/prompt \
  -H "Authorization: Bearer wb_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "<the chat so far>",
    "goal": "Make the upload retry configurable"
  }'

Response

{
  "prompt": "In the upload pipeline, make the retry count configurable…",
  "gist": "Asks the agent to make upload retries configurable with a sane default."
}
GET/v1/presetsno key required

List voices

The curated voice lineup, server-managed so it stays current without a client release. No key required — use it to build a voice picker.

Returns. An array of presets: key, name, tagline, engine, model, and the provider's own voice id.

curl https://ideal-newt-976.convex.site/v1/presets

Response

{ "presets": [ { "key": "aoede", "name": "Aoede", "tagline": "Calm and clear", "engine": "gemini-tts", "model": "google/gemini-3.1-flash-tts-preview", "voice": "Aoede" } ] }

Every enabled preset returned here is accepted by POST /v1/speak. The server routes each voice to its declared engine while keeping provider credentials private.

GET/v1/prompt-catalogno key required

Prompt catalog

The exact instruction text POST /v1/summarize composes its system prompt from — the same catalog the Mac app bundles and refreshes. No key required: it's instruction text, not a secret, and the app needs it before sign-in.

Returns. The full catalog: instruction templates, and instruction text keyed by each verbosity/focus/tone value, plus the language label map and the per-verbosity token ceilings.

curl https://ideal-newt-976.convex.site/v1/prompt-catalog

Response

{
  "version": 1,
  "templates": {
    "cloud": "You are a voice assistant. The user selected some text…",
    "cloudTalkback": "You are narrating someone's text to them out loud…",
    "local": "{preamble}\n\nWrite for the ear…",
    "localTalkback": "{preamble}\n\nKeep EVERY point, detail, number…",
    "localPreamble": "You are a text-to-speech rewriting tool…",
    "attributionCloud": "The text was selected in {app}…",
    "attributionLocal": "The passage was copied from {app}…",
    "language": "\n\nLANGUAGE: Write the ENTIRE spoken response in {language}…",
    "expressiveGemini": "\n\nEXPRESSIVE DELIVERY — this OVERRIDES the \"no symbols\" rule…",
    "expressiveCartesia": "\n\nEXPRESSIVE DELIVERY — this OVERRIDES the \"no symbols\" rule…"
  },
  "verbosity": { "brief": "Give a very brief summary: 1-3 sentences, only the essential outcome.", "balanced": "…", "detailed": "…", "talkback": "Read the entire text aloud, faithfully, in natural spoken language." },
  "autoLength": { "shortUnder": 40, "longOver": 300, "short": "…", "medium": "…", "long": "…" },
  "focus": { "outcome": "Focus only on the final outcome/result — what I now have or what changed for me.", "keyChanges": "…", "walkthrough": "…" },
  "tone": { "neutral": "Use a plain, neutral tone.", "warm": "…", "professional": "…", "energetic": "…", "calm": "…" },
  "languages": { "english": "English", "spanish": "Spanish", "…": "11 more" },
  "maxTokens": { "brief": 180, "balanced": 400, "detailed": 800, "talkback": 4000 }
}

version bumps whenever any string in the catalog changes, so a long-lived client can tell a stale cached copy from a current one rather than comparing string contents.

Models

Endpoints that take a model accept only this list. Anything else is rejected with 400 rather than silently substituted.

ModelUse
google/gemini-2.5-flash-liteSummaries — cheapest tier
google/gemini-3.1-flash-liteSummaries
google/gemini-3.5-flashSummaries — highest quality
google/gemini-3.1-flash-tts-previewSpeech
openai/gpt-audio-miniOne-shot spoken summary
x-ai/grok-voice-tts-1.0Speech — fast and cheap

Errors

Every failure returns the same shape, so one handler covers all of them: { "error": { "message", "code" } }

StatusCodeMeaning
401invalid_keyThe key is missing, malformed, or revoked.
403forbiddenThe key is valid but lacks the scope for this endpoint.
402quota_exhaustedThe key hit its monthly spend cap.
402credits_exhaustedThe account is out of prepaid credits.
400bad_requestMalformed JSON, or a required field is missing.
413text_too_long`text` on /v1/speak is over the 4000-character cap.
400unknown_voice`voice` on /v1/speak doesn't match a key from GET /v1/presets.
502upstreamA model provider failed. Safe to retry.

Retry 502 with backoff — it means a model provider failed, not that your request was wrong. The others will fail identically until you change something.