Developer API

Turn any inbox into a SlyOS channel.

You have somewhere people write to you — a helpdesk, a ticketing system, your own app’s chat. Send those messages to SlyOS and it writes the reply, using the same Brain, the same learned voice and the same history as your own inbox. You send the reply yourself, however you already send things.

10¢ per AI response. Taking messages in, reading things back, checking your balance and reporting what you sent are all free. So is a retry. So is the sandbox, up to 50 drafts a day.

Step 1 — get a key

Open Settings, press Plan, then Developer. Press Create my first key.

That one press makes three things: a project, a sandbox channel to send messages to, and your first key. The key is shown once — copy it then. We keep only a fingerprint of it, so nobody, including us, can show it to you again.

Keys starting sly_test_ are the sandbox: free, they reach nobody, and they write up to 50 replies a day per account — plenty to build and test with, not a way to run a helpdesk. Keys starting sly_live_ are real, and cost 10¢ a reply. Start in the sandbox.

A key created this way can take messages in, ask for drafts, read them back, report deliveries and check the balance — 5 permissions, which is what an integration needs and nothing more. It cannot change what SlyOS believes about anybody. To teach replies about your business from your own server (step 5), give a live key brain:read and brain:write as well: live and sandbox replies use the same knowledge, so a sandbox key may read it but not change it.

Step 2 — set up your terminal

Paste this once. Take the channel id from the Channels list on the same screen — tap it to copy.

export SLY=https://api.slyos.world/v1/developer
export K=your_key_here
export CH=your_channel_id_here
export A="Authorization: Bearer $K"
export J="Content-Type: application/json"

Check it works. This is free:

curl -s $SLY/wallet -H "$A" | jq

Before the first reply — 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).

Your website, every page of it:

curl -s -X POST $SLY/knowledge/sources -H "$A" -H "$J" -d '{"url":"https://www.example.com/","crawl":true}' | jq

A file your replies must follow:

curl -s -X POST $SLY/knowledge/sources -H "$A" -F 'file=@returns-policy.pdf' -F 'rules=true' -F 'title=Returns policy' | jq '{id, status}'

Who you are, and how you sound:

curl -s -X PUT $SLY/knowledge/profile -H "$A" -H "$J" -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”."}' | jq

Step 3 — the four calls

That is the whole API. Everything else is a convenience.

A message arrives in your system · Free — always

Send it to SlyOS the moment it lands. This is free even when your balance is zero, on purpose: a support ticket must never be lost because an account ran out of credit.

POST /v1/developer/messages

Ask SlyOS to write the reply · 10¢

The only call that costs anything. The reply is written by the same Brain your inbox uses — your facts, the voice it has learned for that channel, and the conversation so far.

POST /v1/developer/drafts

Tell SlyOS what happened to it · Free

You send the reply through your own system. This is how every SlyOS surface learns it went out, so the conversation reads the same on the web and on your phone.

POST /v1/developer/deliveries

Check what you can still afford · Free

Call it before a burst. An adapter that checks first can queue or escalate to a human; one that does not finds out in the middle of answering somebody.

GET /v1/developer/wallet

Step 4 — run it

Send a message in. It appears in your SlyOS inbox immediately. The ids are your system’s own; sending the same external_message_id again stores nothing new.

export TICKET=ticket-$(date +%s)
export MSG=$(curl -s -X POST $SLY/messages -H "$A" -H "$J" -d "{
  \"channel_id\": \"$CH\",
  \"external_thread_id\": \"$TICKET\",
  \"external_message_id\": \"$TICKET-comment-1\",
  \"body\": \"Do you ship to Canada, and how long does delivery take?\",
  \"sender_display_name\": \"Dana\"
}" | jq -r .message_id); echo $MSG

Now ask for the reply:

export DRAFT=$(curl -s -X POST $SLY/drafts -H "$A" -H "$J" -d "{
  \"generation_id\": \"${MSG}:reply-1\",
  \"message_id\": \"$MSG\"
}" | tee reply.json | jq -r .draft_id)
jq '{held_for_review, hold_reasons, body, sources}' reply.json

