Developer API

API reference.

Every endpoint, field, status and error — generated from the schemas the server validates with, so it cannot describe an API the server does not run. New here? Start with the quickstart.

Base URL https://api.slyos.world · version v1

QuickstartOpenAPI 3.1 document (JSON)Reference adapter — one file, no dependencies

Turn any inbox you own — a helpdesk, a ticketing system, your own app’s chat — into a SlyOS channel. Send messages in; SlyOS writes the replies with the account’s own Brain: what the business has taught it, the voice it has learned, and the conversation so far. You send each reply through your own system.

Authentication

Every request sends Authorization: Bearer <key>. Keys starting sly_test_ are the sandbox: free, and they reach nobody. Keys starting sly_live_ are live. A sandbox key creates and lists sandbox channels; a live key, live ones — and neither can reach the other's messages, cards or replies: an id from the other environment is a 404. Sandbox drafts are free and limited to 50 in any 24 hours. Each operation names the scope its key must carry, and a key without it is refused 403 missing_scope. A missing, malformed or revoked key is refused 401 unauthorized, always with the same sentence.

What costs money

Only POST /v1/developer/drafts, only on a live key, and only when a reply is produced:

  • From 2026-09-01: 10¢ per AI response (price version 2026-09-01)

Taking messages in, reading replies back, reporting deliveries, usage and wallet lookups are free. So is everything in the sandbox. A reply that is refused, fails, or is judged unnecessary is not charged.

Retrying safely

  • POST /v1/developer/messages: the same external_message_id on the same channel is stored once; the repeat answers 200 with duplicate: true.
  • POST /v1/developer/drafts: generation_id is the idempotency key for the charge, and it is yours: unique within your account and the key’s environment. Derive it from the message_id SlyOS returned — <message_id>:reply-1 — so it is unique and the same on a retry. Repeating a generation_id that already has a reply gets that reply back with 200 and replayed: true, charged nothing (billed_cents: 0; on a live key, already_charged: true). While the first request is still being drafted, the repeat answers 409 generation_in_progress with Retry-After: retry with the same id. The same id sent for a different conversation is refused 409 generation_id_conflict, never answered with the first conversation’s reply.
  • POST /v1/developer/deliveries: send your own external_message_id, and a repeated report is one message.

Teach SlyOS your business

A reply is only as good as what SlyOS knows about the business it speaks for. Before the first one, hand it the material your answers must agree with — every reply is then given your rules and the passages nearest the message, and written in the voice SlyOS has learned for the account:

  • Links and whole sites. POST /v1/developer/knowledge/sources with {"url": "…"} reads one page — your pricing, your policies, a regulation your answers must respect. Add "crawl": true to read the whole site.
  • Files. The same call as multipart/form-data, with the file in a file field: PDF, Word, plain text, Markdown or CSV. It is read to the end.
  • Rules. "rules": true (a rules=true form field, for a file) marks a source replies must follow rather than draw on: terms, a returns policy, a regulation. Every reply carries the rules whole while they fit in one reply; past that, each reply gets the rules nearest its message, and the source says rules_fit: false.
  • Who you are. PUT /v1/developer/knowledge/profile says what the business is and how it wants to sound.

Sources are read in the background: status goes queued, processing, then ready — or failed, with status_reason saying why. A web page is read without running its scripts, so a page that builds its content with JavaScript (prices drawn from a script, a template filled in after it loads) can be missing what a visitor sees: the source is then ready with a warning naming those pages in incomplete_pages — add that text as a file, or as a single link to a page that shows it. Adding and changing them needs a live key carrying brain:write — sandbox and live replies share them, so a sandbox key may only read them; listing them, brain:read. GET /v1/developer/knowledge/sources/{sourceId}/passages shows exactly what was read. Replies use what you add, the conversation they answer and that customer’s earlier ones — never another customer’s, and never a sandbox conversation in a live reply. They also use everything SlyOS knows about the account owner: facts from their own conversations and the ones they told it, their past replies on their other channels, their documents, calendar and saved details, and their About you. The owner can turn that off on the Developer screen (use_personal_context); a key cannot change it. Off, a reply still uses the owner’s name, rules and writing style. Sources added with a key belong to its project unless the request sends "scope": "account". When sources disagree. Each reply is given the passages nearest the message. The closest always goes in. The best passage of each other source about as relevant goes in ahead of the rest, in up to half of what a reply carries, so a long website cannot crowd out a short policy; after that, no one source takes more than half while another has something about as relevant. Where two disagree, the reply follows the higher one — rules, then uploaded files, then single links, then pages read from a whole site — and never contradicts the business’s own documents. That order settles disagreements only: a reply answers from the passage that answers the question, and says what another organisation’s page says as theirs. Every reply then says what it relied on: sources, on the POST /v1/developer/drafts answer and on GET /v1/developer/drafts/{draftId}, lists the passages it was given — rules first — with the source, the page range or section, an excerpt of the passage’s own text, and a SHA-256 of the stored passage (with carried_chars when only its start fit).

Held replies

A reply with held_for_review: true was held by SlyOS’s checks for a person to read first — money, health or dosing, a legal or data-protection matter, another sensitive subject, or something the reply gives out that the account has not sent before — and hold_reasons says which. Do not send it automatically. null means SlyOS can no longer say, and is held; so is a missing field. Every answer that carries a reply carries the hold: the first POST /v1/developer/drafts answer, a replay of it, GET /v1/developer/drafts/{draftId}, and the draft.ready webhook.

Errors

Every refusal has one shape, whatever the status: { "code": "…", "message": "…" }. Branch on code; show message to a person. A 402 insufficient_funds also carries available_cents and needed_cents; a 429 sandbox_daily_limit carries used_today and daily_limit. Each operation lists its codes under the status it answers them with.

  • 400 — the request is malformed. Fix it; retrying the same request will not help.
  • 401 — the key is not valid. 403 — the key is valid but lacks the scope.
  • 402 — out of API credit. Not an outage: the message is stored and can be answered by hand.
  • 404 — no such object for this key.
  • 409 — a settled state, not a fault (for example, somebody asked whether they are talking to a person). Do not retry — except generation_in_progress, not_stored, video_busy, video_no_takes_ready, which the operations that answer them say are safe to retry with the same request.
  • 429 — over the rate limit, on an operation that lists rate_limited. Wait and retry.
  • 503 — SlyOS cannot reach a model provider. Retry with backoff; nothing was charged.

Limits

Each operation states its rate limit in requests a minute; one that lists 429 rate_limited refuses past it. A message body is at most 32,000 characters.

Webhooks

SlyOS can post events to an endpoint the account owner sets on the Developer screen — one for sandbox and one for live, each with its own signing secret; a live endpoint must be HTTPS (see webhooks). Every delivery is signed in the SlyOS-Signature header; verify it before trusting the body. Delivery is at least once: an endpoint that does not answer 2xx within ten seconds is tried again, up to 8 attempts over about 62 minutes, under the same event id — deduplicate on it. A redirect is not followed: it counts as a failed attempt.

Versioning

The version is in the path (/v1). Within a version, responses may gain fields: ignore fields you do not recognise.

Messages

POST/v1/developer/messages

Send a message in

Take a message in from a custom channel. Free, always — including when the wallet is empty, so a ticket is never lost for want of credit.

Scope messages:writeFree1,200 requests a minute

Retry safely: the same external_message_id on the same channel is stored once.

curl -X POST "https://api.slyos.world/v1/developer/messages" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "channel_id": "3f6c2b1e-8a4d-4c7f-9e2b-5d1a7c9e0f42",
  "external_thread_id": "ticket-48213",
  "external_message_id": "ticket-48213-comment-2",
  "body": "Do you ship to Canada, and how long does delivery take?",
  "sender_identifier": "dana@example.com",
  "sender_display_name": "Dana Whitfield",
  "sent_at": "2026-10-09T15:42:07Z"
}'

Request body

channel_idstring (uuid) · required
The custom channel the message arrived on: an id from GET /v1/developer/channels. It must belong to the same environment as the key.
external_thread_idstring · required
Your id for the conversation — a ticket, a chat. Messages that share one are one conversation in SlyOS.1–512 characters
external_message_idstring · required
Your id for this message. Sending the same one on the same channel again stores nothing new and answers 200 with duplicate: true, so a retry is always safe.1–512 characters
bodystring · required
The message text, as the sender wrote it. At most 32,000 characters.1–32,000 characters
sent_atstring (date-time)
When it was sent: ISO 8601 in UTC, ending in Z. An offset such as +01:00 is refused. Defaults to when SlyOS received it.
sender_identifierstring
Who sent it, as your system knows them — an email address, a phone number or a handle. SlyOS uses it to recognise the same person across messages. Leave it out rather than send it blank.at most 512 characters
sender_display_namestring
The sender’s name, as it should be shown.at most 256 characters
subjectstring
A subject line, for channels that have one.at most 512 characters
thread_titlestring
A title for the conversation, shown in the SlyOS inbox.at most 512 characters
directionstring
inbound (the default) for a message your customer sent. outbound for a reply your side sent, so SlyOS’s copy of the conversation is complete.one of: inbound, outbound · default: inbound

201 response

The message is new and was stored.

message_idstring (uuid) · required
SlyOS’s id for the message. Ask for a draft with it.
thread_idstring (uuid) · required
SlyOS’s id for the conversation.
card_idstring (uuid) | null · required
The reply card this message opened, or null when it needs no reply.
duplicateboolean · required
True when this external_message_id was already stored on this channel. Nothing new was written.
needs_replyboolean · required
False when SlyOS decided nobody is waiting for an answer — a receipt, a newsletter. Do not ask for a draft then.
bulkboolean · required
True when the message was recognised as bulk mail. It is stored and shown, with no card.
{
  "message_id": "9b2e4f71-3c8a-4d5e-a1f6-2e7b9c0d4a18",
  "thread_id": "c4a7e2d9-1b6f-4e83-9a50-7d2c8f1e3b65",
  "card_id": "5e8d1c3a-7f24-4b9e-8c61-0a3f9d2e7b14",
  "duplicate": false,
  "needs_reply": true,
  "bulk": false
}

200 response

This external_message_id was already stored on this channel; nothing new was written. The same fields as the 201 response.

{
  "message_id": "9b2e4f71-3c8a-4d5e-a1f6-2e7b9c0d4a18",
  "thread_id": "c4a7e2d9-1b6f-4e83-9a50-7d2c8f1e3b65",
  "card_id": "5e8d1c3a-7f24-4b9e-8c61-0a3f9d2e7b14",
  "duplicate": true,
  "needs_reply": true,
  "bulk": false
}

Errors

bad_request400
The payload is malformed — not JSON, no Content-Type: application/json, or a field of the wrong shape — or the body is over 32,000 characters.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry messages:write.
not_found404
No such channel in this key’s environment.
channel_disabled409
The channel is switched off in SlyOS. Nothing was stored.
not_stored409
SlyOS could not store the message, and nothing was stored. Retry with the same external_message_id: it is stored once. If it keeps failing, keep the ticket on your side and tell us.
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

Drafts

POST/v1/developer/drafts

Ask for a reply

Ask SlyOS to write a reply, using the account’s Brain, voice and history. The one billable call: 10¢ per successful response, charged once per generation_id.

Scope drafts:generate10¢ per AI response on a live key300 requests a minute

The reply is written by the account’s own Brain — what the business has taught SlyOS, the voice it has learned for the channel, and the conversation so far — through the same pipeline as the SlyOS inbox.

The only call that costs anything, and only on a live key. A request that does not produce a reply — refused, failed, or not needed — is not charged.

Check held_for_review before you send — on a replay too, which carries the hold the reply was written with. Send automatically only on false; true or null goes to a person, never straight to your customer, with hold_reasons saying why.

A replay (already_charged: true) is a reply SlyOS already gave you once. If your first request may have sent it, do not send it again: check your own record of what went out.

curl -X POST "https://api.slyos.world/v1/developer/drafts" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "generation_id": "9b2e4f71-3c8a-4d5e-a1f6-2e7b9c0d4a18:reply-1",
  "message_id": "9b2e4f71-3c8a-4d5e-a1f6-2e7b9c0d4a18"
}'

Request body

generation_idstring · required
Your id for this one reply, and the idempotency key for the charge: sending the same generation_id again does not buy a second reply. It is yours alone — unique within your account and the key’s environment, never shared with another account. Make it unique and reproducible — derive it from the message_id SlyOS gave you, e.g. <message_id>:reply-1, and use :reply-2 for a deliberate second reply. An id already used for a different conversation is refused (409 generation_id_conflict), never answered with that conversation’s reply.8–128 characters
card_idstring (uuid)
The card to reply on. Give this or message_id.
message_idstring (uuid)
A message_id from POST /v1/developer/messages. SlyOS replies on the card that message opened. Give this or card_id.
instructionstring
A one-off steer for this reply, in plain words — for example “Offer a call on Thursday.” At most 2,000 characters.at most 2,000 characters

201 response

A reply was written for this generation_id.

draft_idstring (uuid) · required
The draft’s id. GET /v1/developer/drafts/{draftId} reads it back.
card_idstring (uuid) · required
The card the reply is for.
generationinteger · required
Which draft this is on the card: 1 for the first, higher after another reply was asked for.
bodystring · required
The reply text.
held_for_reviewboolean | null · required
True when SlyOS’s checks held this reply for a person — money, health or dosing, a legal or data-protection matter, another sensitive subject, or something it gives out that the account has not sent before. Never send a held reply automatically: show it to a person first. Null when SlyOS can no longer say, because the conversation has had a newer reply since; null means held. Send automatically only on false.
hold_reasonsarray of object · required
Why it was held: one entry per check that held it. Empty when held_for_review is not true.
hold_reasons[].codestring · required
Which check held it: sensitive (money, legal, health or credentials), attachment (it carries a file), group (it goes to a group), regex_floor (it matches a rule you set), outbound_screen (it gives out something not sent before, or answers a risky request). A code not listed here is still a hold — treat every code you do not recognise as one.
hold_reasons[].labelstring · required
The same reason in words, as the SlyOS inbox shows it.
hold_detailstring | null · required
What the checks found, in sentences, when the reply was held. Null when it was not held, or when SlyOS cannot say.
billed_centsinteger · required
What THIS request was charged: the price on a live key’s first success, 0 on a replay and in the sandbox.
already_chargedboolean · required
True when this generation_id had already been paid for and this response is that same reply again.
originally_billed_centsinteger
Only on a replay: what the original generation was charged.
price_versionstring | null · required
The price version the charge was made under. Null in the sandbox.
environmentstring · required
The environment of the key that made the call: sandbox or live.one of: sandbox, live
replayedboolean · required
True when this answer is a reply SlyOS already wrote for this generation_id — in the sandbox too, where nothing is charged. It may already have been sent: do not send it again automatically.
generation_idstring · required
The generation_id you sent, echoed.
sourcesarray of object · required
What this reply relied on: every passage of the business’s own knowledge — the links, sites and files it handed SlyOS, and its rules — that the reply was given, rules first. Empty when the reply was given none: the business has handed SlyOS nothing yet, or nothing it handed over matched.
sources[].source_idstring (uuid) | null · required
The knowledge source the passage came from. Null once that source is deleted.
sources[].passage_idstring (uuid) | null · required
The passage itself. Null once its source is deleted.
sources[].titlestring | null · required
What the source was called when the reply was written: its title, file name or address.
sources[].urlstring | null · required
The page the passage was read from, for a link or a site.
sources[].pageinteger | null · required
The page of the file its own text starts on, for a file that has pages. With page_end, the range it covers.
sources[].page_endinteger | null · required
The last page it runs onto, when it runs past page — a passage starting on page 11 and carrying page 12 is page: 11, page_end: 12 (p.11–12). Null when it stays on one page, or the file has no pages.
sources[].sectionstring | null · required
The heading it sat under, if any.
sources[].ruleboolean · required
True for a binding rule the reply had to follow; false for reference material it could draw on.
sources[].rankinteger · required
Its place in what the reply was given, from 1 — counted separately for rules and for reference.
sources[].scorenumber | null · required
How close the passage was to the message, as cosine similarity. Null when there was nothing to compare.
sources[].excerptstring | null · required
Up to 280 characters of the passage’s OWN text — past the lines it repeats from the passage before it — from the sentence that shares the most words with the reply, else from its start; … marks a cut. While its source still exists.
sources[].text_sha256string · required
SHA-256 of the stored passage the reply was given — all of it, or its first carried_chars characters: proof of which text it relied on, even after the source changes.
sources[].carried_charsinteger | null · required
How many of the passage’s characters the reply was given, when it was cut to fit. Null when it was given the whole passage.
{
  "draft_id": "e1f7a3c5-9d2b-4e68-b0a4-6c3e8f1d2a97",
  "card_id": "5e8d1c3a-7f24-4b9e-8c61-0a3f9d2e7b14",
  "generation": 1,
  "body": "Hi Dana — yes, we ship to Canada. Orders usually arrive within 5–8 business days, and you’ll get a tracking link by email as soon as yours leaves our warehouse.",
  "held_for_review": false,
  "hold_reasons": [],
  "hold_detail": null,
  "billed_cents": 10,
  "already_charged": false,
  "price_version": "2026-09-01",
  "environment": "live",
  "replayed": false,
  "generation_id": "9b2e4f71-3c8a-4d5e-a1f6-2e7b9c0d4a18:reply-1",
  "sources": [
    {
      "source_id": "6b1d4f8a-3c2e-4a9f-b7d0-1e5c8a2f4d63",
      "passage_id": "2f9e4c1a-7b3d-4e8f-a5c2-6d1b9e3f7a40",
      "title": "Example Co — website",
      "url": "https://www.example.com/shipping",
      "page": null,
      "page_end": null,
      "section": "Shipping",
      "rule": false,
      "rank": 1,
      "score": 0.83,
      "excerpt": "We ship to the US and Canada. Orders leave our warehouse within two business days.",
      "text_sha256": "3b7e1f9a2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e2f",
      "carried_chars": null
    }
  ]
}

200 response

A live key sent a generation_id that was already paid for: the same reply again, with the hold it was written with, charged nothing. It may already have been sent — do not send it again automatically. The same fields as the 201 response.

