Skip to content

For developers ​

This page is for anyone embedding an agent into their own app. It covers when you need the API at all, which keys exist, and where to start.

Looking for the list of endpoints?

API reference → — every platform endpoint with schemas, examples and a "try it" button: conversation, widget, agents, flow, triggers, knowledge, actions, requests, team and billing.

When you need the API and when you don't ​

You don't need it if you:

  • put the agent on your site as the ready-made window — a single line of code is enough, see Where the agent answers;
  • connect a Telegram bot — that's done with one phrase in the conversation with the builder;
  • fill the knowledge from your site or from text — the builder does that itself, see What the agent knows.

You need it if you:

  • build your own chat interface — in a mobile app or on your own site;
  • handle requests in your own support system;
  • sync the agent's knowledge with a catalog or database from your system automatically;
  • wake the agent with events from your system — that's the incoming trigger webhook.

One thing is currently possible only through the API

Almost everything the dashboard does happens in the conversation with the builder or on the Conversations, Plan and Team screens. Reading a site into knowledge through the API has the same ceiling as in the conversation — up to 150 pages of one site; pages are taken from the sitemap (sitemap.xml) and by following links. You don't need to ask for scheduled re-reading: it turns on by itself after the first successful read, and you can reconfigure or turn it off in the conversation. And the builder itself is available to agents too — via MCP and the API.

Reference ​

Interactive referencehttps://api.youselfbot.com/docs
Machine-readable spechttps://api.youselfbot.com/v1/openapi.json

The reference describes the whole HTTP API, not just the integration calls: the dashboard is simply the first client of that same API, so everything it can do is available to you too. The spec at the second link is suitable for generating a client.

This page explains what to look for there.

Keys ​

An agent has three keys. Each can be obtained both from the dashboard and through a call.