Only send it without a person looking if held_for_review is false. true means SlyOS’s checks held it for a person — money, a sensitive subject, a rule you set, somebody asking whether they are talking to a person — and hold_reasons says which. Route it to a person instead. null means the verdict could not be read back: treat it as held. A retry’s answer carries the same hold. sources lists what the reply relied on of what your business handed SlyOS (step 5).

Send it however you already send things, then tell SlyOS it went. Name the draft you sent, so the conversation shows it as SlyOS’s reply and the ticket stops waiting, and your own id for the sent message, so a report you repeat is one message:

curl -s -X POST $SLY/deliveries -H "$A" -H "$J" -d "{
  \"message_id\": \"$MSG\",
  \"status\": \"delivered\",
  \"draft_id\": \"$DRAFT\",
  \"external_message_id\": \"comment-1\"
}" | jq

If a person edited the draft before it went, send what actually went as body beside the draft_id. If a person at your end wrote a reply of their own, send it as body with no draft_id: that is your voice, and it is what SlyOS learns your voice from. It never learns from its own drafts.

Step 5 — what replies rely on

Every reply is written from the parts of what you handed SlyOS that answer the message, in your voice. Mark a short policy as rules and every reply carries it whole — up to about 8,000 characters of rules, two or three pages. Past that, each reply gets the rules nearest its message, and the source says so with rules_fit: false: keep as rules what every reply must follow, and add long documents, like a regulation, as reference. Adding knowledge is free; it is also on the Developer screen, under Knowledge. Changing it needs a live key — sandbox and live replies share it, so a sandbox key can only read it.

Each source is read in the background: queued, then ready, or failed with a status_reason that says why in plain words. Read it back, see exactly what was read, read a link again, or delete it. A link read again that has not changed is kept as it was, and costs nothing. A page that builds its content with JavaScript is read without running it, so what it draws can be missing: 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:

GET/v1/developer/knowledge/sourcesGET/v1/developer/knowledge/sources/:sourceIdGET/v1/developer/knowledge/sources/:sourceId/passagesPATCH/v1/developer/knowledge/sources/:sourceIdPOST/v1/developer/knowledge/sources/:sourceId/refreshDELETE/v1/developer/knowledge/sources/:sourceId

When your sources disagree, a reply follows the higher one: rules, then files you uploaded, then single links, then pages read from a whole site — and it never contradicts your own documents. That order settles disagreements only: a reply answers from the passage that answers the question. The passage closest to the message always goes in, and the best passage of each other source about as relevant goes in ahead of the rest, so a long website cannot crowd out a short policy.

Every reply says what it relied on. POST /drafts and GET /drafts/:id carry sources: the source, the page range or section, an excerpt of the passage's own text, how close it was to the message, and whether it was a rule.

"sources": [
  { "source_id": "…", "title": "Customer policy.pdf", "page": 4, "page_end": null, "rule": true,  "rank": 1, "score": 0.41 },
  { "source_id": "…", "title": "Support policy.pdf", "page": 11, "page_end": 12, "rule": false, "rank": 1, "score": 0.38 },
  { "source_id": "…", "title": "Pricing", "url": "https://acme.example.com/pricing", "rule": false, "rank": 2, "score": 0.58 }
]

Replies use what you add here, the conversation and this 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 you send "scope": "account".

Limits: 25 MB a file, two uploads at a time, 200 sources and about 12 million characters an account, and 2 million characters a source. PDFs are read page by page, up to 2,000 pages every 30 days on a paid plan or with money in the wallet, 100 without. Each limit is said in words when it is reached.

The one rule worth learning

generation_id is your name for one reply, and it is what you are billed on.

same idFree. You get back the reply you already paid for, byte for byte. Retry after a timeout as many times as you like.new id10¢. A different reply — a second attempt, a steer, a follow-up.

So make it unique and reproducible: build it from the message_id SlyOS gave you, as <message_id>:reply-1 — the sample above does. A message_id is unique across every SlyOS account, so yours cannot collide with anybody else’s; your own ticket numbers can, because every helpdesk starts counting at 1. If your network drops mid-call, send the identical request again and you will not be charged twice.

A reply that fails is never charged for. Neither is one we refuse, one the model could not produce, or one you cancel.

When it says no

Every refusal has the same shape, whatever the status — a code you can branch on and a message written for a person. Some carry extra fields beside them; a 402 tells you what you had and what the reply needed.