{
  "draft_id": "e1f7a3c5-9d2b-4e68-b0a4-6c3e8f1d2a97",
  "card_id": "5e8d1c3a-7f24-4b9e-8c61-0a3f9d2e7b14",
  "generation": 1,
  "body": "Hi Dana — yes, we ship to Canada. Orders usually arrive within 5–8 business days, and you’ll get a tracking link by email as soon as yours leaves our warehouse.",
  "held_for_review": false,
  "hold_reasons": [],
  "hold_detail": null,
  "billed_cents": 0,
  "already_charged": true,
  "originally_billed_cents": 10,
  "price_version": "2026-09-01",
  "environment": "live",
  "replayed": true,
  "generation_id": "9b2e4f71-3c8a-4d5e-a1f6-2e7b9c0d4a18:reply-1",
  "sources": [
    {
      "source_id": "6b1d4f8a-3c2e-4a9f-b7d0-1e5c8a2f4d63",
      "passage_id": "2f9e4c1a-7b3d-4e8f-a5c2-6d1b9e3f7a40",
      "title": "Example Co — website",
      "url": "https://www.example.com/shipping",
      "page": null,
      "page_end": null,
      "section": "Shipping",
      "rule": false,
      "rank": 1,
      "score": 0.83,
      "excerpt": "We ship to the US and Canada. Orders leave our warehouse within two business days.",
      "text_sha256": "3b7e1f9a2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e2f",
      "carried_chars": null
    }
  ]
}

Errors

bad_request400
The payload is malformed — a generation_id shorter than 8 or longer than 128 characters, an id that is not a UUID — or neither card_id nor message_id was given, or they name two different conversations.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
insufficient_funds402
No API credit remains. The message is still stored and visible, and can be answered by hand. Nothing was charged.
missing_scope403
The key does not carry drafts:generate.
not_found404
No such message or card in this key’s environment. A sandbox key cannot reach a live card, and neither can reach another channel’s.
no_card409
That message needs no reply, so there is nothing to draft.
channel_paused409
The channel is paused on the Developer screen, so nothing is drafted on it. Nothing was charged.
generation_in_progress409
Another request with this generation_id is still being drafted. Retry after Retry-After with the same id to get that draft, charged once.
generation_id_conflict409
That generation_id is already taken on this account. Nothing was charged.
draft_gone409
The reply this generation_id produced was deleted with its conversation. Use a new generation_id.
paused_identity409
Someone asked whether they are talking to a person; drafting is paused until the account owner clears it. Nothing was charged.
paused_allowance409
Sandbox only: the plan’s AI allowance is used up, and sandbox drafts are drawn from the plan. Live drafts are paid from the wallet and never refused for this. Nothing was charged.
filed_away409
The account owner archived or filed the conversation. Do not retry. Nothing was charged.
no_reply_needed409
The model judged nobody is waiting for a reply (an automated message, say), so no draft was written. Nothing was charged.
refused409
No draft could be produced for that card: its conversation or its channel could not be used. A card_id that does not exist is a 404 not_found, not this. Nothing was charged.
ai_off409
The account owner has not allowed SlyOS to send their data to AI providers. Nothing was sent or charged. Do not retry until they allow it in the app.
partner_ai_paused409
The account’s partner pays for its AI with the partner’s own key, and that key is not set or the AI provider refused it. Nothing was sent on any other key, and nothing was charged. Do not retry until the partner has fixed it.
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
sandbox_daily_limit429
Sandbox only: this account has had 50 sandbox drafts in the last 24 hours. Retry after Retry-After, or use a live key. A replay of a draft already written is not counted. Nothing was charged.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
outage503
No model provider is reachable. Retry after Retry-After with the same generation_id. Nothing was charged.
try_again503
The draft could not be charged for in time, so it was not. Retry with the same generation_id.
drafting_unavailable503
Drafting is not configured on this deployment.

GET/v1/developer/drafts/{draftId}

Read a reply back

Read a draft back. Free.

Scope drafts:readFree600 requests a minute

curl "https://api.slyos.world/v1/developer/drafts/$DRAFT_ID" \
  -H "Authorization: Bearer $SLYOS_KEY"

Parameters

draftIdstring (uuid) · in path · required
The draft_id to read, from POST /v1/developer/drafts or draft.ready.

200 response

The draft exists on this account.

draft_idstring (uuid) · required
The draft’s id.
card_idstring (uuid) · required
The card the reply is for.
bodystring · required
The reply text.
generationinteger · required
Which draft this is on the card.
modelstring · required
The model that wrote it.
created_atstring (date-time) · required
When it was written.
held_for_reviewboolean | null · required
True when SlyOS’s checks held this reply for a person — money, health or dosing, a legal or data-protection matter, another sensitive subject, or something it gives out that the account has not sent before. Never send a held reply automatically: show it to a person first. Null when SlyOS can no longer say, because the conversation has had a newer reply since; null means held. Send automatically only on false.
hold_reasonsarray of object · required
Why it was held: one entry per check that held it. Empty when held_for_review is not true.
hold_reasons[].codestring · required
Which check held it: sensitive (money, legal, health or credentials), attachment (it carries a file), group (it goes to a group), regex_floor (it matches a rule you set), outbound_screen (it gives out something not sent before, or answers a risky request). A code not listed here is still a hold — treat every code you do not recognise as one.
hold_reasons[].labelstring · required
The same reason in words, as the SlyOS inbox shows it.
hold_detailstring | null · required
What the checks found, in sentences, when the reply was held. Null when it was not held, or when SlyOS cannot say.
supersededboolean · required
True when a newer reply has since been written on the same card. Send the newest, not this one.
sourcesarray of object · required
What this reply relied on: every passage of the business’s own knowledge — the links, sites and files it handed SlyOS, and its rules — that the reply was given, rules first. Empty when the reply was given none: the business has handed SlyOS nothing yet, or nothing it handed over matched.
sources[].source_idstring (uuid) | null · required
The knowledge source the passage came from. Null once that source is deleted.
sources[].passage_idstring (uuid) | null · required
The passage itself. Null once its source is deleted.
sources[].titlestring | null · required
What the source was called when the reply was written: its title, file name or address.
sources[].urlstring | null · required
The page the passage was read from, for a link or a site.
sources[].pageinteger | null · required
The page of the file its own text starts on, for a file that has pages. With page_end, the range it covers.
sources[].page_endinteger | null · required
The last page it runs onto, when it runs past page — a passage starting on page 11 and carrying page 12 is page: 11, page_end: 12 (p.11–12). Null when it stays on one page, or the file has no pages.
sources[].sectionstring | null · required
The heading it sat under, if any.
sources[].ruleboolean · required
True for a binding rule the reply had to follow; false for reference material it could draw on.
sources[].rankinteger · required
Its place in what the reply was given, from 1 — counted separately for rules and for reference.
sources[].scorenumber | null · required
How close the passage was to the message, as cosine similarity. Null when there was nothing to compare.
sources[].excerptstring | null · required
Up to 280 characters of the passage’s OWN text — past the lines it repeats from the passage before it — from the sentence that shares the most words with the reply, else from its start; … marks a cut. While its source still exists.
sources[].text_sha256string · required
SHA-256 of the stored passage the reply was given — all of it, or its first carried_chars characters: proof of which text it relied on, even after the source changes.
sources[].carried_charsinteger | null · required
How many of the passage’s characters the reply was given, when it was cut to fit. Null when it was given the whole passage.
{
  "draft_id": "e1f7a3c5-9d2b-4e68-b0a4-6c3e8f1d2a97",
  "card_id": "5e8d1c3a-7f24-4b9e-8c61-0a3f9d2e7b14",
  "body": "Hi Dana — yes, we ship to Canada. Orders usually arrive within 5–8 business days, and you’ll get a tracking link by email as soon as yours leaves our warehouse.",
  "generation": 1,
  "model": "claude-opus-5-5",
  "created_at": "2026-10-09T15:42:11.000Z",
  "held_for_review": false,
  "hold_reasons": [],
  "hold_detail": null,
  "superseded": false,
  "sources": [
    {
      "source_id": "6b1d4f8a-3c2e-4a9f-b7d0-1e5c8a2f4d63",
      "passage_id": "2f9e4c1a-7b3d-4e8f-a5c2-6d1b9e3f7a40",
      "title": "Example Co — website",
      "url": "https://www.example.com/shipping",
      "page": null,
      "page_end": null,
      "section": "Shipping",
      "rule": false,
      "rank": 1,
      "score": 0.83,
      "excerpt": "We ship to the US and Canada. Orders leave our warehouse within two business days.",
      "text_sha256": "3b7e1f9a2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e2f",
      "carried_chars": null
    }
  ]
}

Errors

bad_request400
The draft id is not a UUID.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry drafts:read.
not_found404
No such draft in this key’s environment.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

Deliveries

POST/v1/developer/deliveries

Report what happened to a send

Tell SlyOS what happened to the reply you sent through your own system. Recorded, and — given draft_id or body — written into the conversation so every SlyOS surface shows it. Free — we do not charge a customer for reporting their own send.

Scope deliveries:writeFree1,200 requests a minute

Send draft_id when you sent SlyOS’s draft as it was, or body with the text you actually sent. The reply then shows in the conversation on every SlyOS surface and its card closes; without either, the report is recorded and no text is invented for it.

curl -X POST "https://api.slyos.world/v1/developer/deliveries" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "message_id": "9b2e4f71-3c8a-4d5e-a1f6-2e7b9c0d4a18",
  "status": "delivered",
  "draft_id": "e1f7a3c5-9d2b-4e68-b0a4-6c3e8f1d2a97",
  "occurred_at": "2026-10-09T15:44:02Z"
}'

Request body

message_idstring (uuid) · required
The SlyOS message_id the reply answered — the id ingest returned.
statusstring · required
What happened, as your system saw it.one of: delivered, read, failed
detailstring
Your system’s own note — a provider’s error text, for example.at most 512 characters
occurred_atstring (date-time)
When it happened: ISO 8601 in UTC, ending in Z.
draft_idstring (uuid)
The SlyOS draft you sent, as it was. The reply then appears in the conversation, and the card it answered closes as sent.
bodystring
The text you actually sent, when a person edited the draft or wrote it. Wins over draft_id's text when both are given. At most 32,000 characters.1–32,000 characters
external_message_idstring
Your own id for the message you sent. Reporting the same one again is still one message in the conversation.1–512 characters

200 response

The report was recorded.

delivery_idstring (uuid) · required
SlyOS’s record of this report.
message_idstring (uuid) · required
The message the report was about.
statusstring · required
The status you reported.one of: delivered, read, failed
recordedtrue · required
Always true on a 200: the report was stored.
outbound_message_idstring (uuid) | null · required
The reply as it now appears in the conversation. Null when no text was given (draft_id or body), or the status was not delivered.
draft_idstring (uuid) | null · required
The SlyOS draft this was — named by draft_id, or matched by its words. Null when the words were not SlyOS’s.
card_closedstring (uuid) | null · required
The card this reply closed as sent. Null when it closed none.
{
  "delivery_id": "4d7b1e9f-2a6c-4f35-b8e0-9c3a5f1d7e42",
  "message_id": "9b2e4f71-3c8a-4d5e-a1f6-2e7b9c0d4a18",
  "status": "delivered",
  "recorded": true,
  "outbound_message_id": "b3e9c7a1-5d2f-4e86-a0c4-7f1b3d9e5a28",
  "draft_id": "e1f7a3c5-9d2b-4e68-b0a4-6c3e8f1d2a97",
  "card_closed": "5e8d1c3a-7f24-4b9e-8c61-0a3f9d2e7b14"
}

Errors

bad_request400
The payload is malformed — message_id or draft_id is not a UUID, a body is empty or too long — or the status is not delivered, read or failed.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry deliveries:write.
not_found404
No such message in this key’s environment, or the draft named is not on that message’s conversation.
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

Usage

GET/v1/developer/usage

Read usage

Responses written for THIS key’s environment and project, and what they cost, over the last 30 days. Free. A sandbox key sees sandbox drafts at nought cents, never the account’s live spend.

Scope usage:readFree120 requests a minute

curl "https://api.slyos.world/v1/developer/usage" \
  -H "Authorization: Bearer $SLYOS_KEY"

200 response

Always, for a valid key.

environmentstring · required
The key’s environment, and the only one counted: a live key reads its project’s live usage, a sandbox key its sandbox drafts at nought cents.one of: sandbox, live
window_daysinteger · required
How many days back the totals cover.
responsesinteger · required
Billable responses charged in the window. Sandbox responses are never billable.
charged_centsinteger · required
What those responses cost, in total.
priceobject · required
The price in force now.
price.versionstring · required
Today’s price version.
price.cents_per_responseinteger · required
Today’s price for one successful response.
{
  "environment": "live",
  "window_days": 30,
  "responses": 412,
  "charged_cents": 4120,
  "price": {
    "version": "2026-09-01",
    "cents_per_response": 10
  }
}

Errors

unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry usage:read.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

Wallet

GET/v1/developer/wallet

Check the balance

Balance, reserved, and how many responses remain — so an adapter can degrade before it is refused rather than after. A live key reads the wallet; a sandbox key reads no money at all (zeros) and how many of today’s free sandbox drafts are left.

Scope usage:readFree240 requests a minute

Call it before a burst: an adapter that checks first can queue or hand over to a person before it is refused.

curl "https://api.slyos.world/v1/developer/wallet" \
  -H "Authorization: Bearer $SLYOS_KEY"

200 response

Always, for a valid key.

environmentstring · required
The key’s environment. A sandbox key reads no money — every amount is 0 — and the drafts sandbox still allows today.one of: sandbox, live
balance_centsinteger · required
Money in the wallet. Always 0 for a sandbox key.
reserved_centsinteger · required
Held for replies being written right now.
available_centsinteger · required
What can be spent: balance minus reserved.
responses_remaininginteger · required
How many more replies the available balance pays for at today’s price. For a sandbox key, how many sandbox drafts remain in the last 24 hours’ allowance.
sandbox_daily_limitinteger
Sandbox keys only: the drafts sandbox allows in any 24 hours (50).
{
  "environment": "live",
  "balance_cents": 1870,
  "reserved_cents": 10,
  "available_cents": 1860,
  "responses_remaining": 186
}

Errors

unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry usage:read.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

Channels

POST/v1/developer/channels

Create a channel

Create a custom channel. Its environment is the KEY’s — a live key makes a live channel — so a sandbox channel can never end up on live traffic by typo.

Scope channels:manageFree60 requests a minute

The environment is the key’s: a live key makes a live channel and a sandbox key a sandbox one. There is no field to say otherwise, so a sandbox channel cannot end up on live traffic by typo.

curl -X POST "https://api.slyos.world/v1/developer/channels" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Support desk",
  "system": "Zendesk",
  "capabilities": [
    "receive",
    "send",
    "threading"
  ]
}'

Request body

namestring · required
What the channel is called on every SlyOS screen.1–120 characters
systemstring
The system behind it, for your own reference — “Zendesk”, “our app chat”.at most 120 characters
capabilitiesarray of string
What your adapter can actually do on this channel. SlyOS never assumes one it was not told. Defaults to ["receive", "send"].one of: receive, send, attachments, read_receipts, mark_read, delivery_receipts, reactions, typing, threading, groups

201 response

The channel was created, in the key’s environment.

idstring (uuid) · required
The channel’s id: the channel_id every message sent to it names.
namestring · required
Its name on every SlyOS screen.
systemstring | null · required
The system behind it, if you named one.
environmentstring · required
The environment of the key that made the call: sandbox or live.one of: sandbox, live
capabilitiesarray of string · required
What the adapter declared it can do.one of: receive, send, attachments, read_receipts, mark_read, delivery_receipts, reactions, typing, threading, groups
{
  "id": "3f6c2b1e-8a4d-4c7f-9e2b-5d1a7c9e0f42",
  "name": "Support desk",
  "system": "Zendesk",
  "environment": "sandbox",
  "capabilities": [
    "receive",
    "send",
    "threading"
  ]
}

Errors

bad_request400
The payload is malformed: no name, a name over 120 characters, or a capability outside the published set.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
no_slots402
Every channel slot on this plan is in use. Upgrade, or disconnect a channel first. Sandbox channels are free and do not count.
missing_scope403
The key does not carry channels:manage.
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

GET/v1/developer/channels

List channels

The custom channels this key can reach, with their declared capabilities. Free.

Scope channels:manageFree120 requests a minute

curl "https://api.slyos.world/v1/developer/channels" \
  -H "Authorization: Bearer $SLYOS_KEY"

200 response

Always, for a valid key.

channelsarray of object · required
Every custom channel in the key’s environment.
channels[].idstring (uuid) · required
The channel’s id: the channel_id every message sent to it names.
channels[].namestring · required
Its name on every SlyOS screen.
channels[].systemstring | null · required
The system behind it, if you named one.
channels[].environmentstring · required
The environment of the key that made the call: sandbox or live.one of: sandbox, live
channels[].capabilitiesarray of string · required
What the adapter declared it can do.one of: receive, send, attachments, read_receipts, mark_read, delivery_receipts, reactions, typing, threading, groups
channels[].pausedboolean · required
Whether the account owner has paused the channel.
channels[].auto_draftboolean · required
The channel’s automatic-drafting setting, as the owner last saved it.
channels[].last_inbound_atstring (date-time) | null · required
When a message last arrived on it.
channels[].last_outbound_atstring (date-time) | null · required
When a delivery was last reported on it.
known_capabilitiesarray of string · required
Every capability an adapter may declare.one of: receive, send, attachments, read_receipts, mark_read, delivery_receipts, reactions, typing, threading, groups
{
  "channels": [
    {
      "id": "3f6c2b1e-8a4d-4c7f-9e2b-5d1a7c9e0f42",
      "name": "Support desk",
      "system": "Zendesk",
      "environment": "sandbox",
      "capabilities": [
        "receive",
        "send",
        "threading"
      ],
      "paused": false,
      "auto_draft": false,
      "last_inbound_at": "2026-10-09T15:42:08.000Z",
      "last_outbound_at": null
    }
  ],
  "known_capabilities": [
    "receive",
    "send",
    "attachments",
    "read_receipts",
    "mark_read",
    "delivery_receipts",
    "reactions",
    "typing",
    "threading",
    "groups"
  ]
}

Errors

unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry channels:manage.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

Knowledge

POST/v1/developer/knowledge/sources

Add a link, a site or a file

Add a source: JSON {url, crawl?, rules?, title?, scope?} for a page or a whole site, or multipart with a `file` (PDF, Word, text, Markdown, CSV; up to 25 MB). Answers 202; the source is read in the background. Needs a live key.

Scope brain:writeFree60 requests a minute

A link or a site is JSON: {"url": "…"}, with "crawl": true to read the whole site. A file is multipart/form-data with the file in a file field.

Mark terms, policies and regulations rules: replies must follow a rule, and draw on everything else as reference.

As application/json

curl -X POST "https://api.slyos.world/v1/developer/knowledge/sources" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://www.example.com/",
  "crawl": true
}'

As multipart/form-data

