🔌 API & Developers

Build on the assistant

Put it on your site with one script tag, start conversations from your own front end, and have what happens pushed into your systems.

Start here

One header, one base URL. If you can run curl you can be finished with this page in five minutes.

Create a key in Settings → API keys. Choose the scopes it needs and nothing more; a key can be restricted to a single workspace, given an expiry, and revoked at any time. The full key is shown once, at creation — we store only a hash, so there is no way to recover it later and no way for anyone here to read it.

Send it as a Bearer token. Every request is metered and auditable against the key that made it.

curl https://YOUR-HOST/api/v1/assistants \
  -H "Authorization: Bearer sk_live_..."

Feed readers that cannot set a header may pass the key as a query parameter instead. Use it only for the published RSS feed — a key in a URL ends up in logs, proxies and browser history.

curl "https://YOUR-HOST/api/v1/feeds/published.rss?api_key=sk_live_..."

Scopes

A key carries exactly the scopes you ticked. A call without the scope it needs is refused with 403 and the name of the missing scope, so the fix is never a guess.

  • content:readRead content tasks and their output
  • content:writeCreate content tasks and approve gates
  • media:readRead media tasks and renditions
  • media:writeGenerate and edit media
  • assets:readRead the asset library
  • assets:writeUpload and manage assets
  • publish:readRead publishing state and scheduled posts
  • publish:writePublish and schedule posts
  • analytics:readRead content performance metrics
  • webhooks:manageCreate and manage outbound webhooks
  • workspace:readRead workspace settings and members
  • organization:readRead organization details
  • billing:readRead subscription and Credit balance
  • realtime:sessionStart live assistant sessions from your own front end
  • realtime:readRead live assistant sessions and their transcripts

Starting a conversation and reading one back are deliberately separate scopes, because one of them spends money and the other does not. A dashboard that displays transcripts should not be able to open a line.

Put the assistant on your own site

The shortest path: one script tag, no API key, no backend.

Create an embed key in the assistant panel and paste one line into your page. It draws a button; clicking it opens the conversation.

<script src="https://YOUR-HOST/api/v1/realtime/embed/widget.js?key=EMBED_KEY"
        async></script>

The button's label, colour and side are properties of the key, so they are changed in our panel rather than by editing your site — but a query parameter still wins where you give one, because running the same assistant behind two different buttons is a real thing to want.

<script src="https://YOUR-HOST/api/v1/realtime/embed/widget.js?key=EMBED_KEY&label=Ask%20us&color=%234f46e5&position=left"
        async></script>

The conversation opens in an iframe on our origin, not as a bundle inside your page. That is deliberate: it needs a WebRTC stack and a microphone permission, and asking your visitors to grant microphone access to your domain for code you did not write is both worse for them and the first thing a strict Content-Security-Policy breaks.

An embed key is public by design — it is in your page source, and anyone who reads it can start a conversation on your Credits. It is not an API key and carries no scopes. What bounds it is its ceilings: minutes per session, sessions per day, concurrent sessions, which domains may frame it, an expiry, a total number of uses, opening hours, an address range and a passcode. All are off until you set them.

Build your own front end

When the widget is not the interface you want, start the session yourself and render it with LiveKit's own SDK.

Ask for a session and you get a server URL and a room token back. From there use LiveKit's JavaScript, iOS, Android or Flutter SDK. We ship no client library of our own, because LiveKit already publishes one and a wrapper around it would only age.

curl -X POST https://YOUR-HOST/api/v1/realtime/sessions \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"identity": "customer-4821", "assistantId": "asst_..."}'

The workspace is taken from the key when the key is restricted to one, and is otherwise the organization's default — or whichever you name in the body. An assistant id belonging to another tenant does not resolve; it falls back to the default rather than answering as somebody else.

POST/api/v1/realtime/sessionsOpen a session and get a LiveKit URL and token
GET/api/v1/realtime/sessionsList sessions
GET/api/v1/realtime/sessions/:id/transcriptRead what was said, and every tool call
DELETE/api/v1/realtime/sessions/:idEnd a session and settle what it used
GET/api/v1/assistantsList the assistants in the workspace
GET/api/v1/assistants/:idOne assistant, with its persona and limits

