Skip to content
For developers

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.

The API card in the Otto panel settings with a list of keys
API keys in Settings, Connections.
PermissionWhat the key may do
readRead conversations, contacts, complaints, uncertain questions, products, knowledge and statistics. Every GET except webhooks.
writeActions like the panel buttons, and adding and deleting knowledge.
catalogueWrite and delete products in the catalogue.
webhooksView 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 key is secret: whoever has it acts on your behalf. It belongs on your server only, never in website or mobile app code. We show it in full only once and keep only its fingerprint. If it leaks, revoke it in the panel. That system loses access at once and your other keys keep working.

The API and webhooks are part of the Custom plan. Writing the catalogue through the API works on every plan.

Your first request

curl
curl https://ottoai.sk/api/v1/me \
  -H "Authorization: Bearer $OTTO_API_KEY"
Node.js
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();
PHP
$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

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

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"
}
CodeMeaning
400Bad request, errors says exactly what.
401Invalid or missing key.
403The feature is not in your plan (api_not_in_plan, webhooks_not_in_plan), or the key lacks the permission (insufficient_scope).
404The record does not exist or is not yours. Records of other companies return 404, never 403.
409The action does not fit the current state, such as taking over a resolved conversation (conv_resolved).
429Too 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.

RouteWhat it does
POST /conversations/<id>/messagesReplies 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>/notesAn internal note the customer never sees.
POST /conversations/<id>/takeoverTakes over the conversation. Without operatorId the current assignee stays.
POST /conversations/<id>/releaseHands the conversation back to Otto.
POST /conversations/<id>/resolve, /reopenResolves 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>/dismissHides 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:

GET /api/v1/stats returns totals for a period and a series by day, week or month in a time zone you choose:

GET /api/v1/stats?since=2026-10-01T00:00:00Z&interval=week
{
  "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 }
    ]
  }
}