curl -X POST "https://api.slyos.world/v1/developer/knowledge/sources" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -F 'file=@returns-policy.pdf' \
  -F 'rules=true' \
  -F 'title=Returns policy'

Request body — application/json

urlstring · required
A public http(s) address: one page — your pricing page, a policy, a regulation — or, with crawl, the start of a whole site.1–2,048 characters
crawlboolean
Read the whole site from this address: the same site only, robots.txt respected, up to the page limit this operation states. Default: this page alone.
rulesboolean
True when replies must FOLLOW this source — terms, a policy, a regulation — rather than draw on it as reference. Default false.
titlestring
What to call it on the Developer screen and in a reply’s sources.at most 300 characters
scopestring
project: replies asked for with this key’s project use it. account: replies in every project on the account do.one of: project, account

Request body — multipart/form-data

filestring (binary) · required
The file — PDF, Word (.docx), plain text, Markdown or CSV, up to the size this operation states. It is read to the end; a long one is cut into passages, not cut short.
rulesstring
true when replies must follow this file rather than draw on it. Default false.one of: true, false
titlestring
What to call it on the Developer screen and in a reply’s sources.
scopestring
project: replies asked for with this key’s project use it. account: replies in every project on the account do.one of: project, account

202 response

Taken in. It is read in the background: poll GET /v1/developer/knowledge/sources/{sourceId} until status is ready.

idstring (uuid) · required
The source’s id.
kindstring · required
link: one page. site: a whole site, read from one address. file: an upload.one of: link, site, file
urlstring | null · required
The address it is read from. Null for a file.
filenamestring | null · required
The uploaded file’s name. Null for a link or a site.
titlestring | null · required
What it is called on the Developer screen and in a reply’s sources.
rulesboolean · required
True when replies must follow it; false when it is reference material.
scopestring · required
project: replies asked for with this key’s project use it. account: replies in every project on the account do.one of: project, account
statusstring · required
queued, then processing, while it is read in the background; ready once replies use it; failed when it could not be read, with status_reason saying why.one of: queued, processing, ready, failed
status_reasonstring | null · required
In a sentence, why the status is what it is. Null when there is nothing to say.
warningstring | null · required
Set when the source is ready but some of what it shows may be missing, in a sentence naming the pages and what to do — today, pages that build their content with JavaScript, which SlyOS does not run (a price list drawn from a script, a template filled in after the page loads). Null when there is nothing to warn about.
incomplete_pagesarray of string · required
The addresses of the pages warning names: pages whose content is built with JavaScript, so replies may be missing what a visitor sees there. Add that text as a file, or as a single link to a page that shows it. Empty when there are none.
pagesinteger · required
Pages read from it.
passagesinteger · required
Passages it was cut into: what a reply retrieves from.
charactersinteger · required
Characters of text read from it.
size_bytesinteger | null · required
The uploaded file’s size. Null for a link or a site.
content_sha256string | null · required
SHA-256 of the text read from it; it changes when a refresh reads something new.
file_sha256string | null · required
SHA-256 of the uploaded file’s bytes. Null for a link or a site.
used_in_repliesinteger · required
How many replies have relied on a passage of it.
rules_fitboolean | null · required
For a rules source: true when every one of the business’s rules reaches every reply; false when they no longer fit in one reply, and each reply gets the rules nearest its message. Null for reference material, or when it was not measured.
rules_notestring | null · required
When rules_fit is false, in a sentence: how much the rules come to, what one reply carries, and what to do about it.
created_atstring (date-time) · required
When it was added.
updated_atstring (date-time) · required
When it last changed.
fetched_atstring (date-time) | null · required
When it was last read. Null until it has been.
duplicateboolean · required
True when this address or this exact file was already a source: nothing new was added, and this is that source.
{
  "id": "0e7c2a9d-5f1b-4d38-a6e4-9b3c1f7d2a85",
  "kind": "file",
  "url": null,
  "filename": "returns-policy.pdf",
  "title": "Returns policy",
  "rules": true,
  "scope": "project",
  "status": "queued",
  "status_reason": null,
  "warning": null,
  "incomplete_pages": [],
  "pages": 0,
  "passages": 0,
  "characters": 0,
  "size_bytes": 284311,
  "content_sha256": null,
  "file_sha256": "9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c",
  "used_in_replies": 0,
  "rules_fit": true,
  "rules_note": null,
  "created_at": "2026-10-09T15:20:04.000Z",
  "updated_at": "2026-10-09T15:20:04.000Z",
  "fetched_at": null,
  "duplicate": false
}

200 response

That address, or that exact file, is already a source: this is it, and nothing new was added. The same fields as the 202 response.

{
  "id": "6b1d4f8a-3c2e-4a9f-b7d0-1e5c8a2f4d63",
  "kind": "site",
  "url": "https://www.example.com/",
  "filename": null,
  "title": "Example Co — website",
  "rules": false,
  "scope": "project",
  "status": "ready",
  "status_reason": null,
  "warning": null,
  "incomplete_pages": [],
  "pages": 24,
  "passages": 187,
  "characters": 96412,
  "size_bytes": null,
  "content_sha256": "4f2a9c0e7b1d3e5f6a8c9b0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
  "file_sha256": null,
  "used_in_replies": 38,
  "rules_fit": null,
  "rules_note": null,
  "created_at": "2026-10-09T15:20:04.000Z",
  "updated_at": "2026-10-09T15:21:37.000Z",
  "fetched_at": "2026-10-09T15:21:37.000Z",
  "duplicate": true
}

Errors

bad_request400
No url and no file, or an address SlyOS will not read.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry brain:write.
sandbox_read_only403
A sandbox key can read knowledge but not change it.
too_many_sources409
The account already holds the most knowledge sources it may.
knowledge_full409
The account already holds the most knowledge text it may.
too_large413
The file is over 25 MB.
unsupported_type422
Not a file type SlyOS reads (PDF, Word .docx, plain text, Markdown, CSV).
rate_limited429
Over this route’s per-minute limit, counted per account and per key. Wait a minute.
busy429
Too many sources are being read at once. Add more when they finish.
uploads_busy429
Two files are already uploading for this account. Send the next after.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

GET/v1/developer/knowledge/sources

List knowledge sources

The sources this key’s replies use — its project’s and the account’s — with status. Free.

Scope brain:readFree240 requests a minute

curl "https://api.slyos.world/v1/developer/knowledge/sources" \
  -H "Authorization: Bearer $SLYOS_KEY"

200 response

Always, for a valid key.

sourcesarray of object · required
Every source this key’s replies use: its project’s and the account’s.
sources[].idstring (uuid) · required
The source’s id.
sources[].kindstring · required
link: one page. site: a whole site, read from one address. file: an upload.one of: link, site, file
sources[].urlstring | null · required
The address it is read from. Null for a file.
sources[].filenamestring | null · required
The uploaded file’s name. Null for a link or a site.
sources[].titlestring | null · required
What it is called on the Developer screen and in a reply’s sources.
sources[].rulesboolean · required
True when replies must follow it; false when it is reference material.
sources[].scopestring · required
project: replies asked for with this key’s project use it. account: replies in every project on the account do.one of: project, account
sources[].statusstring · required
queued, then processing, while it is read in the background; ready once replies use it; failed when it could not be read, with status_reason saying why.one of: queued, processing, ready, failed
sources[].status_reasonstring | null · required
In a sentence, why the status is what it is. Null when there is nothing to say.
sources[].warningstring | null · required
Set when the source is ready but some of what it shows may be missing, in a sentence naming the pages and what to do — today, pages that build their content with JavaScript, which SlyOS does not run (a price list drawn from a script, a template filled in after the page loads). Null when there is nothing to warn about.
sources[].incomplete_pagesarray of string · required
The addresses of the pages warning names: pages whose content is built with JavaScript, so replies may be missing what a visitor sees there. Add that text as a file, or as a single link to a page that shows it. Empty when there are none.
sources[].pagesinteger · required
Pages read from it.
sources[].passagesinteger · required
Passages it was cut into: what a reply retrieves from.
sources[].charactersinteger · required
Characters of text read from it.
sources[].size_bytesinteger | null · required
The uploaded file’s size. Null for a link or a site.
sources[].content_sha256string | null · required
SHA-256 of the text read from it; it changes when a refresh reads something new.
sources[].file_sha256string | null · required
SHA-256 of the uploaded file’s bytes. Null for a link or a site.
sources[].used_in_repliesinteger · required
How many replies have relied on a passage of it.
sources[].rules_fitboolean | null · required
For a rules source: true when every one of the business’s rules reaches every reply; false when they no longer fit in one reply, and each reply gets the rules nearest its message. Null for reference material, or when it was not measured.
sources[].rules_notestring | null · required
When rules_fit is false, in a sentence: how much the rules come to, what one reply carries, and what to do about it.
sources[].created_atstring (date-time) · required
When it was added.
sources[].updated_atstring (date-time) · required
When it last changed.
sources[].fetched_atstring (date-time) | null · required
When it was last read. Null until it has been.
{
  "sources": [
    {
      "id": "6b1d4f8a-3c2e-4a9f-b7d0-1e5c8a2f4d63",
      "kind": "site",
      "url": "https://www.example.com/",
      "filename": null,
      "title": "Example Co — website",
      "rules": false,
      "scope": "project",
      "status": "ready",
      "status_reason": null,
      "warning": null,
      "incomplete_pages": [],
      "pages": 24,
      "passages": 187,
      "characters": 96412,
      "size_bytes": null,
      "content_sha256": "4f2a9c0e7b1d3e5f6a8c9b0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
      "file_sha256": null,
      "used_in_replies": 38,
      "rules_fit": null,
      "rules_note": null,
      "created_at": "2026-10-09T15:20:04.000Z",
      "updated_at": "2026-10-09T15:21:37.000Z",
      "fetched_at": "2026-10-09T15:21:37.000Z"
    }
  ]
}

Errors

unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry brain:read.
rate_limited429
Over this route’s per-minute limit, counted per account and per key. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

GET/v1/developer/knowledge/sources/{sourceId}

Read a knowledge source

One source: status and why, pages and passages read, and how many replies relied on it. Free.

Scope brain:readFree240 requests a minute

curl "https://api.slyos.world/v1/developer/knowledge/sources/$SOURCE_ID" \
  -H "Authorization: Bearer $SLYOS_KEY"

Parameters

sourceIdstring (uuid) · in path · required
A knowledge source’s id, from adding it or listing them.

200 response

The source exists for this key.

idstring (uuid) · required
The source’s id.
kindstring · required
link: one page. site: a whole site, read from one address. file: an upload.one of: link, site, file
urlstring | null · required
The address it is read from. Null for a file.
filenamestring | null · required
The uploaded file’s name. Null for a link or a site.
titlestring | null · required
What it is called on the Developer screen and in a reply’s sources.
rulesboolean · required
True when replies must follow it; false when it is reference material.
scopestring · required
project: replies asked for with this key’s project use it. account: replies in every project on the account do.one of: project, account
statusstring · required
queued, then processing, while it is read in the background; ready once replies use it; failed when it could not be read, with status_reason saying why.one of: queued, processing, ready, failed
status_reasonstring | null · required
In a sentence, why the status is what it is. Null when there is nothing to say.
warningstring | null · required
Set when the source is ready but some of what it shows may be missing, in a sentence naming the pages and what to do — today, pages that build their content with JavaScript, which SlyOS does not run (a price list drawn from a script, a template filled in after the page loads). Null when there is nothing to warn about.
incomplete_pagesarray of string · required
The addresses of the pages warning names: pages whose content is built with JavaScript, so replies may be missing what a visitor sees there. Add that text as a file, or as a single link to a page that shows it. Empty when there are none.
pagesinteger · required
Pages read from it.
passagesinteger · required
Passages it was cut into: what a reply retrieves from.
charactersinteger · required
Characters of text read from it.
size_bytesinteger | null · required
The uploaded file’s size. Null for a link or a site.
content_sha256string | null · required
SHA-256 of the text read from it; it changes when a refresh reads something new.
file_sha256string | null · required
SHA-256 of the uploaded file’s bytes. Null for a link or a site.
used_in_repliesinteger · required
How many replies have relied on a passage of it.
rules_fitboolean | null · required
For a rules source: true when every one of the business’s rules reaches every reply; false when they no longer fit in one reply, and each reply gets the rules nearest its message. Null for reference material, or when it was not measured.
rules_notestring | null · required
When rules_fit is false, in a sentence: how much the rules come to, what one reply carries, and what to do about it.
created_atstring (date-time) · required
When it was added.
updated_atstring (date-time) · required
When it last changed.
fetched_atstring (date-time) | null · required
When it was last read. Null until it has been.
{
  "id": "6b1d4f8a-3c2e-4a9f-b7d0-1e5c8a2f4d63",
  "kind": "site",
  "url": "https://www.example.com/",
  "filename": null,
  "title": "Example Co — website",
  "rules": false,
  "scope": "project",
  "status": "ready",
  "status_reason": null,
  "warning": null,
  "incomplete_pages": [],
  "pages": 24,
  "passages": 187,
  "characters": 96412,
  "size_bytes": null,
  "content_sha256": "4f2a9c0e7b1d3e5f6a8c9b0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
  "file_sha256": null,
  "used_in_replies": 38,
  "rules_fit": null,
  "rules_note": null,
  "created_at": "2026-10-09T15:20:04.000Z",
  "updated_at": "2026-10-09T15:21:37.000Z",
  "fetched_at": "2026-10-09T15:21:37.000Z"
}

Errors

bad_request400
The source id is not a UUID.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry brain:read.
not_found404
No such source for this key.
rate_limited429
Over this route’s per-minute limit, counted per account and per key. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

PATCH/v1/developer/knowledge/sources/{sourceId}

Mark a source as rules, or rename it

Mark a source as binding rules or as reference material, or rename it.

Scope brain:writeFree60 requests a minute

curl -X PATCH "https://api.slyos.world/v1/developer/knowledge/sources/$SOURCE_ID" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "rules": true
}'

Parameters

sourceIdstring (uuid) · in path · required
A knowledge source’s id, from adding it or listing them.

Request body

rulesboolean
True to make it a binding rule; false to make it reference material.
titlestring | null
A new name for it; null to go back to the name it was read with.at most 300 characters

200 response

Changed. The next reply uses it as changed.

idstring (uuid) · required
The source’s id.
kindstring · required
link: one page. site: a whole site, read from one address. file: an upload.one of: link, site, file
urlstring | null · required
The address it is read from. Null for a file.
filenamestring | null · required
The uploaded file’s name. Null for a link or a site.
titlestring | null · required
What it is called on the Developer screen and in a reply’s sources.
rulesboolean · required
True when replies must follow it; false when it is reference material.
scopestring · required
project: replies asked for with this key’s project use it. account: replies in every project on the account do.one of: project, account
statusstring · required
queued, then processing, while it is read in the background; ready once replies use it; failed when it could not be read, with status_reason saying why.one of: queued, processing, ready, failed
status_reasonstring | null · required
In a sentence, why the status is what it is. Null when there is nothing to say.
warningstring | null · required
Set when the source is ready but some of what it shows may be missing, in a sentence naming the pages and what to do — today, pages that build their content with JavaScript, which SlyOS does not run (a price list drawn from a script, a template filled in after the page loads). Null when there is nothing to warn about.
incomplete_pagesarray of string · required
The addresses of the pages warning names: pages whose content is built with JavaScript, so replies may be missing what a visitor sees there. Add that text as a file, or as a single link to a page that shows it. Empty when there are none.
pagesinteger · required
Pages read from it.
passagesinteger · required
Passages it was cut into: what a reply retrieves from.
charactersinteger · required
Characters of text read from it.
size_bytesinteger | null · required
The uploaded file’s size. Null for a link or a site.
content_sha256string | null · required
SHA-256 of the text read from it; it changes when a refresh reads something new.
file_sha256string | null · required
SHA-256 of the uploaded file’s bytes. Null for a link or a site.
used_in_repliesinteger · required
How many replies have relied on a passage of it.
rules_fitboolean | null · required
For a rules source: true when every one of the business’s rules reaches every reply; false when they no longer fit in one reply, and each reply gets the rules nearest its message. Null for reference material, or when it was not measured.
rules_notestring | null · required
When rules_fit is false, in a sentence: how much the rules come to, what one reply carries, and what to do about it.
created_atstring (date-time) · required
When it was added.
updated_atstring (date-time) · required
When it last changed.
fetched_atstring (date-time) | null · required
When it was last read. Null until it has been.
{
  "id": "6b1d4f8a-3c2e-4a9f-b7d0-1e5c8a2f4d63",
  "kind": "site",
  "url": "https://www.example.com/",
  "filename": null,
  "title": "Example Co — website",
  "rules": true,
  "scope": "project",
  "status": "ready",
  "status_reason": null,
  "warning": null,
  "incomplete_pages": [],
  "pages": 24,
  "passages": 187,
  "characters": 96412,
  "size_bytes": null,
  "content_sha256": "4f2a9c0e7b1d3e5f6a8c9b0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
  "file_sha256": null,
  "used_in_replies": 38,
  "rules_fit": true,
  "rules_note": null,
  "created_at": "2026-10-09T15:20:04.000Z",
  "updated_at": "2026-10-09T15:21:37.000Z",
  "fetched_at": "2026-10-09T15:21:37.000Z"
}

Errors

bad_request400
The source id is not a UUID, or the body is malformed: nothing to change (send rules or title), or a title over 300 characters.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry brain:write.
sandbox_read_only403
A sandbox key can read knowledge but not change it.
not_found404
No such source for this key.
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

DELETE/v1/developer/knowledge/sources/{sourceId}

Delete a source

Delete a source and every passage read from it. Replies stop using it at once.

Scope brain:writeFree60 requests a minute

curl -X DELETE "https://api.slyos.world/v1/developer/knowledge/sources/$SOURCE_ID" \
  -H "Authorization: Bearer $SLYOS_KEY"

Parameters

sourceIdstring (uuid) · in path · required
A knowledge source’s id, from adding it or listing them.

200 response

Deleted, with every passage read from it. No reply uses it from now on.

idstring (uuid) · required
The source that was deleted.
deletedtrue · required
Always true on a 200.
{
  "id": "6b1d4f8a-3c2e-4a9f-b7d0-1e5c8a2f4d63",
  "deleted": true
}

Errors

bad_request400
The source id is not a UUID.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry brain:write.
sandbox_read_only403
A sandbox key can read knowledge but not change it.
not_found404
No such source for this key.
rate_limited429
Over this route’s per-minute limit, counted per account and per key. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

GET/v1/developer/knowledge/sources/{sourceId}/passages