{ "code": "insufficient_funds", "message": "…", "available_cents": 0, "needed_cents": 10 }
401The key is wrong, revoked, or not one we issued. Stop and check the key. Retrying will not help.402Out of credit. This is not an outage. The message is stored and visible in SlyOS and can be answered by hand — only the AI reply is paused. Add funds, or escalate to a person. Do not drop the ticket.403The key is fine but lacks a permission. Make a key that has it; checking the secret again will not help.409Usually a real state, not a fault — somebody asked whether they were talking to a human, or the conversation is filed. Do not retry those. One 409 is different: generation_in_progress means another request with your generation_id is still being written. Wait for Retry-After and send the same request again, with the same id — you get that reply, charged once. So is not_stored on a message you send in: send the same request again. It is safe — a message is stored once — and a ticket is never dropped.429Too many calls this minute, or the sandbox’s 50 drafts for the day are used. Wait for Retry-After and send it again.503Our providers are unreachable. Wait for Retry-After and retry with the same generation_id. You were not charged.

Every refusal worth sending again, exactly as the API declares it. Anything not on this list will answer the same way the second time:

409 generation_in_progressAnother request with this generation_id is still being drafted. Retry after Retry-After with the same id to get that draft, charged once.503 outageNo model provider is reachable. Retry after Retry-After with the same generation_id. Nothing was charged.503 try_againThe draft could not be charged for in time, so it was not. Retry with the same generation_id.429 rate_limitedOver this route’s per-minute limit, counted per account and per key, sandbox included. Wait a minute.429 sandbox_daily_limitSandbox 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.

Optional — get told instead of asking

Set an endpoint on the Developer screen and SlyOS will post to it when a draft is ready, instead of you polling. Sandbox and live are separate endpoints: drafts made with a sandbox key go to the sandbox one, drafts made with a live key to the live one, so your production server never receives a test. A live endpoint must be https; the sandbox one may be plain http, for a tunnel while you build. Neither may point at a private or internal address, and both must listen on port 443 (or 80). Read the reply with GET /v1/developer/drafts/{draft_id} and send it only when it is not held for review — the event and the read both carry held_for_review, and a reply that does not say false is held. Send from the webhook or from the answer to your POST, not both: every reply raises the event, including the ones you already sent. Every delivery is signed:

POST <your endpoint>
SlyOS-Signature: t=<unix seconds>,v1=<hex>
Content-Type: application/json

{
  "id": "<event id — the same on every retry of this event>",
  "type": "draft.ready",
  "created": 1791100800,
  "environment": "live",
  "data": {
    "draft_id": "…",
    "card_id": "…",
    "generation_id": "<yours>",
    "generation": 1,
    "environment": "live",
    "held_for_review": false,
    "hold_reasons": [],
    "hold_detail": null
  }
}

Never send a draft whose held_for_review is true without a person reading it first. SlyOS held it on purpose, and hold_reasons says why (each reason as a code and a label). The event types are draft.ready and webhook.test; webhook.test is what the Send test button sends.

Answer with any 2xx within ten seconds. Anything else — an error, a timeout, or a redirect, which is never followed — is retried: 8 attempts in all, thirty seconds apart and doubling, over about 62 minutes. After the last one the event is shown as failed on the Developer screen, where Retry sends it again. An endpoint that stops answering is tried less often, so that window can stretch. Delivered and failed events, with their attempts, are kept for 30 days. A retry can reach you after an attempt that did in fact arrive, so dedupe on the event’s id.

The signature is HMAC-SHA256 of the string "<t>.<raw body>" using your endpoint secret. Check the timestamp is within 5 minutes and compare in constant time — the timestamp is signed with the body so that a delivery somebody captures cannot be replayed at you forever. Every retry is signed afresh.

During a secret rotation you will see two v1= values in one header. Accept the request if either matches; that is what lets you redeploy without dropping deliveries. The old secret keeps verifying for an hour.

A complete, dependency-free adapter — the four calls, this check, and the rule that a held reply is never sent — in one file: adapter.mjs.

Everything else

Every endpoint, field, status, error and rate limit — generated from the schemas the server validates with, so it cannot describe an API the server does not run.

The API reference →

The same document as OpenAPI 3.1, for code generators and API tools: /openapi.json.

← All docs