API
With the API you read conversations, contacts, complaints and statistics, write your catalogue and knowledge, and do what the buttons in the panel do.
API keys
In the panel open Settings, the Connections section, the API row, and click Create a key. Name the key after the system that will use it and pick what it may do. Send the key with every request in the Authorization: Bearer <key> header.

| Permission | What the key may do |
|---|---|
read | Read conversations, contacts, complaints, uncertain questions, products, knowledge and statistics. Every GET except webhooks. |
write | Actions like the panel buttons, and adding and deleting knowledge. |
catalogue | Write and delete products in the catalogue. |
webhooks | View webhook deliveries, resend them and send a test event. |
Give each system its own key with only what it needs. Accounting needs just read, a stock system catalogue. Every address in API reference shows the permission it needs. Without it you get 403 with errcode insufficient_scope.
The API and webhooks are part of the Custom plan. Writing the catalogue through the API works on every plan.
Your first request
curl https://ottoai.sk/api/v1/me \
-H "Authorization: Bearer $OTTO_API_KEY"const r = await fetch("https://ottoai.sk/api/v1/conversations?status=human&limit=20", {
headers: { Authorization: "Bearer " + process.env.OTTO_API_KEY },
});
if (!r.ok) throw new Error((await r.json()).errcode);
const { conversations, nextBefore } = await r.json();$ch = curl_init("https://ottoai.sk/api/v1/leads?since=2026-10-01T00:00:00Z");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("OTTO_API_KEY")],
CURLOPT_RETURNTRANSFER => true,
]);
$kontakty = json_decode(curl_exec($ch), true)["leads"];Format
- All responses are JSON in UTF-8.
- Times are ISO 8601 in UTC, for example
2026-10-10T08:00:00.000Z. - Conversation and contact ids are UUIDs, complaint and uncertain question ids are numbers in a string.
- The full specification is in OpenAPI 3.1, which you can load into Postman, Insomnia or a client generator. All routes are listed in API reference.
Paging
Lists return nextBefore. Send it back as before= until it is null. It is an opaque string, do not build it yourself. The row count on a page does not tell you that you reached the end.
let before = null;
do {
const r = await fetch("https://ottoai.sk/api/v1/leads?limit=100" + (before ? "&before=" + before : ""), { headers });
const d = await r.json();
for (const contact of d.leads) process(contact);
before = d.nextBefore;
} while (before);Filters
sinceanduntillimit the creation time (from, up to but not including),updatedSincethe last change. They take ISO 8601, a bad time returns 400.- Conversations:
status(bot,human,resolved),outcomeandlang. - Complaints:
status(new,approved,rejected,resolved) andkind(claim,withdrawal). limitis at most 100 for conversations, contacts, complaints and questions (default 50), and 200 for products and knowledge (default 100).
Errors
Every error has { error, errcode, requestId }. errcode is stable and in English, use it in your code. error is a sentence for people. A bad parameter also adds errors with the exact path.
{
"error": "Neplatný parameter.",
"errcode": "invalid_param",
"errors": [{ "path": "status", "message": "Použite bot, human alebo resolved." }],
"requestId": "1f0c6a7e-3b9a-4f0e-9c1d-2a7b8e5d4c3f"
}| Code | Meaning |
|---|---|
400 | Bad request, errors says exactly what. |
401 | Invalid or missing key. |
403 | The feature is not in your plan (api_not_in_plan, webhooks_not_in_plan), or the key lacks the permission (insufficient_scope). |
404 | The record does not exist or is not yours. Records of other companies return 404, never 403. |
409 | The action does not fit the current state, such as taking over a resolved conversation (conv_resolved). |
429 | Too many requests. Retry-After says how many seconds to wait. |
The error texts are in Slovak for now. Every response has an x-request-id header. Send your own and we return it, and we can find it in our logs too.
Limits
The limit is 120 requests per minute per company. RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset in every response tell you what is left and in how many seconds the limit frees up. You can send the whole catalogue through snapshot at most 12 times per hour.
Actions
What the buttons in the panel do, the API can do too. Actions send the same webhook events as the panel.
| Route | What it does |
|---|---|
POST /conversations/<id>/messages | Replies to the customer as a colleague. Otto stays quiet in the conversation, a customer who left your website gets the reply by email. |
POST /conversations/<id>/notes | An internal note the customer never sees. |
POST /conversations/<id>/takeover | Takes over the conversation. Without operatorId the current assignee stays. |
POST /conversations/<id>/release | Hands the conversation back to Otto. |
POST /conversations/<id>/resolve, /reopen | Resolves the conversation, or reopens it. |
PATCH /leads/<id> | Marks a contact as handled: { "handled": true }. |
PATCH /returns/<id> | Decides on a complaint, notify: true emails the customer. |
POST /unanswered/<id>/dismiss | Hides an uncertain question from the list. |
curl -X POST https://ottoai.sk/api/v1/conversations/$ID/messages \
-H "Authorization: Bearer $OTTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Hello, your order left our warehouse this morning.", "operatorId": "$OPERATOR" }'Find your colleagues’ ids in GET /api/v1/operators. Without operatorId, the panel shows API as the author.
Conversation outcome and statistics
Every conversation has an outcome:
resolved_by_otto: Otto handled it alone.unanswered: Otto alone, but with an uncertain answer.handed_off: a colleague replied.missed: the customer wanted a person and nobody replied.
GET /api/v1/stats returns totals for a period and a series by day, week or month in a time zone you choose:
{
"conversationsTotal": 1824,
"conversationsMonth": 212,
"products": 236,
"range": {
"since": "2026-10-01T00:00:00.000Z",
"until": "2026-10-10T16:00:00.000Z",
"interval": "week",
"timezone": "Europe/Bratislava",
"totals": { "conversations": 64, "resolvedByOtto": 49, "unanswered": 6, "handedOff": 8, "missed": 1,
"ratingUp": 11, "ratingDown": 1, "leads": 7, "returns": 2 },
"series": [
{ "date": "2026-09-28", "conversations": 31, "resolvedByOtto": 24, "unanswered": 3, "handedOff": 4, "missed": 0, "ratingUp": 6, "ratingDown": 0 },
{ "date": "2026-10-05", "conversations": 33, "resolvedByOtto": 25, "unanswered": 3, "handedOff": 4, "missed": 1, "ratingUp": 5, "ratingDown": 1 }
]
}
}