Read what was taken from a source

The passages read from a source, in order, with page, section and URL: exactly what replies retrieve from. Free.

Scope brain:readFree120 requests a minute

Exactly the passages replies retrieve from — check here what SlyOS read.

curl "https://api.slyos.world/v1/developer/knowledge/sources/$SOURCE_ID/passages" \
  -H "Authorization: Bearer $SLYOS_KEY"

Parameters

sourceIdstring (uuid) · in path · required
A knowledge source’s id, from adding it or listing them.
limitinteger · in query
How many passages to return.default: 50 · at least 1 · at most 200
afterinteger · in query
Return passages after this ordinal: the previous page’s next_after.default: -1

200 response

The source exists for this key.

source_idstring (uuid) · required
The source the passages belong to.
passagesarray of object · required
The passages, in order.
passages[].idstring (uuid) · required
The passage’s id: a reply’s sources[].passage_id.
passages[].ordinalinteger · required
Its place in the source, from 0.
passages[].pageinteger | null · required
The page of the file its own text starts on, if the file has pages.
passages[].page_endinteger | null · required
The last page it runs onto, when it runs past page. Null when it stays on one page, the file has no pages, or it was read before page ranges were recorded.
passages[].sectionstring | null · required
The heading it sits under, if any.
passages[].urlstring | null · required
The page of a site it was read from.
passages[].textstring · required
The passage, exactly as replies are given it.
next_afterinteger | null · required
Pass as after for the next page. Null on the last page.
{
  "source_id": "6b1d4f8a-3c2e-4a9f-b7d0-1e5c8a2f4d63",
  "passages": [
    {
      "id": "2f9e4c1a-7b3d-4e8f-a5c2-6d1b9e3f7a40",
      "ordinal": 0,
      "page": null,
      "page_end": null,
      "section": "Shipping",
      "url": "https://www.example.com/shipping",
      "text": "We ship to the US and Canada. Orders leave our warehouse within two business days."
    }
  ],
  "next_after": null
}

Errors

bad_request400
The source id is not a UUID, or limit is not 1 to 200, or after is not a whole number from -1.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry brain:read.
not_found404
No such source for this key.
rate_limited429
Over this route’s per-minute limit, counted per account and per key. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

POST/v1/developer/knowledge/sources/{sourceId}/refresh

Read a link or a site again

Read a link or a site again. The copy already read keeps answering until the new one is ready.

Scope brain:writeFree30 requests a minute

Refresh after your site or a regulation page changes; replies use the new text once it is ready.

curl -X POST "https://api.slyos.world/v1/developer/knowledge/sources/$SOURCE_ID/refresh" \
  -H "Authorization: Bearer $SLYOS_KEY"

Parameters

sourceIdstring (uuid) · in path · required
A knowledge source’s id, from adding it or listing them.

202 response

Queued. The copy already read keeps answering until the new one is ready.

idstring (uuid) · required
The source’s id.
kindstring · required
link: one page. site: a whole site, read from one address. file: an upload.one of: link, site, file
urlstring | null · required
The address it is read from. Null for a file.
filenamestring | null · required
The uploaded file’s name. Null for a link or a site.
titlestring | null · required
What it is called on the Developer screen and in a reply’s sources.
rulesboolean · required
True when replies must follow it; false when it is reference material.
scopestring · required
project: replies asked for with this key’s project use it. account: replies in every project on the account do.one of: project, account
statusstring · required
queued, then processing, while it is read in the background; ready once replies use it; failed when it could not be read, with status_reason saying why.one of: queued, processing, ready, failed
status_reasonstring | null · required
In a sentence, why the status is what it is. Null when there is nothing to say.
warningstring | null · required
Set when the source is ready but some of what it shows may be missing, in a sentence naming the pages and what to do — today, pages that build their content with JavaScript, which SlyOS does not run (a price list drawn from a script, a template filled in after the page loads). Null when there is nothing to warn about.
incomplete_pagesarray of string · required
The addresses of the pages warning names: pages whose content is built with JavaScript, so replies may be missing what a visitor sees there. Add that text as a file, or as a single link to a page that shows it. Empty when there are none.
pagesinteger · required
Pages read from it.
passagesinteger · required
Passages it was cut into: what a reply retrieves from.
charactersinteger · required
Characters of text read from it.
size_bytesinteger | null · required
The uploaded file’s size. Null for a link or a site.
content_sha256string | null · required
SHA-256 of the text read from it; it changes when a refresh reads something new.
file_sha256string | null · required
SHA-256 of the uploaded file’s bytes. Null for a link or a site.
used_in_repliesinteger · required
How many replies have relied on a passage of it.
rules_fitboolean | null · required
For a rules source: true when every one of the business’s rules reaches every reply; false when they no longer fit in one reply, and each reply gets the rules nearest its message. Null for reference material, or when it was not measured.
rules_notestring | null · required
When rules_fit is false, in a sentence: how much the rules come to, what one reply carries, and what to do about it.
created_atstring (date-time) · required
When it was added.
updated_atstring (date-time) · required
When it last changed.
fetched_atstring (date-time) | null · required
When it was last read. Null until it has been.
{
  "id": "6b1d4f8a-3c2e-4a9f-b7d0-1e5c8a2f4d63",
  "kind": "site",
  "url": "https://www.example.com/",
  "filename": null,
  "title": "Example Co — website",
  "rules": false,
  "scope": "project",
  "status": "queued",
  "status_reason": null,
  "warning": null,
  "incomplete_pages": [],
  "pages": 24,
  "passages": 187,
  "characters": 96412,
  "size_bytes": null,
  "content_sha256": "4f2a9c0e7b1d3e5f6a8c9b0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
  "file_sha256": null,
  "used_in_replies": 38,
  "rules_fit": null,
  "rules_note": null,
  "created_at": "2026-10-09T15:20:04.000Z",
  "updated_at": "2026-10-09T15:21:37.000Z",
  "fetched_at": "2026-10-09T15:21:37.000Z"
}

Errors

bad_request400
The source id is not a UUID.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry brain:write.
sandbox_read_only403
A sandbox key can read knowledge but not change it.
not_found404
No such source for this key.
cannot_refresh409
A file is read once and not kept. Upload the new version instead.
busy409
That source is being read right now.
rate_limited429
Over this route’s per-minute limit, counted per account and per key. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

GET/v1/developer/knowledge/profile

Read the business profile

The business profile this key’s replies use: name, description, tone notes. Free.

Scope brain:readFree120 requests a minute

curl "https://api.slyos.world/v1/developer/knowledge/profile" \
  -H "Authorization: Bearer $SLYOS_KEY"

200 response

Always, for a valid key.

business_namestring | null · required
The business’s name, as replies give it.
descriptionstring | null · required
What the business is and does, in its own words.
tone_notesstring | null · required
How replies should sound.
use_personal_contextboolean · required
Whether API replies also use everything SlyOS knows about the account owner: facts from their own conversations and the ones they told it, their past replies on their other channels, their documents, calendar and saved details, and their About you. On unless the owner turned it off on the Developer screen; a key cannot change it. Off, a reply still uses the owner’s name, rules and writing style. Either way, nothing one customer wrote or was sent goes into another customer’s reply, and nothing from a sandbox conversation goes into a live one; every reply the account sends still shapes its writing style.
updated_atstring (date-time) | null · required
When the profile last changed. Null if it never has.
{
  "business_name": "Example Co",
  "description": "We make and sell refillable kitchen goods online, shipping to the US and Canada. We do not take phone orders.",
  "tone_notes": "Warm and brief. Sign off as “The Example Co team”.",
  "use_personal_context": true,
  "updated_at": "2026-10-09T15:18:52.000Z"
}

Errors

unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry brain:read.
rate_limited429
Over this route’s per-minute limit, counted per account and per key. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

PUT/v1/developer/knowledge/profile

Say who the business is and how it sounds

Set what the business is and how it wants to sound. Used in every API reply of this key’s project.

Scope brain:writeFree30 requests a minute

A field you leave out is left as it was; null clears it. Whether replies use everything SlyOS knows about the owner (use_personal_context) is the owner’s switch, on the Developer screen: a key may send back the value GET answered, and a key that tries to change it is refused.

curl -X PUT "https://api.slyos.world/v1/developer/knowledge/profile" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "business_name": "Example Co",
  "description": "We make and sell refillable kitchen goods online, shipping to the US and Canada. We do not take phone orders.",
  "tone_notes": "Warm and brief. Sign off as “The Example Co team”."
}'

Request body

business_namestring | null
The business’s name, as replies should give it.at most 200 characters
descriptionstring | null
What the business is and does, in your own words: what it sells, who it serves, what it will and will not do.at most 8,000 characters
tone_notesstring | null
How replies should sound — formal or warm, how to sign off — on top of the voice SlyOS learns from your own replies.at most 2,000 characters
use_personal_contextboolean
Accepted only when it equals the stored value, so the object GET returns can be sent back. A key that tries to change it, either way, is refused: only the account owner can, on the Developer screen.
updated_atstring | null
Accepted and ignored, so the object GET returns can be sent back.

200 response

Saved. Every reply asked for with this key’s project uses it from now on.

business_namestring | null · required
The business’s name, as replies give it.
descriptionstring | null · required
What the business is and does, in its own words.
tone_notesstring | null · required
How replies should sound.
use_personal_contextboolean · required
Whether API replies also use everything SlyOS knows about the account owner: facts from their own conversations and the ones they told it, their past replies on their other channels, their documents, calendar and saved details, and their About you. On unless the owner turned it off on the Developer screen; a key cannot change it. Off, a reply still uses the owner’s name, rules and writing style. Either way, nothing one customer wrote or was sent goes into another customer’s reply, and nothing from a sandbox conversation goes into a live one; every reply the account sends still shapes its writing style.
updated_atstring (date-time) | null · required
When the profile last changed. Null if it never has.
{
  "business_name": "Example Co",
  "description": "We make and sell refillable kitchen goods online, shipping to the US and Canada. We do not take phone orders.",
  "tone_notes": "Warm and brief. Sign off as “The Example Co team”.",
  "use_personal_context": true,
  "updated_at": "2026-10-09T15:18:52.000Z"
}

Errors

bad_request400
The body is malformed — a field this route does not take, or one longer than it may be — or it tries to change use_personal_context, which only the account owner can, on the Developer screen.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
missing_scope403
The key does not carry brain:write.
sandbox_read_only403
A sandbox key can read knowledge but not change it.
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

POST/v1/developer/knowledge/uploads/{token}

Upload a file to a one-time address

Where the account’s own screen uploads a file directly (multipart `file`), with a one-time token from developerKnowledgeUploadUrl in place of a key.

Free30 requests a minute

For the Developer screen’s own uploads: the address carries a one-time token in place of a key. From your server, use POST /v1/developer/knowledge/sources.

curl -X POST "https://api.slyos.world/v1/developer/knowledge/uploads/$TOKEN" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -F 'file=@returns-policy.pdf' \
  -F 'title=Returns policy'

Parameters

tokenstring · in path · required
The one-time token in the upload address SlyOS gave the Developer screen.20–200 characters

Request body — multipart/form-data

filestring (binary) · required
The file — PDF, Word (.docx), plain text, Markdown or CSV, up to the size this operation states. It is read to the end; a long one is cut into passages, not cut short.
titlestring
What to call it on the Developer screen and in a reply’s sources.

202 response

Taken in, and read in the background.

idstring (uuid) · required
The source’s id.
kindstring · required
link: one page. site: a whole site, read from one address. file: an upload.one of: link, site, file
urlstring | null · required
The address it is read from. Null for a file.
filenamestring | null · required
The uploaded file’s name. Null for a link or a site.
titlestring | null · required
What it is called on the Developer screen and in a reply’s sources.
rulesboolean · required
True when replies must follow it; false when it is reference material.
scopestring · required
project: replies asked for with this key’s project use it. account: replies in every project on the account do.one of: project, account
statusstring · required
queued, then processing, while it is read in the background; ready once replies use it; failed when it could not be read, with status_reason saying why.one of: queued, processing, ready, failed
status_reasonstring | null · required
In a sentence, why the status is what it is. Null when there is nothing to say.
warningstring | null · required
Set when the source is ready but some of what it shows may be missing, in a sentence naming the pages and what to do — today, pages that build their content with JavaScript, which SlyOS does not run (a price list drawn from a script, a template filled in after the page loads). Null when there is nothing to warn about.
incomplete_pagesarray of string · required
The addresses of the pages warning names: pages whose content is built with JavaScript, so replies may be missing what a visitor sees there. Add that text as a file, or as a single link to a page that shows it. Empty when there are none.
pagesinteger · required
Pages read from it.
passagesinteger · required
Passages it was cut into: what a reply retrieves from.
charactersinteger · required
Characters of text read from it.
size_bytesinteger | null · required
The uploaded file’s size. Null for a link or a site.
content_sha256string | null · required
SHA-256 of the text read from it; it changes when a refresh reads something new.
file_sha256string | null · required
SHA-256 of the uploaded file’s bytes. Null for a link or a site.
used_in_repliesinteger · required
How many replies have relied on a passage of it.
rules_fitboolean | null · required
For a rules source: true when every one of the business’s rules reaches every reply; false when they no longer fit in one reply, and each reply gets the rules nearest its message. Null for reference material, or when it was not measured.
rules_notestring | null · required
When rules_fit is false, in a sentence: how much the rules come to, what one reply carries, and what to do about it.
created_atstring (date-time) · required
When it was added.
updated_atstring (date-time) · required
When it last changed.
fetched_atstring (date-time) | null · required
When it was last read. Null until it has been.
duplicateboolean · required
True when this address or this exact file was already a source: nothing new was added, and this is that source.
{
  "id": "0e7c2a9d-5f1b-4d38-a6e4-9b3c1f7d2a85",
  "kind": "file",
  "url": null,
  "filename": "returns-policy.pdf",
  "title": "Returns policy",
  "rules": true,
  "scope": "project",
  "status": "queued",
  "status_reason": null,
  "warning": null,
  "incomplete_pages": [],
  "pages": 0,
  "passages": 0,
  "characters": 0,
  "size_bytes": 284311,
  "content_sha256": null,
  "file_sha256": "9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c",
  "used_in_replies": 0,
  "rules_fit": true,
  "rules_note": null,
  "created_at": "2026-10-09T15:20:04.000Z",
  "updated_at": "2026-10-09T15:20:04.000Z",
  "fetched_at": null,
  "duplicate": false
}

200 response

That exact file is already a source: this is it. The same fields as the 202 response.

{
  "id": "0e7c2a9d-5f1b-4d38-a6e4-9b3c1f7d2a85",
  "kind": "file",
  "url": null,
  "filename": "returns-policy.pdf",
  "title": "Returns policy",
  "rules": true,
  "scope": "project",
  "status": "ready",
  "status_reason": null,
  "warning": null,
  "incomplete_pages": [],
  "pages": 0,
  "passages": 0,
  "characters": 0,
  "size_bytes": 284311,
  "content_sha256": null,
  "file_sha256": "9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c",
  "used_in_replies": 0,
  "rules_fit": true,
  "rules_note": null,
  "created_at": "2026-10-09T15:20:04.000Z",
  "updated_at": "2026-10-09T15:20:04.000Z",
  "fetched_at": null,
  "duplicate": true
}

Errors

bad_request400
No file in the upload, or the upload address is malformed.
not_found404
The upload address is unknown, used, or expired. Ask for a new one.
too_many_sources409
The account already holds the most knowledge sources it may.
knowledge_full409
The account already holds the most knowledge text it may.
too_large413
The file is over 25 MB.
unsupported_type422
Not a file type SlyOS reads (PDF, Word .docx, plain text, Markdown, CSV).
rate_limited429
Over this route’s per-minute limit, counted per account. Wait a minute.
busy429
Too many sources are being read at once. Add more when they finish.
uploads_busy429
Two files are already uploading for this account. Send the next after.
internal500
An unexpected fault on SlyOS’s side. It is recorded.

Videos

POST/v1/developer/videos

Start a video

Start a video: your own script, or none yet. `kind: check` starts one whose only take is a video you want checked. The same `request_id` twice makes one video.

Scope video:writeFree20 requests a minute

Retry safely with the same request_id: you get the same object, spent once. A request_id reused for different work is refused 409 video_request_reused.

Starting a video spends nothing. Uploading takes counts footage; making the video spends a video.

curl -X POST "https://api.slyos.world/v1/developer/videos" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "request_id": "5f7b9d1e-3a5c-4e7a-9c1e-3b5d7f9a1c86",
  "platform": "both",
  "target_seconds": 40,
  "script": {
    "title": "Refills ship free",
    "lines": [
      {
        "text": "You can stop buying new bottles today.",
        "role": "hook",
        "on_screen": "Stop buying bottles"
      },
      {
        "text": "Every Example Co bottle refills, and refills now ship free in the US and Canada.",
        "role": "line",
        "on_screen": null
      },
      {
        "text": "Orders leave our warehouse within two business days.",
        "role": "line",
        "on_screen": null
      },
      {
        "text": "Tap the link in our bio to get your first refill.",
        "role": "cta",
        "on_screen": "Link in bio"
      }
    ],
    "caption": "Refill, don’t rebuy. Free refill shipping in the US and Canada.",
    "hashtags": [
      "#refill",
      "#zerowaste"
    ],
    "source_ids": [
      "0a2c4e6b-8d1f-4a3c-9e5b-7d9f1b3d5f78"
    ]
  }
}'

Request body

request_idstring (uuid) · required
Your idempotency id, a UUID. The same one twice makes one video; reusing it for different work is refused.
kindstring
make (the default) or check.one of: make, check · default: make
titlestring
What it is called. Defaults to the script’s title.1–120 characters
platformstring
tiktok, instagram or both (the default).one of: tiktok, instagram, both · default: both
target_secondsinteger
The length to aim for, 10–180 seconds. Default 45.default: 45 · at least 10 · at most 180
scriptobject
Your own script. Leave it out to add takes without one.
script.titlestring · required
What the video is called. The downloaded file is named after it.1–120 characters
script.linesarray of object · required
The spoken lines, in the order they are written.
script.lines[].textstring · required
What is said.1–400 characters
script.lines[].rolestring · required
hook opens the video wherever it sits in the script; cta closes it; line is everything between. At most one hook.one of: hook, line, cta
script.lines[].on_screenstring | null · required
Big words shown while the line is said, or null.1–60 characters
script.captionstring · required
The caption to post with it — up to 2,200 characters, Instagram’s limit.at most 2,200 characters
script.hashtagsarray of string · required
Up to 10 hashtags, each # and letters, digits or _.
script.source_idsarray of string (uuid) · required
The About-you sources the script drew on, by id, so its facts can be traced. Empty when it drew on none.