Whichever way in was used — the widget, this API, or your own backend acting for your users — the session record, the transcript, the assistant's knowledge and the ceilings are identical.

Content, publishing and metrics

The content pipeline and the publishing calendar are reachable from the same key.

GET/api/v1/content/tasksList content tasks
POST/api/v1/content/tasksStart one
GET/api/v1/content/tasks/:idOne task and its state
DELETE/api/v1/content/tasks/:idCancel it
POST/api/v1/content/tasks/:id/topics/approveApprove the topic gate
POST/api/v1/content/tasks/:id/drafts/approveApprove the draft gate
POST/api/v1/content/packages/:id/approveApprove a finished post
GET/api/v1/publish/tasksWhat is scheduled and what shipped
POST/api/v1/publish/tasksPublish or schedule
GET/api/v1/analytics/tasks/:idMetrics for one task
GET/api/v1/feeds/published.rssEverything published, as RSS

Approving a gate through the API calls the same service function the interface does, so the same rules, the same audit record and the same events apply. There is no back door that skips a review.

Webhooks

Rather than polling: we POST to your endpoint when something happens, signed, with retries.

Register an endpoint and pick the events you want, or subscribe to everything with a single asterisk. Each delivery carries the event id, its type, a timestamp and a signature.

X-Wcolo-Event-Id:    <uuid>
X-Wcolo-Event-Type:  service.handover.requested
X-Wcolo-Timestamp:   1758412800
X-Wcolo-Signature:   v1=<hex>

Verify before you trust it. The signature is an HMAC-SHA256 over the timestamp, a dot, and the exact request body, keyed with your endpoint's signing secret. Compare the bytes you received, not a re-serialised copy of them, and reject a timestamp that is far from now.

const mac = crypto.createHmac("sha256", SIGNING_SECRET);
mac.update(`${headers["x-wcolo-timestamp"]}.${rawBody}`);
const expected = `v1=${mac.digest("hex")}`;
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(headers["x-wcolo-signature"]));

The body is the same shape for every event: an id, a type, a version, the organization and workspace it belongs to, when it happened, and a data object that differs per type.

{
  "id": "evt_...",
  "type": "realtime.session.ended",
  "version": 1,
  "organizationId": "...",
  "workspaceId": "...",
  "occurredAt": "2026-09-21T10:04:11.000Z",
  "data": { }
}

Answer 2xx quickly and do your work afterwards; a delivery times out after ten seconds and is retried. Deliveries can arrive more than once, so key your handler on the event id.

  • content.task.completed
  • content.task.failed
  • content.topics.ready
  • content.draft.ready
  • content.package.ready
  • content.package.delivered
  • content.package.imported
  • content.media.decision_needed
  • content.media.review_needed
  • content.gate.auto_approved
  • media.task.ready
  • media.task.failed
  • media.plan.ready
  • media.project.rendered
  • media.rendition.completed
  • publish.task.created
  • publish.post.published
  • publish.task.failed
  • publish.cloud.halted
  • engage.message.received
  • engage.reply.sent
  • realtime.session.started
  • realtime.session.ended
  • realtime.proposal.created
  • realtime.service.hired
  • service.handover.requested
  • analytics.snapshot.updated
  • credits.low
  • payment.settled

Two events are deliberately not offered. Every inbound customer message would be a firehose rather than a signal, and consent is a legal state we act on ourselves — a system acting on it independently is how somebody gets messaged after they said stop.

Errors, limits and usage

What comes back when something is wrong, and how to see what a key has been doing.

  • 401The key is missing, malformed, expired or revoked.
  • 403The key is valid but lacks the scope, or the record belongs to another tenant. The message names the missing scope.
  • 404No such record in this organization. A record that exists elsewhere is a 404 here, never a 403 — the difference would tell you it exists.
  • 429Rate limited. Back off and retry.
  • 5xxOurs. Safe to retry with the same idempotency, which is why cost-bearing operations settle rather than double-charge.

Settings → API keys shows what each key has done: requests, errors, how many were rate limited, and when it was last used. A key that has not been used in months is one to revoke.