KeyWhere you get itWhere it works
pk_… Publicin the conversation: "give me the website code"; GET /v1/dashboard/bots/{id}only the chat window in the browser
sk_… Secret (server)panel → Channels row → New API key; POST /v1/dashboard/bots/{id}/secretthis API (/v1/api/*, /v1/admin/*)
Visitor verificationpanel → Channels row → New visitor signing key; POST /v1/dashboard/bots/{id}/identity-secretsigning a visitor's identity on your server

There are two more keys for agent programs, and they're not for this API: the builder key ck_… — so another program can build agents (Builder MCP), and the agent access key ak_… — so it can talk to the agent (agent as a tool).

Both switches in the panel reissue a key rather than show the current one: nobody knows the current one, including us. So only press them when the old key has leaked or been lost.

The public key doesn't work for this API: any /v1/api/* call with a pk_ key returns 403 with the code wrong_key_type — not 401. The key is valid, just the wrong type. This is the most common mistake when starting out.

The secret key lives only on the backend

Never expose sk_… in a browser, a mobile app or a public repository. A browser on someone else's page can't reach these calls anyway: only the dashboard's domains are allowed.

The key is returned once

Both the secret key and the visitor verification key are returned in full only in the response to the issue request — after that the system knows only their fingerprint. Reissuing breaks the old key immediately: everything that uses it stops until you put in the new one.

Hello world ​

bash
KEY="sk_your_secret_key"
BASE="https://api.youselfbot.com"
SESSION=$(uuidgen)   # unpredictable id, one per dialogue

curl -sS -X POST "$BASE/v1/api/chat" \
  -H "X-Bot-Key: $KEY" -H "Content-Type: application/json" \
  -d "{\"session_id\":\"$SESSION\",\"message\":\"What are your opening hours?\"}"
# {"answer":"We're open 9:00 to 18:00, Mon–Fri.","session_id":"…"}

The next message in the same dialogue uses the same session_id. That's how the agent remembers the context.

session_id is the key to a conversation, not its number

Take it from uuidgen, crypto.randomUUID() or another source of cryptographic randomness. Not user-42, not a sequence number, not the customer's email.

The reason is simple: whoever knows the session_id can read that conversation, write into it and attach files to it — and your operator will see that as the customer's message. A predictable identifier makes this available to anyone who can count.

The agent's key doesn't protect you here: the widget's public key sits in your page's markup by design.

Conversation ​

CallWhat it does
POST /v1/api/chatone message, full answer
POST /v1/api/chat/streamthe same, but streamed (SSE) — the answer types out as you watch
POST /v1/api/visitors/erasedelete everything stored about one visitor

A 200 response to chat has three modes, and they all come with the same status: a normal answer, "handoff": true (a person is handling the conversation) and "quota_exceeded": true (the limit is used up). Check the flags explicitly.

Don't show an empty answer as a message

When a person is handling the conversation, answer may be empty: the message was simply forwarded to the operator. This is the most common mistake in a first integration — empty bubbles from the bot appear in the chat.

Handing over to a person ​

CallWhat it does
POST /v1/api/handoffthe customer asks for an operator
GET /v1/api/messagesfetch new operator messages
POST /v1/api/typing"customer is typing…" for your operators
GET /v1/api/operator/threadslist of requests
GET /v1/api/operator/threads/{sid}full transcript
POST /v1/api/operator/threads/{sid}/replyoperator reply
POST /v1/api/operator/threads/{sid}/typing"operator is typing…"
POST /v1/api/operator/threads/{sid}/resolveclose the request
bash
KEY="sk_your_secret_key"
BASE="https://api.youselfbot.com"
SID="3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"

# Read the request transcript
curl -sS "$BASE/v1/api/operator/threads/$SID" -H "X-Bot-Key: $KEY"

# Reply to the customer
curl -sS -X POST "$BASE/v1/api/operator/threads/$SID/reply" \
  -H "X-Bot-Key: $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"text":"Hi! Checking your order, one moment."}'

# Show the customer "operator is typing…"
curl -sS -X POST "$BASE/v1/api/operator/threads/$SID/typing" -H "X-Bot-Key: $KEY"

# Close — the agent answers again from here on
curl -sS -X POST "$BASE/v1/api/operator/threads/$SID/resolve" -H "X-Bot-Key: $KEY"

The operator's reply reaches the visitor in real time. These calls are for those who have their own support system: in the dashboard, the Conversations section does the same job — see When a customer asks for a person.

Besides messages (author: visitor, bot, operator), the transcript contains events — author: "event": the customer left a contact in the window or rated an answer. The text field says this in one sentence ("The customer left a contact: Olena, +380…"), and the event field carries the data: kind (contact | rating), for a contact — name, contact, note, for a rating — rating (up | down) and ref (the id of the message it refers to). Events are not given to the customer — they're not in GET /v1/api/messages. Show an unknown author or kind as the text line rather than dropping it.

Together with outgoing webhooks this gives you a full cycle without polling. GET /v1/api/operator/threads is useful for an initial sync or for recovering after downtime.

The customer side: your own chat ​

If you have your own chat interface instead of our window:

bash
# The customer asks for an operator
curl -sS -X POST "$BASE/v1/api/handoff" \
  -H "X-Bot-Key: $KEY" -H "Content-Type: application/json" \
  -d "{\"session_id\":\"$SID\"}"
# {"status":"waiting"}

# Fetch new operator messages
curl -sS "$BASE/v1/api/messages?session_id=$SID" -H "X-Bot-Key: $KEY"
json
{
  "messages": [
    { "id": "msg_…", "author": "operator", "text": "Hi there!", "at": 1753600000123 }
  ],
  "status": "operator",
  "operator_typing": false,
  "next_cursor": "…"
}
  • The first poll has no position. After that, pass ?cursor= with the next_cursor value from the previous response.
  • The old after parameter no longer exists. If you send it, the server simply won't see it and will return the transcript from the beginning — that is, the same messages a second time.
  • operator_typing goes off by itself after about 6 seconds, so just redraw the indicator from the value in each poll.
  • operator_here: true — the conversation is still in the queue (status: "waiting"), but an operator has already picked it up and is reading: show the customer that a person is here without waiting for the first message.

Knowledge ​

CallWhat it does
POST /v1/api/knowledgeteach the agent with a document
GET /v1/api/knowledgelist sources (paginated)
GET /v1/api/knowledge/{id}read a source in full
DELETE /v1/api/knowledge/{id}delete a source
bash
curl -sS -X POST "$BASE/v1/api/knowledge" \
  -H "X-Bot-Key: $KEY" -H "Content-Type: application/json" \
  -d '{
        "source_id": "catalog-item-4417",
        "title": "Nord 40W lamp",
        "text": "Nord 40W lamp. Price 1290 UAH. 24-month warranty…"
      }'

source_id is your own identifier. The same source_id updates the source rather than adding a second one. That's exactly what makes a regular catalog sync from your system safe: run it every hour if you like.

Everything added this way is seen by the agent on a par with the knowledge the builder gave it in the conversation — and vice versa.

What to know up front ​

Idempotency ​

Every call with a side effect accepts an Idempotency-Key header. A retry after a timeout won't perform the action twice — you get the saved response from the first attempt with the header Idempotency-Replayed: true. The key lives for 24 hours.

This isn't a "just in case" option: without it, a network timeout on POST .../reply can send the customer a second, identical message.

Errors ​

Every error is JSON with two fields:

json
{ "error": "wrong key type: this endpoint needs a secret key (sk_)", "code": "wrong_key_type" }

Branch on code, not on error: the text can change at any time. The list of codes grows — treat an unknown code as a generic error of the corresponding HTTP status, rather than crashing.

"Not allowed" and "didn't work" are different responses ​

401 and 403 are about your key: it's missing, invalid or the wrong type. There's no point retrying such a request — the response will be the same.

503 with the code service_unavailable says something else: we couldn't perform the check — storage is unavailable right now. The key is valid, retry in a moment.

Confusing them is costly specifically in the "not allowed" direction. A client that reads any failure as "access denied" throws the user back to the sign-in screen during a brief blip in our database — that is, it tells them their session has ended even though it's alive. The rule is simple:

ResponseWhat the client should do
401, 403show sign-in / check the key; don't retry
429wait until RateLimit-Reset and retry
503, 502retry in a moment with the same key
other 5xxretry with backoff, then give up and tell the person

Rate limits ​

60 requests per minute for dialogue and writes, 240 for reads and signals. The window is one minute, counted separately for each "key + IP" pair.

The RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers are on every response, not just on 429.

Versioning ​

The /v1 prefix fixes the contract. Within it, changes are only additive: new calls, new fields in responses, new optional fields in requests, new code values. Existing fields are not renamed, don't change type and don't disappear.

So your client must ignore unknown fields and tolerate unknown enum values. An incompatible change = a new prefix that runs in parallel.

Request ID ​

Every response contains X-Request-Id. Save it in your logs — attached to a support request, it points straight at the right entry in ours. You can send your own — then a single identifier passes end to end through your systems and ours.

What's not here ​

Building the agent — the flow and its steps, triggers, the run log, state, ready-made presets — is the /v1/dashboard/* group. It is not a public contract: the dashboard itself uses it, and it changes along with the interface. You can use it, but we don't promise compatibility between versions here. You don't need it for a deep crawl of a site: POST /v1/admin/scrape with the body {"url": "…", "max_pages": 150, "refresh_days": 7} is part of the public API (max_pages above 150 is cut to 150; refresh_days — re-read once every that many days, from 1 to 180; without the field — once every 7 days, and 0 means "don't re-read"; the response tells you what schedule you ended up with).

Public and stable is exactly what's listed above: /v1/api/*, /v1/admin/*, the incoming trigger webhook and outgoing webhooks.

Next ​

Webhooks — events from us to you and a signal from you to the agent.

YouSelfBot platform documentation