201 response

The video exists — made now, or made before with this request_id. Add takes next.

idstring (uuid) · required
The video’s id. Every other video endpoint takes it.
kindstring · required
make: script, takes, finished video. check: a video uploaded only to be checked.one of: make, check
titlestring · required
What it is called.
statestring · required
Where it stands, worked out from its takes, jobs and cuts: script, takes, making, ready or failed.one of: script, takes, making, ready, failed
platformstring · required
tiktok, instagram or both. One 9:16 file suits both.one of: tiktok, instagram, both
target_secondsinteger · required
The length the cut aims for, in seconds.
versionsvalue · required
1, or 3 cuts to pick from where the plan makes three.
scriptobject · required
The script. Null for a check video, or before one is written.
script.titlestring · required
What the video is called. The downloaded file is named after it.1–120 characters
script.linesarray of object · required
The spoken lines, in the order they are written.
script.lines[].textstring · required
What is said.1–400 characters
script.lines[].rolestring · required
hook opens the video wherever it sits in the script; cta closes it; line is everything between. At most one hook.one of: hook, line, cta
script.lines[].on_screenstring | null · required
Big words shown while the line is said, or null.1–60 characters
script.captionstring · required
The caption to post with it — up to 2,200 characters, Instagram’s limit.at most 2,200 characters
script.hashtagsarray of string · required
Up to 10 hashtags, each # and letters, digits or _.
script.source_idsarray of string (uuid) · required
The About-you sources the script drew on, by id, so its facts can be traced. Empty when it drew on none.
takesarray of object · required
Every take, in the order added.
takes[].idstring (uuid) · required
The take’s id.
takes[].ninteger · required
Take 1, 2, 3… in the order they were added.
takes[].filenamestring · required
The name the file was sent with. Never part of a storage key.
takes[].statestring · required
uploading → uploaded → preparing → ready. Or failed (failure says why) or discarded.one of: uploading, uploaded, preparing, ready, failed, discarded
takes[].secondsnumber | null · required
How long the take runs, as SlyOS measured it. Null until measured.
takes[].poster_urlstring (uri) | null · required
A still from the take, good for an hour. Null until it is prepared.
takes[].failurestring | null · required
Why the take cannot be used, in a sentence. Set exactly when state is failed.
takes[].notestring | null · required
Something worth knowing that is not a failure — "No face found, so it is framed in the middle." Null when there is nothing.
takes[].uploadobject · required
While state is uploading: how the file is cut into parts and which parts storage holds. Null otherwise.
takes[].upload.part_bytesinteger · required
Bytes in every part but the last (16,777,216).
takes[].upload.part_countinteger · required
How many parts the file is cut into.
takes[].upload.parts_donearray of integer | null · required
Part numbers storage already holds — skip them when resuming. Null when SlyOS cannot list them: resume from your own record.
takes[].created_atstring (date-time) · required
When the take was added.
jobobject · required
The job running or queued now, else the last one. Null when there has been none.
job.idstring (uuid) · required
The job’s id.
job.kindstring · required
prepare a take, assemble the video, or evaluate (check) it.one of: prepare, assemble, evaluate
job.statestring · required
queued, running, succeeded, failed or canceled.one of: queued, running, succeeded, failed, canceled
job.stepstring · required
What the job is doing now. step_words says it in words.one of: waiting, probing, listening, framing, proxying, choosing, cutting, rendering, finishing, watching, reading, judging
job.step_wordsstring · required
The step in plain words — the same words every SlyOS screen shows.
job.progressinteger | null · required
0–100, from a count SlyOS really has (parts rendered of parts to render). Null when there is none; never an estimate.at most 100
job.aheadinteger | null · required
Jobs of other accounts ahead of this one while it waits. Null once it runs.
job.failurestring | null · required
Why it failed, in a sentence. Set exactly when state is failed.
job.created_atstring (date-time) · required
When it was queued.
job.finished_atstring (date-time) | null · required
When it succeeded, failed or was canceled. Null until then.
rendersarray of object · required
Finished cuts, newest assembly first; within one, A then B then C.
renders[].idstring (uuid) · required
The cut’s id. Mint a download link for it with the download endpoint.
renders[].labelstring · required
A, B or C: the order the versions are shown in.1–2 characters
renders[].stylestring · required
tight (the shortest that keeps every point), story (keeps the breaths that carry it) or punchy (opens on the strongest line, fastest cuts).one of: tight, story, punchy
renders[].statestring · required
rendering, ready, failed, or expired — kept past the plan’s days and its files deleted.one of: rendering, ready, failed, expired
renders[].secondsnumber | null · required
How long it runs. Null until rendered.
renders[].widthinteger | null · required
Pixels across (1080). Null until rendered.
renders[].heightinteger | null · required
Pixels down (1920). Null until rendered.
renders[].bytesinteger | null · required
The MP4’s size. Null until rendered.
renders[].poster_urlstring (uri) | null · required
A still to show before it plays, good for an hour. Null until rendered.
renders[].preview_urlstring (uri) | null · required
The cut to PLAY inline, good for an hour. To download it, use the download endpoint. Null until rendered.
renders[].chosenboolean · required
Picked from the versions. The next change starts from it.
renders[].kept_untilstring (date-time) | null · required
When its files are deleted. Download it before then to keep it.
renders[].created_atstring (date-time) · required
When the assembly that made it started.
captionstring | null · required
The caption to post with the chosen cut. Null until written.
hashtagsarray of string · required
The hashtags to post with it.
changes_leftinteger · required
Changes left ("make it shorter") before the next assembly spends another of the month’s videos.
evaluation_idsarray of string (uuid) · required
Its checks, newest first. Read each with GET /v1/developer/video-evaluations/{evaluationId}.
created_atstring (date-time) · required
When it was started.
updated_atstring (date-time) · required
When anything about it last changed.
{
  "id": "a3c1e5f7-2b4d-4e6f-8a1c-3e5f7a9b1d24",
  "kind": "make",
  "title": "Refills ship free",
  "state": "script",
  "platform": "both",
  "target_seconds": 40,
  "versions": 3,
  "script": {
    "title": "Refills ship free",
    "lines": [
      {
        "text": "You can stop buying new bottles today.",
        "role": "hook",
        "on_screen": "Stop buying bottles"
      },
      {
        "text": "Every Example Co bottle refills, and refills now ship free in the US and Canada.",
        "role": "line",
        "on_screen": null
      },
      {
        "text": "Orders leave our warehouse within two business days.",
        "role": "line",
        "on_screen": null
      },
      {
        "text": "Tap the link in our bio to get your first refill.",
        "role": "cta",
        "on_screen": "Link in bio"
      }
    ],
    "caption": "Refill, don’t rebuy. Free refill shipping in the US and Canada.",
    "hashtags": [
      "#refill",
      "#zerowaste"
    ],
    "source_ids": [
      "0a2c4e6b-8d1f-4a3c-9e5b-7d9f1b3d5f78"
    ]
  },
  "takes": [],
  "job": null,
  "renders": [],
  "caption": "Refill, don’t rebuy. Free refill shipping in the US and Canada.",
  "hashtags": [
    "#refill",
    "#zerowaste"
  ],
  "changes_left": 3,
  "evaluation_ids": [],
  "created_at": "2026-10-21T18:00:00.000Z",
  "updated_at": "2026-10-21T18:00:00.000Z"
}

Errors

bad_request400
The body does not match ApiCreateVideoRequest.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
video_not_in_plan402
The account’s plan makes no videos.
missing_scope403
The key does not carry video:write.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
video_request_reused409
That request_id already started something else. Do not retry with it; use a new one.
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

GET/v1/developer/videos/{videoId}

Read a video

Everything about one video: its script, every take and its upload, the job under way, the finished cuts with links to play them, the caption and hashtags.

Scope video:readFree120 requests a minute

Poll this while a job runs — every few seconds is plenty — or wait for video.ready. The poster_url and preview_url links work for an hour; read the video again for fresh ones.

curl "https://api.slyos.world/v1/developer/videos/$VIDEO_ID" \
  -H "Authorization: Bearer $SLYOS_KEY"

Parameters

videoIdstring (uuid) · in path · required
The video, as id on its VideoProjectView.

200 response

The video, as it stands now.

idstring (uuid) · required
The video’s id. Every other video endpoint takes it.
kindstring · required
make: script, takes, finished video. check: a video uploaded only to be checked.one of: make, check
titlestring · required
What it is called.
statestring · required
Where it stands, worked out from its takes, jobs and cuts: script, takes, making, ready or failed.one of: script, takes, making, ready, failed
platformstring · required
tiktok, instagram or both. One 9:16 file suits both.one of: tiktok, instagram, both
target_secondsinteger · required
The length the cut aims for, in seconds.
versionsvalue · required
1, or 3 cuts to pick from where the plan makes three.
scriptobject · required
The script. Null for a check video, or before one is written.
script.titlestring · required
What the video is called. The downloaded file is named after it.1–120 characters
script.linesarray of object · required
The spoken lines, in the order they are written.
script.lines[].textstring · required
What is said.1–400 characters
script.lines[].rolestring · required
hook opens the video wherever it sits in the script; cta closes it; line is everything between. At most one hook.one of: hook, line, cta
script.lines[].on_screenstring | null · required
Big words shown while the line is said, or null.1–60 characters
script.captionstring · required
The caption to post with it — up to 2,200 characters, Instagram’s limit.at most 2,200 characters
script.hashtagsarray of string · required
Up to 10 hashtags, each # and letters, digits or _.
script.source_idsarray of string (uuid) · required
The About-you sources the script drew on, by id, so its facts can be traced. Empty when it drew on none.
takesarray of object · required
Every take, in the order added.
takes[].idstring (uuid) · required
The take’s id.
takes[].ninteger · required
Take 1, 2, 3… in the order they were added.
takes[].filenamestring · required
The name the file was sent with. Never part of a storage key.
takes[].statestring · required
uploading → uploaded → preparing → ready. Or failed (failure says why) or discarded.one of: uploading, uploaded, preparing, ready, failed, discarded
takes[].secondsnumber | null · required
How long the take runs, as SlyOS measured it. Null until measured.
takes[].poster_urlstring (uri) | null · required
A still from the take, good for an hour. Null until it is prepared.
takes[].failurestring | null · required
Why the take cannot be used, in a sentence. Set exactly when state is failed.
takes[].notestring | null · required
Something worth knowing that is not a failure — "No face found, so it is framed in the middle." Null when there is nothing.
takes[].uploadobject · required
While state is uploading: how the file is cut into parts and which parts storage holds. Null otherwise.
takes[].upload.part_bytesinteger · required
Bytes in every part but the last (16,777,216).
takes[].upload.part_countinteger · required
How many parts the file is cut into.
takes[].upload.parts_donearray of integer | null · required
Part numbers storage already holds — skip them when resuming. Null when SlyOS cannot list them: resume from your own record.
takes[].created_atstring (date-time) · required
When the take was added.
jobobject · required
The job running or queued now, else the last one. Null when there has been none.
job.idstring (uuid) · required
The job’s id.
job.kindstring · required
prepare a take, assemble the video, or evaluate (check) it.one of: prepare, assemble, evaluate
job.statestring · required
queued, running, succeeded, failed or canceled.one of: queued, running, succeeded, failed, canceled
job.stepstring · required
What the job is doing now. step_words says it in words.one of: waiting, probing, listening, framing, proxying, choosing, cutting, rendering, finishing, watching, reading, judging
job.step_wordsstring · required
The step in plain words — the same words every SlyOS screen shows.
job.progressinteger | null · required
0–100, from a count SlyOS really has (parts rendered of parts to render). Null when there is none; never an estimate.at most 100
job.aheadinteger | null · required
Jobs of other accounts ahead of this one while it waits. Null once it runs.
job.failurestring | null · required
Why it failed, in a sentence. Set exactly when state is failed.
job.created_atstring (date-time) · required
When it was queued.
job.finished_atstring (date-time) | null · required
When it succeeded, failed or was canceled. Null until then.
rendersarray of object · required
Finished cuts, newest assembly first; within one, A then B then C.
renders[].idstring (uuid) · required
The cut’s id. Mint a download link for it with the download endpoint.
renders[].labelstring · required
A, B or C: the order the versions are shown in.1–2 characters
renders[].stylestring · required
tight (the shortest that keeps every point), story (keeps the breaths that carry it) or punchy (opens on the strongest line, fastest cuts).one of: tight, story, punchy
renders[].statestring · required
rendering, ready, failed, or expired — kept past the plan’s days and its files deleted.one of: rendering, ready, failed, expired
renders[].secondsnumber | null · required
How long it runs. Null until rendered.
renders[].widthinteger | null · required
Pixels across (1080). Null until rendered.
renders[].heightinteger | null · required
Pixels down (1920). Null until rendered.
renders[].bytesinteger | null · required
The MP4’s size. Null until rendered.
renders[].poster_urlstring (uri) | null · required
A still to show before it plays, good for an hour. Null until rendered.
renders[].preview_urlstring (uri) | null · required
The cut to PLAY inline, good for an hour. To download it, use the download endpoint. Null until rendered.
renders[].chosenboolean · required
Picked from the versions. The next change starts from it.
renders[].kept_untilstring (date-time) | null · required
When its files are deleted. Download it before then to keep it.
renders[].created_atstring (date-time) · required
When the assembly that made it started.
captionstring | null · required
The caption to post with the chosen cut. Null until written.
hashtagsarray of string · required
The hashtags to post with it.
changes_leftinteger · required
Changes left ("make it shorter") before the next assembly spends another of the month’s videos.
evaluation_idsarray of string (uuid) · required
Its checks, newest first. Read each with GET /v1/developer/video-evaluations/{evaluationId}.
created_atstring (date-time) · required
When it was started.
updated_atstring (date-time) · required
When anything about it last changed.
{
  "id": "a3c1e5f7-2b4d-4e6f-8a1c-3e5f7a9b1d24",
  "kind": "make",
  "title": "Refills ship free",
  "state": "ready",
  "platform": "both",
  "target_seconds": 40,
  "versions": 3,
  "script": {
    "title": "Refills ship free",
    "lines": [
      {
        "text": "You can stop buying new bottles today.",
        "role": "hook",
        "on_screen": "Stop buying bottles"
      },
      {
        "text": "Every Example Co bottle refills, and refills now ship free in the US and Canada.",
        "role": "line",
        "on_screen": null
      },
      {
        "text": "Orders leave our warehouse within two business days.",
        "role": "line",
        "on_screen": null
      },
      {
        "text": "Tap the link in our bio to get your first refill.",
        "role": "cta",
        "on_screen": "Link in bio"
      }
    ],
    "caption": "Refill, don’t rebuy. Free refill shipping in the US and Canada.",
    "hashtags": [
      "#refill",
      "#zerowaste"
    ],
    "source_ids": [
      "0a2c4e6b-8d1f-4a3c-9e5b-7d9f1b3d5f78"
    ]
  },
  "takes": [
    {
      "id": "7e2a4c6f-1b3d-4f58-9a7c-2e4b6d8f0a13",
      "n": 1,
      "filename": "take-1.mov",
      "state": "ready",
      "seconds": 48.2,
      "poster_url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-work/7e2a4c6f-1b3d-4f58-9a7c-2e4b6d8f0a13?response-content-type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example",
      "failure": null,
      "note": null,
      "upload": null,
      "created_at": "2026-10-21T18:01:02.000Z"
    },
    {
      "id": "d4f6a8c0-2e4a-4c6e-9b1d-4f6a8c0e2b35",
      "n": 2,
      "filename": "take-2.mov",
      "state": "ready",
      "seconds": 51.7,
      "poster_url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-work/d4f6a8c0-2e4a-4c6e-9b1d-4f6a8c0e2b35?response-content-type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example",
      "failure": null,
      "note": null,
      "upload": null,
      "created_at": "2026-10-21T18:01:02.000Z"
    }
  ],
  "job": {
    "id": "c5e7a9b1-3d5f-4a7c-8e1b-5d7f9a1c3e57",
    "kind": "assemble",
    "state": "succeeded",
    "step": "finishing",
    "step_words": "Adding the last touches",
    "progress": 100,
    "ahead": null,
    "failure": null,
    "created_at": "2026-10-21T18:05:40.000Z",
    "finished_at": "2026-10-21T18:06:40.000Z"
  },
  "renders": [
    {
      "id": "1f3b5d7a-9c2e-4a4c-8e6b-0d2f4a6c8e19",
      "label": "A",
      "style": "tight",
      "state": "ready",
      "seconds": 34.6,
      "width": 1080,
      "height": 1920,
      "bytes": 20760000,
      "poster_url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-outputs/1f3b5d7a-9c2e-4a4c-8e6b-0d2f4a6c8e19?response-content-type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example",
      "preview_url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-outputs/1f3b5d7a-9c2e-4a4c-8e6b-0d2f4a6c8e19?response-content-type=video%2Fmp4&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example",
      "chosen": true,
      "kept_until": "2026-11-20T18:06:40.000Z",
      "created_at": "2026-10-21T18:05:40.000Z"
    },
    {
      "id": "2a4c6e8b-0d1f-4b3a-9c5e-7f9b1d3e5a20",
      "label": "B",
      "style": "story",
      "state": "ready",
      "seconds": 41.2,
      "width": 1080,
      "height": 1920,
      "bytes": 24720000,
      "poster_url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-outputs/2a4c6e8b-0d1f-4b3a-9c5e-7f9b1d3e5a20?response-content-type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example",
      "preview_url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-outputs/2a4c6e8b-0d1f-4b3a-9c5e-7f9b1d3e5a20?response-content-type=video%2Fmp4&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example",
      "chosen": false,
      "kept_until": "2026-11-20T18:06:40.000Z",
      "created_at": "2026-10-21T18:05:40.000Z"
    },
    {
      "id": "3b5d7f9c-1e2a-4c4b-a6d8-8e0c2f4a6b31",
      "label": "C",
      "style": "punchy",
      "state": "ready",
      "seconds": 37.9,
      "width": 1080,
      "height": 1920,
      "bytes": 22740000,
      "poster_url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-outputs/3b5d7f9c-1e2a-4c4b-a6d8-8e0c2f4a6b31?response-content-type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example",
      "preview_url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-outputs/3b5d7f9c-1e2a-4c4b-a6d8-8e0c2f4a6b31?response-content-type=video%2Fmp4&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example",
      "chosen": false,
      "kept_until": "2026-11-20T18:06:40.000Z",
      "created_at": "2026-10-21T18:05:40.000Z"
    }
  ],
  "caption": "Refill, don’t rebuy. Free refill shipping in the US and Canada.",
  "hashtags": [
    "#refill",
    "#zerowaste"
  ],
  "changes_left": 3,
  "evaluation_ids": [
    "6d8f0b2c-4e6a-4d8c-b0e2-a4c6e8f0b243"
  ],
  "created_at": "2026-10-21T18:00:00.000Z",
  "updated_at": "2026-10-21T18:06:40.000Z"
}

Errors

bad_request400
A path id is not a UUID.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
missing_scope403
The key does not carry video:read.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

DELETE/v1/developer/videos/{videoId}

Delete a video

Delete a video: its script, transcripts and checks at once, and every file of it queued for deletion from storage. What it used of the month is not given back.

Scope video:writeFree30 requests a minute

curl -X DELETE "https://api.slyos.world/v1/developer/videos/$VIDEO_ID" \
  -H "Authorization: Bearer $SLYOS_KEY"

Parameters

videoIdstring (uuid) · in path · required
The video, as id on its VideoProjectView.

200 response

Deleted.

deletedtrue · required
Always true. The video’s rows are gone and its files are queued for deletion from storage.
{
  "deleted": true
}

Errors

bad_request400
A path id is not a UUID.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
missing_scope403
The key does not carry video:write.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

POST/v1/developer/videos/{videoId}/takes

Upload a take

Open the upload of one take, straight to storage in parts: the take, its part size, and addresses for the first parts. Refused before a byte travels when the take is over the plan’s size or length, or the month’s footage.

Scope video:writeFree30 requests a minute

The file goes straight to storage, never through SlyOS: cut it into 16,777,216-byte parts (the last may be shorter), PUT each part's bytes to its address — three at a time is plenty — and keep each answer's ETag header. Then POST …/complete with every part's ETag. Addresses work for 15 minutes; ask for more, or fresh ones, with part-urls.

Interrupted? GET /v1/developer/videos/{videoId}/takes/{takeId} says which parts storage already holds (upload.parts_done); upload the rest. A take whose upload is not completed within a day is abandoned.

The plan’s limits are checked before any upload: a take’s size and length, takes per video, and footage per video and per month. Footage counts as soon as the take is opened, as declared, and is corrected to what SlyOS measures.

curl -X POST "https://api.slyos.world/v1/developer/videos/$VIDEO_ID/takes" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "filename": "take-1.mov",
  "mime_type": "video/quicktime",
  "size_bytes": 41943040,
  "duration_seconds": 48.2
}'

Parameters

videoIdstring (uuid) · in path · required
The video, as id on its VideoProjectView.

Request body

filenamestring · required
The file’s name, for people. Never part of a storage key.1–300 characters
mime_typestring · required
video/mp4, video/quicktime or video/webm. Checked against the bytes later.one of: video/mp4, video/quicktime, video/webm
size_bytesinteger · required
The file’s exact size in bytes. Completing the take checks it.
duration_secondsnumber · required
How long it runs, read on your side. Checked against the plan before any upload, and against SlyOS’s own measure after.at most 86400

201 response

The upload is open. PUT each part to its address, then complete the take.

takeobject · required
The take, uploading.
take.idstring (uuid) · required
The take’s id.
take.ninteger · required
Take 1, 2, 3… in the order they were added.
take.filenamestring · required
The name the file was sent with. Never part of a storage key.
take.statestring · required
uploading → uploaded → preparing → ready. Or failed (failure says why) or discarded.one of: uploading, uploaded, preparing, ready, failed, discarded
take.secondsnumber | null · required
How long the take runs, as SlyOS measured it. Null until measured.
take.poster_urlstring (uri) | null · required
A still from the take, good for an hour. Null until it is prepared.
take.failurestring | null · required
Why the take cannot be used, in a sentence. Set exactly when state is failed.
take.notestring | null · required
Something worth knowing that is not a failure — "No face found, so it is framed in the middle." Null when there is nothing.
take.uploadobject · required
While state is uploading: how the file is cut into parts and which parts storage holds. Null otherwise.
take.upload.part_bytesinteger · required
Bytes in every part but the last (16,777,216).
take.upload.part_countinteger · required
How many parts the file is cut into.
take.upload.parts_donearray of integer | null · required
Part numbers storage already holds — skip them when resuming. Null when SlyOS cannot list them: resume from your own record.
take.created_atstring (date-time) · required
When the take was added.
urlsarray of object · required
Addresses for the first parts — up to 20. Ask for the rest with part-urls.
urls[].part_numberinteger · required
Which part to PUT to this address (1-based).at least 1 · at most 10000
urls[].urlstring (uri) · required
A presigned PUT, good for fifteen minutes. Send the part’s bytes as the body and keep the ETag header of the answer: completing the take needs it.
expires_atstring (date-time) · required
When these addresses stop working. Ask for fresh ones with part-urls.
{
  "take": {
    "id": "7e2a4c6f-1b3d-4f58-9a7c-2e4b6d8f0a13",
    "n": 1,
    "filename": "take-1.mov",
    "state": "uploading",
    "seconds": null,
    "poster_url": null,
    "failure": null,
    "note": null,
    "upload": {
      "part_bytes": 16777216,
      "part_count": 3,
      "parts_done": []
    },
    "created_at": "2026-10-21T18:01:02.000Z"
  },
  "urls": [
    {
      "part_number": 1,
      "url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-takes/7e2a4c6f-1b3d-4f58-9a7c-2e4b6d8f0a13?partNumber=1&uploadId=example&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example"
    },
    {
      "part_number": 2,
      "url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-takes/7e2a4c6f-1b3d-4f58-9a7c-2e4b6d8f0a13?partNumber=2&uploadId=example&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example"
    },
    {
      "part_number": 3,
      "url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-takes/7e2a4c6f-1b3d-4f58-9a7c-2e4b6d8f0a13?partNumber=3&uploadId=example&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example"
    }
  ],
  "expires_at": "2026-10-21T18:16:02.000Z"
}

Errors

bad_request400
A path id is not a UUID. Or the body does not match OpenVideoTakeRequest.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
video_not_in_plan402
The account’s plan makes no videos.
video_footage_used402
This month’s footage is already uploaded.
missing_scope403
The key does not carry video:write.
ai_off403
The account has not allowed AI in SlyOS, so a live take cannot be transcribed. A sandbox take is not asked.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
video_too_many_takes409
The video has as many takes as the plan allows.
video_take_too_big413
The take is over the plan’s size or length, or would take the video past its footage. In the sandbox a take is at most 50 MB.
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
video_sandbox_limit429
A sandbox key may open 20 takes in any 24 hours; the Retry-After header says when the next one may. Nothing was charged.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

GET/v1/developer/videos/{videoId}/takes/{takeId}

Read a take

One take: where it stands, and while it uploads, the parts storage already holds — so an upload that was interrupted continues where it stopped.

Scope video:readFree120 requests a minute

curl "https://api.slyos.world/v1/developer/videos/$VIDEO_ID/takes/$TAKE_ID" \
  -H "Authorization: Bearer $SLYOS_KEY"

Parameters

videoIdstring (uuid) · in path · required
The video, as id on its VideoProjectView.
takeIdstring (uuid) · in path · required
One take of that video, as id on its VideoTakeView.

200 response

The take. While it uploads, upload.parts_done lists the parts storage holds.

idstring (uuid) · required
The take’s id.
ninteger · required
Take 1, 2, 3… in the order they were added.
filenamestring · required
The name the file was sent with. Never part of a storage key.
statestring · required
uploading → uploaded → preparing → ready. Or failed (failure says why) or discarded.one of: uploading, uploaded, preparing, ready, failed, discarded
secondsnumber | null · required
How long the take runs, as SlyOS measured it. Null until measured.
poster_urlstring (uri) | null · required
A still from the take, good for an hour. Null until it is prepared.
failurestring | null · required
Why the take cannot be used, in a sentence. Set exactly when state is failed.
notestring | null · required
Something worth knowing that is not a failure — "No face found, so it is framed in the middle." Null when there is nothing.
uploadobject · required
While state is uploading: how the file is cut into parts and which parts storage holds. Null otherwise.
upload.part_bytesinteger · required
Bytes in every part but the last (16,777,216).
upload.part_countinteger · required
How many parts the file is cut into.
upload.parts_donearray of integer | null · required
Part numbers storage already holds — skip them when resuming. Null when SlyOS cannot list them: resume from your own record.
created_atstring (date-time) · required
When the take was added.
{
  "id": "7e2a4c6f-1b3d-4f58-9a7c-2e4b6d8f0a13",
  "n": 1,
  "filename": "take-1.mov",
  "state": "uploading",
  "seconds": null,
  "poster_url": null,
  "failure": null,
  "note": null,
  "upload": {
    "part_bytes": 16777216,
    "part_count": 3,
    "parts_done": [
      1,
      2
    ]
  },
  "created_at": "2026-10-21T18:01:02.000Z"
}

Errors

bad_request400
A path id is not a UUID.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
missing_scope403
The key does not carry video:read.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

POST/v1/developer/videos/{videoId}/takes/{takeId}/part-urls

More upload addresses

Addresses for more parts of a take’s upload, or fresh ones for parts whose addresses lapsed. Each works for fifteen minutes.

Scope video:writeFree240 requests a minute

curl -X POST "https://api.slyos.world/v1/developer/videos/$VIDEO_ID/takes/$TAKE_ID/part-urls" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "part_numbers": [
    3
  ]
}'

Parameters

videoIdstring (uuid) · in path · required
The video, as id on its VideoProjectView.
takeIdstring (uuid) · in path · required
One take of that video, as id on its VideoTakeView.

Request body

part_numbersarray of integer · required
The parts that need an address, 1 to 100 at a time.

200 response

Fresh addresses for the parts asked for.

urlsarray of object · required
Upload addresses, one per part.
urls[].part_numberinteger · required
Which part to PUT to this address (1-based).at least 1 · at most 10000
urls[].urlstring (uri) · required
A presigned PUT, good for fifteen minutes. Send the part’s bytes as the body and keep the ETag header of the answer: completing the take needs it.
expires_atstring (date-time) · required
When these addresses stop working. Ask for fresh ones with part-urls.
{
  "urls": [
    {
      "part_number": 3,
      "url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-takes/7e2a4c6f-1b3d-4f58-9a7c-2e4b6d8f0a13?partNumber=3&uploadId=example&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example"
    }
  ],
  "expires_at": "2026-10-21T18:19:30.000Z"
}

Errors

bad_request400
A path id is not a UUID. Or the body does not match VideoPartUrlsRequest.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
missing_scope403
The key does not carry video:write.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
too_large413
The request body is larger than this endpoint accepts.
video_upload_incomplete422
A part number past the take’s last part, or a take that is no longer uploading.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

POST/v1/developer/videos/{videoId}/takes/{takeId}/complete

Finish a take’s upload

Finish a take’s upload with every part’s ETag. The take is checked to be the size it was opened for and prepared. Sent again, it answers with the same take.

Scope video:writeFree30 requests a minute

Sent again, it answers with the same take.

curl -X POST "https://api.slyos.world/v1/developer/videos/$VIDEO_ID/takes/$TAKE_ID/complete" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "parts": [
    {
      "part_number": 1,
      "etag": "\"015f2a9c0e7b1d3e5f6a8c9b0d1e2f3a\""
    },
    {
      "part_number": 2,
      "etag": "\"025f2a9c0e7b1d3e5f6a8c9b0d1e2f3a\""
    },
    {
      "part_number": 3,
      "etag": "\"035f2a9c0e7b1d3e5f6a8c9b0d1e2f3a\""
    }
  ]
}'

Parameters

videoIdstring (uuid) · in path · required
The video, as id on its VideoProjectView.
takeIdstring (uuid) · in path · required
One take of that video, as id on its VideoTakeView.

Request body

partsarray of object · required
Every part, each with the ETag storage answered its PUT with.
parts[].part_numberinteger · required
The part (1-based).at least 1 · at most 10000
parts[].etagstring · required
The ETag header of that part’s PUT answer, as sent.1–200 characters

200 response

Storage holds the whole take, at the size it was opened for. It is prepared next; read the video until it is ready.

idstring (uuid) · required
The take’s id.
ninteger · required
Take 1, 2, 3… in the order they were added.
filenamestring · required
The name the file was sent with. Never part of a storage key.
statestring · required
uploading → uploaded → preparing → ready. Or failed (failure says why) or discarded.one of: uploading, uploaded, preparing, ready, failed, discarded
secondsnumber | null · required
How long the take runs, as SlyOS measured it. Null until measured.
poster_urlstring (uri) | null · required
A still from the take, good for an hour. Null until it is prepared.
failurestring | null · required
Why the take cannot be used, in a sentence. Set exactly when state is failed.
notestring | null · required
Something worth knowing that is not a failure — "No face found, so it is framed in the middle." Null when there is nothing.
uploadobject · required
While state is uploading: how the file is cut into parts and which parts storage holds. Null otherwise.
upload.part_bytesinteger · required
Bytes in every part but the last (16,777,216).
upload.part_countinteger · required
How many parts the file is cut into.
upload.parts_donearray of integer | null · required
Part numbers storage already holds — skip them when resuming. Null when SlyOS cannot list them: resume from your own record.
created_atstring (date-time) · required
When the take was added.
{
  "id": "7e2a4c6f-1b3d-4f58-9a7c-2e4b6d8f0a13",
  "n": 1,
  "filename": "take-1.mov",
  "state": "uploaded",
  "seconds": null,
  "poster_url": null,
  "failure": null,
  "note": null,
  "upload": null,
  "created_at": "2026-10-21T18:01:02.000Z"
}

Errors

bad_request400
A path id is not a UUID. Or the body does not match CompleteVideoTakeRequest.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
missing_scope403
The key does not carry video:write.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
too_large413
The request body is larger than this endpoint accepts.
video_upload_incomplete422
Storage does not hold those parts, or the file is not the size the take was opened for.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

POST/v1/developer/videos/{videoId}/assemble

Make the video

Make the video from its ready takes, or change the last one with your notes. Spends one of the month’s videos — a change spends one only past the plan’s changes per video. The same `request_id` twice is one assembly.

Scope video:writeFree10 requests a minute

On a live key this spends one of the account’s videos this month — the same monthly count the SlyOS app, the Brain and MCP spend (read it with GET /v1/developer/video-usage). Nothing is charged to the API wallet. In the sandbox it spends nothing, at most 20 a day.

A change to a video that has a cut — your notes, "make it shorter" — spends no video until the plan’s changes per video are used (changes_left).

Retry safely with the same request_id: you get the same object, spent once. A request_id reused for different work is refused 409 video_request_reused.

The cut is 9:16, 1080×1920: the best take of each line, pauses and restarts cut out, the face kept in frame, captions in the business’s look, the hook first. Where the plan makes three versions, three cuts come back to pick from.

curl -X POST "https://api.slyos.world/v1/developer/videos/$VIDEO_ID/assemble" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "request_id": "8a0c2e4f-6b8d-4f0a-b2c4-6e8a0c2e4f97",
  "notes": "Open with the free shipping."
}'

Parameters

videoIdstring (uuid) · in path · required
The video, as id on its VideoProjectView.

Request body

request_idstring (uuid) · required
Your idempotency id, a UUID. The same one twice is one assembly: a retry never spends a second video.
notesstring
Your words for the edit — "shorter", "open with the price", "use take 3 for the ending". Sent to the editor as written.at most 1,000 characters
versionsvalue
1 asks for one cut where the plan makes three. Never more than the plan’s.
take_idsarray of string (uuid)
Only these takes. Absent: every ready take.
from_render_idstring (uuid)
For a change: the cut it starts from — the version being looked at. Absent: the chosen cut, else the newest.
target_secondsinteger
The length to aim for this time, 10–180 seconds.at least 10 · at most 180
appearanceobject
How it looks and moves, set exactly rather than by the editor. A field left out keeps what the cut it changes has. With no notes, a change of only these re-renders that cut’s own clips.
appearance.caption_colorstring | null
The caption words: white, yellow, orange, pink, green, blue or brand_orange. null: the brand kit’s text colour again.one of: white, yellow, orange, pink, green, blue, brand_orange
appearance.caption_highlightstring | null
The word being said, in a pop caption: the same colours. null: the brand kit’s highlight again.one of: white, yellow, orange, pink, green, blue, brand_orange
appearance.caption_fontstring | null
clean (a sans), bold (a heavy display sans), classic (a serif) or marker (handwritten). null: the brand kit’s font again.one of: clean, bold, classic, marker
appearance.speednumber
The whole video’s speed, 0.75 to 1.15; the voice keeps its pitch and the captions keep time. 1 is as recorded. 1.15 is the fastest a video plays smoothly, so a faster one is refused bad_request, never made at another speed.at least 0.75 · at most 1.15
appearance.lookstring
A colour look over the picture (never the captions or the logo): none, warm, cool, vivid, mono (black and white) or film.one of: none, warm, cool, vivid, mono, film
appearance.backgroundstring
blur: everything behind the person softly blurred, the person sharp — what every new video is made with unless asked otherwise. none: the background as filmed. A take with nobody in it is never blurred.one of: blur, none

202 response

Queued. Read the video until it is ready, or wait for video.ready.

jobobject · required
The job, queued.
job.idstring (uuid) · required
The job’s id.
job.kindstring · required
prepare a take, assemble the video, or evaluate (check) it.one of: prepare, assemble, evaluate
job.statestring · required
queued, running, succeeded, failed or canceled.one of: queued, running, succeeded, failed, canceled
job.stepstring · required
What the job is doing now. step_words says it in words.one of: waiting, probing, listening, framing, proxying, choosing, cutting, rendering, finishing, watching, reading, judging
job.step_wordsstring · required
The step in plain words — the same words every SlyOS screen shows.
job.progressinteger | null · required
0–100, from a count SlyOS really has (parts rendered of parts to render). Null when there is none; never an estimate.at most 100
job.aheadinteger | null · required
Jobs of other accounts ahead of this one while it waits. Null once it runs.
job.failurestring | null · required
Why it failed, in a sentence. Set exactly when state is failed.
job.created_atstring (date-time) · required
When it was queued.
job.finished_atstring (date-time) | null · required
When it succeeded, failed or was canceled. Null until then.
{
  "job": {
    "id": "c5e7a9b1-3d5f-4a7c-8e1b-5d7f9a1c3e57",
    "kind": "assemble",
    "state": "queued",
    "step": "waiting",
    "step_words": "Waiting for a free editor",
    "progress": null,
    "ahead": 0,
    "failure": null,
    "created_at": "2026-10-21T18:05:40.000Z",
    "finished_at": null
  }
}

Errors

bad_request400
A path id is not a UUID. Or the body does not match AssembleVideoRequest.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
video_not_in_plan402
The account’s plan makes no videos.
video_month_used402
This month’s videos are made.
missing_scope403
The key does not carry video:write.
ai_off403
The account has not allowed AI in SlyOS, so a live video cannot be cut. A sandbox video is not asked.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
video_busy409
A job of this account is already running. Retry with the same request_id when it is done.
video_no_takes_ready409
No take is ready to cut yet. Retry with the same request_id once a take is ready.
video_request_reused409
That request_id already started something else. Do not retry with it; use a new one.
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
video_sandbox_limit429
A sandbox key may start 20 assemblies and checks in any 24 hours; the Retry-After header says when the next one may. Nothing was charged.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

GET/v1/developer/videos/{videoId}/renders/{renderId}/download

Download a cut

A link that downloads one finished cut as an MP4, minted on the request and good for ten minutes. Fetch it from your server; do not publish it.

Scope video:readFree60 requests a minute

Mint it when you are about to fetch: it lapses in minutes and is never stored. The cut itself is kept until its kept_until.

curl "https://api.slyos.world/v1/developer/videos/$VIDEO_ID/renders/$RENDER_ID/download" \
  -H "Authorization: Bearer $SLYOS_KEY"

Parameters

videoIdstring (uuid) · in path · required
The video, as id on its VideoProjectView.
renderIdstring (uuid) · in path · required
One finished cut of that video, as id in its renders.

200 response

A link that downloads the MP4, good for 10 minutes.

urlstring (uri) · required
A presigned GET that downloads the MP4, good for 10 minutes. Fetch it from your server; do not publish it.
expires_atstring (date-time) · required
When the link stops working. Ask again for a fresh one.
filenamestring · required
The file name it downloads as.
bytesinteger · required
Its size.
mime_typestring · required
video/mp4 here: this route hands out the video. (image/jpeg is a cut’s cover, which the SlyOS app’s own route hands out with ?file=cover.)one of: video/mp4, image/jpeg
{
  "url": "https://storage.example.com/0b6f3c2a-5d47-4e81-9a2c-7f1e3d5b8a60/video-outputs/1f3b5d7a-9c2e-4a4c-8e6b-0d2f4a6c8e19?response-content-disposition=attachment%3B%20filename%3D%22Refills%20ship%20free.mp4%22&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=example",
  "expires_at": "2026-10-21T18:20:00.000Z",
  "filename": "Refills ship free.mp4",
  "bytes": 20760000,
  "mime_type": "video/mp4"
}

Errors

bad_request400
A path id is not a UUID.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
missing_scope403
The key does not carry video:read.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

POST/v1/developer/videos/{videoId}/evaluate

Check a video

Check a video against the business’s About you and the links you send: its words and stills are read, and the feedback cites what it relied on. Spends one of the month’s checks. The same `request_id` twice is one check.

Scope video:writeFree10 requests a minute

On a live key this spends one of the account’s checks this month — the same monthly count the SlyOS app, the Brain and MCP spend (read it with GET /v1/developer/video-usage). Nothing is charged to the API wallet. In the sandbox it spends nothing, at most 20 a day.

To check a video you did not make with SlyOS: start a video with kind: check, upload it as its one take, and check that take. A check reads at most 180 seconds of video.

Retry safely with the same request_id: you get the same object, spent once. A request_id reused for different work is refused 409 video_request_reused.

curl -X POST "https://api.slyos.world/v1/developer/videos/$VIDEO_ID/evaluate" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "request_id": "4c6e8a0b-2d4f-4a6c-8e0a-2c4e6a8c0e15",
  "subject": {
    "render_id": "1f3b5d7a-9c2e-4a4c-8e6b-0d2f4a6c8e19"
  },
  "links": [
    "https://www.example.com/shipping"
  ],
  "focus": "Is the shipping right?"
}'

Parameters

videoIdstring (uuid) · in path · required
The video, as id on its VideoProjectView.

Request body

request_idstring (uuid) · required
Your idempotency id, a UUID. The same one twice is one check, spent once.
subjectvalue · required
What to check: one of the video’s takes, or a cut SlyOS made.
linksarray of string
Up to 5 pages to judge it against, beside About you.
use_about_youboolean
Judge it against the business’s About you too. Default true.default: true
focusstring
What you want to know, in your words: "is the pricing right?"at most 500 characters

202 response

Started. Read the check until it is ready, or wait for video.evaluated.

evaluationobject · required
The check, checking. Read it again until state is ready, or wait for video.evaluated.
evaluation.idstring (uuid) · required
The check’s id.
evaluation.project_idstring (uuid) · required
The video it belongs to.
evaluation.subjectvalue · required
What was checked: one of the video’s takes, or a cut SlyOS made.
evaluation.statestring · required
checking, ready or failed.one of: checking, ready, failed
evaluation.bodyobject · required
The feedback. Null until state is ready.
evaluation.body.verdictstring · required
strong (ready to post), good (good, with a few fixes) or needs_work (worth another take).one of: strong, good, needs_work
evaluation.body.summarystring · required
The feedback in a few sentences.1–600 characters
evaluation.body.hookobject · required
Whether the opening works, why, and what to say instead.
evaluation.body.hook.worksboolean · required
Whether the first seconds would keep a viewer.
evaluation.body.hook.whystring · required
Why, in a sentence.1–300 characters
evaluation.body.hook.insteadstring | null · required
A better opening line, or null.at most 300 characters
evaluation.body.strengthsarray of object · required
What works, at most five.
evaluation.body.strengths[].pointstring · required
What works, in a sentence.1–400 characters
evaluation.body.strengths[].at_secondsnumber | null · required
Where in the video, in seconds. Null for the whole of it.
evaluation.body.strengths[].citesarray of integer · required
The references it stands on, by n.
evaluation.body.fixesarray of object · required
What to change, at most eight, most important first.
evaluation.body.fixes[].pointstring · required
What to change, in a sentence.1–400 characters
evaluation.body.fixes[].at_secondsnumber | null · required
Where in the video, in seconds. Null for the whole of it.
evaluation.body.fixes[].citesarray of integer · required
The references it stands on, by n.
evaluation.body.fixes[].weightstring · required
must (wrong or off-brand), should (weaker than it could be) or could (polish).one of: must, should, could
evaluation.body.fixes[].insteadstring | null · required
What to say or do instead, when there is something concrete. Else null.at most 400 characters
evaluation.body.claimsarray of object · required
Every fact the video states, checked against About you and the links: supported, contradicted, or not found.
evaluation.body.claims[].claimstring · required
The fact, as the video states it.1–300 characters
evaluation.body.claims[].at_secondsnumber | null · required
Where in the video, in seconds. Null when it is said more than once.
evaluation.body.claims[].statusstring · required
supported, contradicted, or not_found — nothing given to SlyOS says it, which is not the same as wrong.one of: supported, contradicted, not_found
evaluation.body.claims[].citesarray of integer · required
The references it was checked against, by n.
evaluation.body.referencesarray of object · required
What the feedback relied on, numbered. Every cites names one of these; a citation of anything else is refused before the feedback is stored.
evaluation.body.references[].ninteger · required
Its number, as cites names it.
evaluation.body.references[].kindstring · required
about_rules, about_document or about_link (from About you), or link (a link sent with the check).one of: about_rules, about_document, about_link, link
evaluation.body.references[].labelstring · required
What it is, in a few words.1–200 characters
evaluation.body.references[].source_idstring (uuid) | null · required
The About-you source, for an About-you reference. Else null.
evaluation.body.references[].urlstring (uri) | null · required
Where it is on the web, when it is a page. Else null.
evaluation.body.references[].excerptstring · required
The words relied on, quoted.at most 400 characters
evaluation.failurestring | null · required
Why it failed, in a sentence. Set exactly when state is failed.
evaluation.created_atstring (date-time) · required
When it was asked for.
jobobject · required
The job doing the check.
job.idstring (uuid) · required
The job’s id.
job.kindstring · required
prepare a take, assemble the video, or evaluate (check) it.one of: prepare, assemble, evaluate
job.statestring · required
queued, running, succeeded, failed or canceled.one of: queued, running, succeeded, failed, canceled
job.stepstring · required
What the job is doing now. step_words says it in words.one of: waiting, probing, listening, framing, proxying, choosing, cutting, rendering, finishing, watching, reading, judging
job.step_wordsstring · required
The step in plain words — the same words every SlyOS screen shows.
job.progressinteger | null · required
0–100, from a count SlyOS really has (parts rendered of parts to render). Null when there is none; never an estimate.at most 100
job.aheadinteger | null · required
Jobs of other accounts ahead of this one while it waits. Null once it runs.
job.failurestring | null · required
Why it failed, in a sentence. Set exactly when state is failed.
job.created_atstring (date-time) · required
When it was queued.
job.finished_atstring (date-time) | null · required
When it succeeded, failed or was canceled. Null until then.
{
  "evaluation": {
    "id": "6d8f0b2c-4e6a-4d8c-b0e2-a4c6e8f0b243",
    "project_id": "a3c1e5f7-2b4d-4e6f-8a1c-3e5f7a9b1d24",
    "subject": {
      "render_id": "1f3b5d7a-9c2e-4a4c-8e6b-0d2f4a6c8e19"
    },
    "state": "checking",
    "body": null,
    "failure": null,
    "created_at": "2026-10-21T18:08:00.000Z"
  },
  "job": {
    "id": "9e1a3c5d-7f9b-4b1d-8e3f-5a7c9e1b3d64",
    "kind": "evaluate",
    "state": "queued",
    "step": "waiting",
    "step_words": "Waiting for a free editor",
    "progress": null,
    "ahead": 0,
    "failure": null,
    "created_at": "2026-10-21T18:05:40.000Z",
    "finished_at": null
  }
}

Errors

bad_request400
A path id is not a UUID. Or the body does not match EvaluateVideoRequest.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
video_not_in_plan402
The account’s plan makes no videos.
video_checks_used402
This month’s checks are made.
missing_scope403
The key does not carry video:write.
ai_off403
The account has not allowed AI in SlyOS, so nothing can be checked.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
video_busy409
A job of this account is already running. Retry with the same request_id when it is done.
video_no_takes_ready409
The video to check is not ready yet. Retry with the same request_id once it is.
video_request_reused409
That request_id already started something else. Do not retry with it; use a new one.
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
video_sandbox_limit429
A sandbox key may start 20 assemblies and checks in any 24 hours; the Retry-After header says when the next one may. Nothing was charged.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

Video scripts

POST/v1/developer/video-scripts

Have SlyOS write a script

Have SlyOS write a script from a topic — from the business’s About you and the links you send — and start a video with it. The same `request_id` twice is one script.

Scope video:writeFree20 requests a minute

Retry safely with the same request_id: you get the same object, spent once. A request_id reused for different work is refused 409 video_request_reused.

Written from the business’s About you — its rules whole, the passages nearest the topic — and the links you send. A sandbox key gets a fixed example script, written without a model.

A script SlyOS writes is a Brain answer: it is counted as one, and refused with paused_allowance while the Brain is paused on the account. A script you write yourself (POST /developer/videos with script) is never refused for that.

curl -X POST "https://api.slyos.world/v1/developer/video-scripts" \
  -H "Authorization: Bearer $SLYOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "request_id": "e9b1d3f5-7a9c-4e1b-a3d5-f7b9d1e3a526",
  "topic": "Refills now ship free in the US and Canada",
  "platform": "both",
  "target_seconds": 40,
  "links": [
    "https://www.example.com/shipping"
  ]
}'

Request body

request_idstring (uuid) · required
Your idempotency id, a UUID. The same one twice is one script, written once.
topicstring · required
What the video is about, in your words.1–2,000 characters
platformstring
tiktok, instagram or both (the default).one of: tiktok, instagram, both · default: both
target_secondsinteger
The length to aim for, 10–180 seconds. Default 45.default: 45 · at least 10 · at most 180
notesstring
Anything else the script should do: a tone, a point to make, a line to avoid.at most 1,000 characters
linksarray of string
Up to 5 pages to draw on beside About you.

201 response

The script is written, and a video started with it — or this request_id wrote it before.

idstring (uuid) · required
The video’s id. Every other video endpoint takes it.
kindstring · required
make: script, takes, finished video. check: a video uploaded only to be checked.one of: make, check
titlestring · required
What it is called.
statestring · required
Where it stands, worked out from its takes, jobs and cuts: script, takes, making, ready or failed.one of: script, takes, making, ready, failed
platformstring · required
tiktok, instagram or both. One 9:16 file suits both.one of: tiktok, instagram, both
target_secondsinteger · required
The length the cut aims for, in seconds.
versionsvalue · required
1, or 3 cuts to pick from where the plan makes three.
scriptobject · required
The script. Null for a check video, or before one is written.
script.titlestring · required
What the video is called. The downloaded file is named after it.1–120 characters
script.linesarray of object · required
The spoken lines, in the order they are written.
script.lines[].textstring · required
What is said.1–400 characters
script.lines[].rolestring · required
hook opens the video wherever it sits in the script; cta closes it; line is everything between. At most one hook.one of: hook, line, cta
script.lines[].on_screenstring | null · required
Big words shown while the line is said, or null.1–60 characters
script.captionstring · required
The caption to post with it — up to 2,200 characters, Instagram’s limit.at most 2,200 characters
script.hashtagsarray of string · required
Up to 10 hashtags, each # and letters, digits or _.
script.source_idsarray of string (uuid) · required
The About-you sources the script drew on, by id, so its facts can be traced. Empty when it drew on none.
takesarray of object · required
Every take, in the order added.
takes[].idstring (uuid) · required
The take’s id.
takes[].ninteger · required
Take 1, 2, 3… in the order they were added.
takes[].filenamestring · required
The name the file was sent with. Never part of a storage key.
takes[].statestring · required
uploading → uploaded → preparing → ready. Or failed (failure says why) or discarded.one of: uploading, uploaded, preparing, ready, failed, discarded
takes[].secondsnumber | null · required
How long the take runs, as SlyOS measured it. Null until measured.
takes[].poster_urlstring (uri) | null · required
A still from the take, good for an hour. Null until it is prepared.
takes[].failurestring | null · required
Why the take cannot be used, in a sentence. Set exactly when state is failed.
takes[].notestring | null · required
Something worth knowing that is not a failure — "No face found, so it is framed in the middle." Null when there is nothing.
takes[].uploadobject · required
While state is uploading: how the file is cut into parts and which parts storage holds. Null otherwise.
takes[].upload.part_bytesinteger · required
Bytes in every part but the last (16,777,216).
takes[].upload.part_countinteger · required
How many parts the file is cut into.
takes[].upload.parts_donearray of integer | null · required
Part numbers storage already holds — skip them when resuming. Null when SlyOS cannot list them: resume from your own record.
takes[].created_atstring (date-time) · required
When the take was added.
jobobject · required
The job running or queued now, else the last one. Null when there has been none.
job.idstring (uuid) · required
The job’s id.
job.kindstring · required
prepare a take, assemble the video, or evaluate (check) it.one of: prepare, assemble, evaluate
job.statestring · required
queued, running, succeeded, failed or canceled.one of: queued, running, succeeded, failed, canceled
job.stepstring · required
What the job is doing now. step_words says it in words.one of: waiting, probing, listening, framing, proxying, choosing, cutting, rendering, finishing, watching, reading, judging
job.step_wordsstring · required
The step in plain words — the same words every SlyOS screen shows.
job.progressinteger | null · required
0–100, from a count SlyOS really has (parts rendered of parts to render). Null when there is none; never an estimate.at most 100
job.aheadinteger | null · required
Jobs of other accounts ahead of this one while it waits. Null once it runs.
job.failurestring | null · required
Why it failed, in a sentence. Set exactly when state is failed.
job.created_atstring (date-time) · required
When it was queued.
job.finished_atstring (date-time) | null · required
When it succeeded, failed or was canceled. Null until then.
rendersarray of object · required
Finished cuts, newest assembly first; within one, A then B then C.
renders[].idstring (uuid) · required
The cut’s id. Mint a download link for it with the download endpoint.
renders[].labelstring · required
A, B or C: the order the versions are shown in.1–2 characters
renders[].stylestring · required
tight (the shortest that keeps every point), story (keeps the breaths that carry it) or punchy (opens on the strongest line, fastest cuts).one of: tight, story, punchy
renders[].statestring · required
rendering, ready, failed, or expired — kept past the plan’s days and its files deleted.one of: rendering, ready, failed, expired
renders[].secondsnumber | null · required
How long it runs. Null until rendered.
renders[].widthinteger | null · required
Pixels across (1080). Null until rendered.
renders[].heightinteger | null · required
Pixels down (1920). Null until rendered.
renders[].bytesinteger | null · required
The MP4’s size. Null until rendered.
renders[].poster_urlstring (uri) | null · required
A still to show before it plays, good for an hour. Null until rendered.
renders[].preview_urlstring (uri) | null · required
The cut to PLAY inline, good for an hour. To download it, use the download endpoint. Null until rendered.
renders[].chosenboolean · required
Picked from the versions. The next change starts from it.
renders[].kept_untilstring (date-time) | null · required
When its files are deleted. Download it before then to keep it.
renders[].created_atstring (date-time) · required
When the assembly that made it started.
captionstring | null · required
The caption to post with the chosen cut. Null until written.
hashtagsarray of string · required
The hashtags to post with it.
changes_leftinteger · required
Changes left ("make it shorter") before the next assembly spends another of the month’s videos.
evaluation_idsarray of string (uuid) · required
Its checks, newest first. Read each with GET /v1/developer/video-evaluations/{evaluationId}.
created_atstring (date-time) · required
When it was started.
updated_atstring (date-time) · required
When anything about it last changed.
{
  "id": "a3c1e5f7-2b4d-4e6f-8a1c-3e5f7a9b1d24",
  "kind": "make",
  "title": "Refills ship free",
  "state": "script",
  "platform": "both",
  "target_seconds": 40,
  "versions": 3,
  "script": {
    "title": "Refills ship free",
    "lines": [
      {
        "text": "You can stop buying new bottles today.",
        "role": "hook",
        "on_screen": "Stop buying bottles"
      },
      {
        "text": "Every Example Co bottle refills, and refills now ship free in the US and Canada.",
        "role": "line",
        "on_screen": null
      },
      {
        "text": "Orders leave our warehouse within two business days.",
        "role": "line",
        "on_screen": null
      },
      {
        "text": "Tap the link in our bio to get your first refill.",
        "role": "cta",
        "on_screen": "Link in bio"
      }
    ],
    "caption": "Refill, don’t rebuy. Free refill shipping in the US and Canada.",
    "hashtags": [
      "#refill",
      "#zerowaste"
    ],
    "source_ids": [
      "0a2c4e6b-8d1f-4a3c-9e5b-7d9f1b3d5f78"
    ]
  },
  "takes": [],
  "job": null,
  "renders": [],
  "caption": "Refill, don’t rebuy. Free refill shipping in the US and Canada.",
  "hashtags": [
    "#refill",
    "#zerowaste"
  ],
  "changes_left": 3,
  "evaluation_ids": [],
  "created_at": "2026-10-21T18:00:00.000Z",
  "updated_at": "2026-10-21T18:00:00.000Z"
}

Errors

bad_request400
The body does not match WriteVideoScriptRequest.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
video_not_in_plan402
The account’s plan makes no videos.
missing_scope403
The key does not carry video:write.
ai_off403
The account has not allowed AI in SlyOS, so no script can be written.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
video_request_reused409
That request_id already started something else. Do not retry with it; use a new one.
paused_allowance409
The Brain is paused on this account until its plan renews, and a script SlyOS writes is a Brain answer. Do not retry until it renews; start the video with your own script instead (POST /developer/videos with script).
too_large413
The request body is larger than this endpoint accepts.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

Video evaluations

GET/v1/developer/video-evaluations/{evaluationId}

Read a check

One check: still checking, the feedback with the references it cites, or why it failed.

Scope video:readFree120 requests a minute

Every cites names a reference in references; a check that cited anything else is refused before it is stored, so a citation is never invented.

curl "https://api.slyos.world/v1/developer/video-evaluations/$EVALUATION_ID" \
  -H "Authorization: Bearer $SLYOS_KEY"

Parameters

evaluationIdstring (uuid) · in path · required
The check, as evaluation.id in the answer that started it.

200 response

The check: still checking, ready with its feedback, or failed with why.

idstring (uuid) · required
The check’s id.
project_idstring (uuid) · required
The video it belongs to.
subjectvalue · required
What was checked: one of the video’s takes, or a cut SlyOS made.
statestring · required
checking, ready or failed.one of: checking, ready, failed
bodyobject · required
The feedback. Null until state is ready.
body.verdictstring · required
strong (ready to post), good (good, with a few fixes) or needs_work (worth another take).one of: strong, good, needs_work
body.summarystring · required
The feedback in a few sentences.1–600 characters
body.hookobject · required
Whether the opening works, why, and what to say instead.
body.hook.worksboolean · required
Whether the first seconds would keep a viewer.
body.hook.whystring · required
Why, in a sentence.1–300 characters
body.hook.insteadstring | null · required
A better opening line, or null.at most 300 characters
body.strengthsarray of object · required
What works, at most five.
body.strengths[].pointstring · required
What works, in a sentence.1–400 characters
body.strengths[].at_secondsnumber | null · required
Where in the video, in seconds. Null for the whole of it.
body.strengths[].citesarray of integer · required
The references it stands on, by n.
body.fixesarray of object · required
What to change, at most eight, most important first.
body.fixes[].pointstring · required
What to change, in a sentence.1–400 characters
body.fixes[].at_secondsnumber | null · required
Where in the video, in seconds. Null for the whole of it.
body.fixes[].citesarray of integer · required
The references it stands on, by n.
body.fixes[].weightstring · required
must (wrong or off-brand), should (weaker than it could be) or could (polish).one of: must, should, could
body.fixes[].insteadstring | null · required
What to say or do instead, when there is something concrete. Else null.at most 400 characters
body.claimsarray of object · required
Every fact the video states, checked against About you and the links: supported, contradicted, or not found.
body.claims[].claimstring · required
The fact, as the video states it.1–300 characters
body.claims[].at_secondsnumber | null · required
Where in the video, in seconds. Null when it is said more than once.
body.claims[].statusstring · required
supported, contradicted, or not_found — nothing given to SlyOS says it, which is not the same as wrong.one of: supported, contradicted, not_found
body.claims[].citesarray of integer · required
The references it was checked against, by n.
body.referencesarray of object · required
What the feedback relied on, numbered. Every cites names one of these; a citation of anything else is refused before the feedback is stored.
body.references[].ninteger · required
Its number, as cites names it.
body.references[].kindstring · required
about_rules, about_document or about_link (from About you), or link (a link sent with the check).one of: about_rules, about_document, about_link, link
body.references[].labelstring · required
What it is, in a few words.1–200 characters
body.references[].source_idstring (uuid) | null · required
The About-you source, for an About-you reference. Else null.
body.references[].urlstring (uri) | null · required
Where it is on the web, when it is a page. Else null.
body.references[].excerptstring · required
The words relied on, quoted.at most 400 characters
failurestring | null · required
Why it failed, in a sentence. Set exactly when state is failed.
created_atstring (date-time) · required
When it was asked for.
{
  "id": "6d8f0b2c-4e6a-4d8c-b0e2-a4c6e8f0b243",
  "project_id": "a3c1e5f7-2b4d-4e6f-8a1c-3e5f7a9b1d24",
  "subject": {
    "render_id": "1f3b5d7a-9c2e-4a4c-8e6b-0d2f4a6c8e19"
  },
  "state": "ready",
  "body": {
    "verdict": "good",
    "summary": "A clear, friendly video with one wrong fact: refills ship free in the US and Canada, not everywhere.",
    "hook": {
      "works": true,
      "why": "It names the viewer’s problem in the first second.",
      "instead": null
    },
    "strengths": [
      {
        "point": "It ends on one plain thing to do.",
        "at_seconds": 31.5,
        "cites": []
      }
    ],
    "fixes": [
      {
        "point": "It says refills ship free everywhere; the shipping page says the US and Canada.",
        "at_seconds": 9.4,
        "cites": [
          1
        ],
        "weight": "must",
        "instead": "Refills now ship free in the US and Canada."
      },
      {
        "point": "The pause before the last line is long.",
        "at_seconds": 28.2,
        "cites": [],
        "weight": "could",
        "instead": null
      }
    ],
    "claims": [
      {
        "claim": "Refills ship free everywhere.",
        "at_seconds": 9.4,
        "status": "contradicted",
        "cites": [
          1
        ]
      },
      {
        "claim": "Orders leave the warehouse within two business days.",
        "at_seconds": 17.8,
        "status": "supported",
        "cites": [
          1
        ]
      }
    ],
    "references": [
      {
        "n": 1,
        "kind": "about_link",
        "label": "Example Co — shipping",
        "source_id": "0a2c4e6b-8d1f-4a3c-9e5b-7d9f1b3d5f78",
        "url": "https://www.example.com/shipping",
        "excerpt": "We ship to the US and Canada. Orders leave our warehouse within two business days."
      }
    ]
  },
  "failure": null,
  "created_at": "2026-10-21T18:08:00.000Z"
}

Errors

bad_request400
A path id is not a UUID.
unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
missing_scope403
The key does not carry video:read.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

Video usage

GET/v1/developer/video-usage

This month’s videos

What the account’s plan includes and what this month has used — videos and checks as counts, footage as a percentage — and when they reset. One meter for every door: the app, the Brain, this API and MCP.

Scope video:readFree120 requests a minute

curl "https://api.slyos.world/v1/developer/video-usage" \
  -H "Authorization: Bearer $SLYOS_KEY"

200 response

What the plan includes and what this month has used.

includedboolean · required
Whether the account’s plan makes videos at all.
videosobject · required
Videos this calendar month (UTC).
videos.usedinteger · required
Made so far. A change within a video’s allowance is not a new one.
videos.ofinteger · required
What the plan includes.
checksobject · required
Checks this calendar month (UTC).
checks.usedinteger · required
Asked for so far.
checks.ofinteger · required
What the plan includes.
footage_pctinteger · required
Footage uploaded this month, as a share of the plan’s, rounded down.at most 100
resets_atstring (date-time) · required
When the counts start again: the first moment of next month, UTC.
versionsvalue · required
1, or 3 cuts to pick from.
keep_daysinteger · required
Days a finished cut is kept before its files are deleted.
take_max_bytesinteger · required
The largest one take may be.
take_max_minutesinteger · required
The longest one take may run.
output_max_secondsinteger · required
The longest a finished video may run.
{
  "included": true,
  "videos": {
    "used": 3,
    "of": 15
  },
  "checks": {
    "used": 2,
    "of": 15
  },
  "footage_pct": 40,
  "resets_at": "2026-11-01T00:00:00.000Z",
  "versions": 3,
  "keep_days": 30,
  "take_max_bytes": 4294967296,
  "take_max_minutes": 10,
  "output_max_seconds": 120
}

Errors

unauthorized401
The key is missing, malformed, revoked, or not one we issued.
brain_requires_paid_plan402
The account’s plan has no Brain, and video is part of the Brain. The message says so in words.
missing_scope403
The key does not carry video:read.
not_found404
No such video in this key’s environment and project, or video is not switched on for this account.
rate_limited429
Over this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.
internal500
An unexpected fault on SlyOS’s side. It is recorded.
video_unavailable503
SlyOS cannot make videos right now: no store or render worker is reachable. Nothing was spent. Retry later.

Webhooks

Verifying SlyOS-Signature

t=<unix seconds>,v1=<hex>. The v1 value is HMAC-SHA256, keyed with your endpoint's signing secret, of the string "<t>.<raw request body>".

To verify: split on commas; read t; refuse the delivery if t is more than 300 seconds from your clock; compute the HMAC over the RAW body bytes you received (not re-serialised JSON); accept if it equals ANY v1= value, compared in constant time.

During a secret rotation the header carries two v1= values, one per secret, until the old one expires — which is why the rule is "any".

The reference adapter’s verify function does exactly this, with no dependencies: adapter.mjs.

POSTdraft.ready

A reply is ready

Sent after POST /v1/developer/drafts writes a reply, to the endpoint set for the environment of the key that asked — a sandbox reply to the sandbox endpoint, a live one to the live endpoint. Retried until it is answered 2xx, under the same id: deduplicate on it. It carries the hold: send automatically only when held_for_review is false; true or null goes to a person. Read the reply’s text with GET /v1/developer/drafts/{draftId}, which carries the same hold. The same reply also came back in the POST answer: if your integration sends from that answer, do not send it again from this event.

Signed with SlyOS-Signature; verify it before reading a single field of the body.

Payload

idstring (uuid) · required
The event’s own id. A retry of the same event carries the same id: deduplicate on it.
type"draft.ready" · required
The event type.
createdinteger · required
When the event happened, in Unix seconds — the same on every retry, so an old event reads as old.
environmentstring · required
Which of the account’s two endpoints it was sent to: sandbox or live.one of: sandbox, live
dataobject · required
What is ready.
data.draft_idstring (uuid) · required
The draft that is ready. Read it with GET /v1/developer/drafts/{draftId}.
data.card_idstring (uuid) · required
The card it is for.
data.generation_idstring · required
The generation_id you asked for it with.
data.generationinteger · required
Which draft this is on the card: 1 for the first, higher after another.
data.environmentstring · required
The environment of the key that made the call: sandbox or live.one of: sandbox, live
data.held_for_reviewboolean | null · required
True when SlyOS’s checks held this reply for a person — money, health or dosing, a legal or data-protection matter, another sensitive subject, or something it gives out that the account has not sent before. Never send a held reply automatically: show it to a person first. Null when SlyOS can no longer say, because the conversation has had a newer reply since; null means held. Send automatically only on false.
data.hold_reasonsarray of object · required
Why it was held: one entry per check that held it. Empty when held_for_review is not true.
data.hold_reasons[].codestring · required
Which check held it: sensitive (money, legal, health or credentials), attachment (it carries a file), group (it goes to a group), regex_floor (it matches a rule you set), outbound_screen (it gives out something not sent before, or answers a risky request). A code not listed here is still a hold — treat every code you do not recognise as one.
data.hold_reasons[].labelstring · required
The same reason in words, as the SlyOS inbox shows it.
data.hold_detailstring | null · required
What the checks found, in sentences, when the reply was held. Null when it was not held, or when SlyOS cannot say.
{
  "id": "8c2f6e1a-4b7d-4a93-9e05-3f1c7a2d6b58",
  "type": "draft.ready",
  "created": 1791560531,
  "environment": "live",
  "data": {
    "draft_id": "e1f7a3c5-9d2b-4e68-b0a4-6c3e8f1d2a97",
    "card_id": "5e8d1c3a-7f24-4b9e-8c61-0a3f9d2e7b14",
    "generation_id": "9b2e4f71-3c8a-4d5e-a1f6-2e7b9c0d4a18:reply-1",
    "generation": 1,
    "environment": "live",
    "held_for_review": false,
    "hold_reasons": [],
    "hold_detail": null
  }
}

POSTvideo.ready

A video is cut

Sent when a video a key asked for has its cuts — one, or three to pick from — to the endpoint of that key’s environment. Retried until it is answered 2xx, under the same id: deduplicate on it. Mint a download link for each cut with GET /v1/developer/videos/{videoId}/renders/{renderId}/download. A video made in the SlyOS app sends none.

Signed with SlyOS-Signature; verify it before reading a single field of the body.

Payload

idstring (uuid) · required
The event’s own id. A retry of the same event carries the same id: deduplicate on it.
type"video.ready" · required
The event type.
createdinteger · required
When the event happened, in Unix seconds — the same on every retry, so an old event reads as old.
environmentstring · required
Which of the account’s two endpoints it was sent to: sandbox or live.one of: sandbox, live
dataobject · required
Which video, and its cuts.
data.video_idstring (uuid) · required
The video, as id on its VideoProjectView.
data.render_idsarray of string (uuid) · required
The cuts this assembly made — one, or three to pick from — A first. Mint a download link for each with GET /v1/developer/videos/{videoId}/renders/{renderId}/download.
data.environmentstring · required
The environment of the key that made the call: sandbox or live.one of: sandbox, live
{
  "id": "f1a3c5e7-9b2d-4f6a-8c0e-2a4c6e8f0b12",
  "type": "video.ready",
  "created": 1792605600,
  "environment": "live",
  "data": {
    "video_id": "a3c1e5f7-2b4d-4e6f-8a1c-3e5f7a9b1d24",
    "render_ids": [
      "1f3b5d7a-9c2e-4a4c-8e6b-0d2f4a6c8e19",
      "2a4c6e8b-0d1f-4b3a-9c5e-7f9b1d3e5a20",
      "3b5d7f9c-1e2a-4c4b-a6d8-8e0c2f4a6b31"
    ],
    "environment": "live"
  }
}

POSTvideo.failed

A video could not be made

Sent when an assembly a key asked for fails for good, to that key’s environment’s endpoint, with the reason in a sentence. The video it would have spent is given back. Retried until answered 2xx, under the same id.

Signed with SlyOS-Signature; verify it before reading a single field of the body.

Payload

idstring (uuid) · required
The event’s own id. A retry of the same event carries the same id: deduplicate on it.
type"video.failed" · required
The event type.
createdinteger · required
When the event happened, in Unix seconds — the same on every retry, so an old event reads as old.
environmentstring · required
Which of the account’s two endpoints it was sent to: sandbox or live.one of: sandbox, live
dataobject · required
Which video, and why it could not be made.
data.video_idstring (uuid) · required
The video, as id on its VideoProjectView.
data.job_idstring (uuid) · required
The assembly that failed.
data.failurestring · required
Why, in a sentence written for a person. The video it would have spent is given back.
data.environmentstring · required
The environment of the key that made the call: sandbox or live.one of: sandbox, live
{
  "id": "f1a3c5e7-9b2d-4f6a-8c0e-2a4c6e8f0b12",
  "type": "video.failed",
  "created": 1792605600,
  "environment": "live",
  "data": {
    "video_id": "a3c1e5f7-2b4d-4e6f-8a1c-3e5f7a9b1d24",
    "job_id": "c5e7a9b1-3d5f-4a7c-8e1b-5d7f9a1c3e57",
    "failure": "None of the takes had speech SlyOS could hear. Record again somewhere quieter.",
    "environment": "live"
  }
}

POSTvideo.evaluated

A check is done

Sent when a check a key asked for has its feedback, to that key’s environment’s endpoint. Read it with GET /v1/developer/video-evaluations/{evaluationId}. Retried until answered 2xx, under the same id.

Signed with SlyOS-Signature; verify it before reading a single field of the body.

Payload

idstring (uuid) · required
The event’s own id. A retry of the same event carries the same id: deduplicate on it.
type"video.evaluated" · required
The event type.
createdinteger · required
When the event happened, in Unix seconds — the same on every retry, so an old event reads as old.
environmentstring · required
Which of the account’s two endpoints it was sent to: sandbox or live.one of: sandbox, live
dataobject · required
Which check is done, and its verdict.
data.video_idstring (uuid) · required
The video, as id on its VideoProjectView.
data.evaluation_idstring (uuid) · required
The check. Read the feedback with GET /v1/developer/video-evaluations/{evaluationId}.
data.verdictstring · required
strong (ready to post), good (good, with a few fixes) or needs_work (worth another take).one of: strong, good, needs_work
data.environmentstring · required
The environment of the key that made the call: sandbox or live.one of: sandbox, live
{
  "id": "f1a3c5e7-9b2d-4f6a-8c0e-2a4c6e8f0b12",
  "type": "video.evaluated",
  "created": 1792605600,
  "environment": "live",
  "data": {
    "video_id": "a3c1e5f7-2b4d-4e6f-8a1c-3e5f7a9b1d24",
    "evaluation_id": "6d8f0b2c-4e6a-4d8c-b0e2-a4c6e8f0b243",
    "verdict": "good",
    "environment": "live"
  }
}

POSTwebhook.test

A test delivery

Sent when the account owner presses Send test on the Developer screen, to that environment’s endpoint. Sent once and never retried. Nothing happened; use it to check your signature verification.

Signed with SlyOS-Signature; verify it before reading a single field of the body.

Payload

idstring (uuid) · required
The event’s own id. A retry of the same event carries the same id: deduplicate on it.
type"webhook.test" · required
The event type.
createdinteger · required
When the event happened, in Unix seconds — the same on every retry, so an old event reads as old.
environmentstring · required
Which of the account’s two endpoints it was sent to: sandbox or live.one of: sandbox, live
dataobject · required
Nothing but the sentence.
data.messagestring · required
A sentence saying it is a test.
{
  "id": "1a9d3f7c-6e2b-4c48-8b1f-5d0e9a3c7f26",
  "type": "webhook.test",
  "created": 1791560400,
  "environment": "sandbox",
  "data": {
    "message": "This is a test delivery from SlyOS. Nothing happened."
  }
}

← All docs