API basics
Base address and version of the XICBOT API, access zones, error formats, response headers, HTTPS, CORS, pagination and safely repeating requests.
In plain language
The API is the way other programs talk to the service. This page applies to all of its endpoints. It gives the base address and the version. It shows how a request is built. It also shows what an error looks like. It adds rules for secure connections. Long lists come in parts. The page says what happens when a request is sent again.
All documentation topics
This page covers what applies to every route of the XICBOT API: base address and version, access zones, how requests and errors are structured, HTTPS, response headers, CORS, pagination and how repeated requests behave. Individual routes are documented on the pages for authentication, integration and the other areas.
Base address and version
The API is served under https://app.xicbot.de/api/. Every functional route carries the version v1 in its path, so the base address for your own calls is https://app.xicbot.de/api/v1/. The web app and the clients in the tool kit call the same routes.
To check whether the API is working, call GET /api/v1/public/status; no sign-in is needed. It reports only whether each part is working and gives no further detail. See availability and monitoring.
The simplest way to test a connection is the integration status route. It is the only integration route that needs no scope, and it reads no data:
Shell command bash
curl https://app.xicbot.de/api/v1/integration/status \
-H 'Accept: application/json' \
-H 'X-Api-Key: xbot_…'The response names the account, the scopes and the per-minute cap of the key. Its fields are covered in Integration endpoints, the caps in API limits.
Access zones
Routes under /api/v1/ are grouped into zones, each with its own form of authentication. Within a zone, individual routes may additionally require a role, a plan feature or a key scope.
| Zone | Paths under /api/v1/ | Authentication | Further requirements | Documentation |
|---|---|---|---|---|
| Sign-in | register, verify, login, password/forgot, password/reset | none | - | Authentication |
| Public | public/status, public/changelog, public/accessibility, public/enquiry, public/demo, public/demo/{id}, invitation/{token}, invitation/accept, cart/pickup/{token} | none | - | Public endpoints |
| Widget | public/widget/{key}/… | widget key in the path | - | Widget endpoints |
| Integration | integration/… | header X-Api-Key | plan feature; plus a key scope per route, such as conversations or leads.write | Integration |
| Account | me, logout, password, totp, webauthn, sessions, logins, confirmation/resend, tenant/switch | Authorization: Bearer | active account, except for tenant/switch and logout | Account |
| Keys and webhooks | keys, webhooks | Authorization: Bearer | active account, admin role; plan feature for creating, changing and testing | Keys and webhooks |
| Business areas | knowledge, assistants, conversations, catalog, records/{type} and more | Authorization: Bearer | active account, confirmed email address, role or plan feature depending on the route | Knowledge and assistants, Conversations and management |
| Activity | activity | Authorization: Bearer | active account, confirmed email address, admin role | Team and roles |
Requests
- Send request bodies as JSON with
Content-Type: application/jsonand ask for JSON withAccept: application/json. The web app does the same. - Files, for example for a knowledge source of type
file, are sent asmultipart/form-data. Let your HTTP client set theContent-Typeheader including the boundary; a header set by hand without the boundary makes the body unreadable. - Under
/api/, errors come back as JSON even without anAcceptheader, not as an HTML page. - App routes authenticate with
Authorization: Bearer <token>, the integration with theX-Api-Keyheader. If a key appears in the URL (?api_key=or?key=), the integration rejects the request with 400 andkey_in_url. See authentication for details. - Timestamps are returned as ISO 8601 strings, for example in the
createdfield. - Assistant replies, for example to
POST integration/chat, arrive as a stream withContent-Type: text/event-stream. Errors detected before the stream starts come back as JSON with a matching status. Once the stream is running, an error arrives as an event in the stream and the status no longer changes. The Python client in the tool kit requests these responses withAccept: text/event-stream. - The web server aborts a request if the application sends no data for 300 seconds. Set your client timeout accordingly if you expect long responses.
Error format
Errors are returned as JSON with a matching HTTP status. Depending on where the error originates, the body takes one of five forms:
| Form | When | Example |
|---|---|---|
error with a code, sometimes with further fields | errors the API detects itself: key, role, plan, unknown route | {"error":"role_too_low","required":"admin"} |
message and errors | input validation, status 422; errors lists the messages for each field | {"message":"…","errors":{"email":["…"]}} |
error: "validation" and fields | additional checks after validation, for example an email address already taken at registration | {"error":"validation","fields":{"email":["validation.unique"]}} |
only message | for example a missing or invalid session token (401) and the general rate limit (429) | {"message":"…"} |
message: "Serverfehler" | unexpected error with status 500 or above | {"message":"Serverfehler"} |
For an unexpected error with status 500 or above, the API replaces the text in message with Serverfehler and removes the fields exception, file, line and trace. A response without message does not get one. Messages with a status from 400 to 499 are passed through unchanged, including field errors.
Status codes and error codes
| Status | Code | Meaning |
|---|---|---|
| 400 | key_in_url | Integration key sent in the URL instead of the header; note says where it belongs. |
| 401 | key_missing | The X-Api-Key header is missing or does not start with xbot_. |
| 401 | invalid_key | Key unknown, revoked or expired; all three cases return the same response. |
| 401 | only message | Session token missing or no longer valid. |
| 402 | plan_feature_missing | The plan does not include the feature. Comes with feature, plan, plan_name and available_in, listing the plans that do. See plans and quotas. |
| 402 | plan_limit_reached | A quantity limit of the plan has been reached. Comes with limit, allowed, used, plan and plan_name. |
| 403 | role_too_low | Insufficient role; required names the role needed. See team and roles. |
| 403 | email_unverified | The email address has not been confirmed yet. |
| 403 | no_tenant, tenant_… | No account selected, or the account is not active. For an inactive account the code contains its status, and message reads Das Konto ist derzeit nicht aktiv. |
| 403 | tenant_suspended | The account behind the integration key is not active. |
| 403 | scope_missing, read_only | The key lacks the required scope; read_only if a write scope is missing. required names the scope. |
| 403 | https_required | A request over http with a method other than GET or HEAD that reaches the application unencrypted; see HTTPS. |
| 404 | route_unknown | Unknown route. |
| 404 | not_found | Record does not exist. IDs from another account also return 404. |
| 404 | unknown_widget, demo_expired | Unknown widget key or expired demo. |
| 404 | unknown_cart | Unknown cart pickup token. |
| 405 | method_not_allowed | Known route, wrong method; allowed lists the permitted methods. |
| 409 | run_in_progress | A run is already open, for example when starting a setup; run describes it. |
| 409 | reply_in_progress | A reply to this conversation is already running via POST integration/chat. |
| 410 | cart_expired | The pickup period of the cart has expired. |
| 413 | input_too_long | The chat input is longer than allowed; limit gives the maximum length, length the length sent. |
| 422 | validation or errors | Invalid input. |
| 422 | bad_credentials, invalid_token | Wrong sign-in credentials or invalid confirmation link. |
| 422 | no_version | The assistant has no version yet that it could answer with. |
| 429 | too_many_requests, quota_exhausted, too_many_attempts or only message | Cap reached. The waiting time in seconds is in the Retry-After header; sign-in and password reset return it in the retry_in field instead. The integration adds scope (minute or day), limit, retry_after, limit_per_min and wait. quota_exhausted reports an exhausted monthly quota for chat via the integration, with scope: "month", budget, used and retry_after. See limits. |
| 500 | message: "Serverfehler", plan_gate_misconfigured | Unexpected server-side error; plan_gate_misconfigured reports a misconfigured plan check. |
| 500 | not_logged | The action could not be logged and was therefore not carried out. On every path the body is just {"error":"not_logged"}, without message. The application reports “Not saved: the entry in the audit log could not be written. Please try again.” |
| 503 | plan_limit_unmeasured | The usage of a plan limit could not be determined; reason gives the cause. The request can be retried later. |
Unknown routes and wrong methods
A call for which no route exists is answered by a catch-all branch with JSON instead of an HTML error page. It covers GET, HEAD, POST, PUT, PATCH and DELETE under /api/, including paths outside /api/v1/: /api/ itself and any made-up address return 404 with route_unknown.
If the path exists but not for the method used, the API responds with 405 and method_not_allowed. The allowed field lists the permitted methods in alphabetical order, without HEAD. The catch-all branch does not answer OPTIONS, so preflight requests from other origins stay with the CORS layer.
Shell command bash
curl -s https://app.xicbot.de/api/v1/login -H 'Accept: application/json'
{"error":"method_not_allowed","allowed":["POST"]}
curl -s https://app.xicbot.de/api/v1/beispiel -H 'Accept: application/json'
{"error":"route_unknown"}HTTPS and HSTS
The API runs over HTTPS. The web server redirects http requests under /api/ to the https address with 301, whatever the method. If an unencrypted request does reach the application, it redirects only GET and HEAD with 301 and rejects every other method with 403 and https_required. The application builds the redirect target from its own address, not from the Host header of the request.
Responses over https carry Strict-Transport-Security: max-age=31536000; includeSubDomains, which tells browsers to use https only for this address for one year. preload is deliberately not set.
Response headers
| Header | Value | Meaning |
|---|---|---|
X-Konfigurationsstand | 12 characters | Fingerprint of the effective configuration: the first 12 characters of a SHA-256 hash, no content. Captured when the application starts and also set on error responses. A new value shows that a different version is answering. |
X-Conf-Stand | identifier | Version of the web server settings for /api/. Shows from outside which version of these settings is in place. |
Strict-Transport-Security | max-age=31536000; includeSubDomains | Over https only; see HTTPS. |
X-Content-Type-Options | nosniff | Browsers use the declared content type as is. |
X-Frame-Options | DENY | Responses cannot be embedded in frames. |
Referrer-Policy | no-referrer | Browsers send no referring address when following links. |
X-Robots-Tag | noindex, nofollow | Asks search engines not to index the responses. |
Retry-After | seconds | On status 429 from the integration, the monthly quota and the general rate limit. Sign-in and password reset give the waiting time in the retry_in field. |
Content-Type, Cache-Control, X-Accel-Buffering | text/event-stream; charset=utf-8, no-cache, no-store, no | On streamed responses, regardless of the Accept header of the request. |
x-rows, x-masked, x-capped | number, 0 or 1 | On CSV exports of conversations: row count and whether content is masked; x-capped only on the export of a single conversation and shows whether it was truncated. See conversations and management. |
x-sections, x-failed, x-unexported, x-truncated | number | On the account data export: number of sections, of failed sections, of tables not covered and of shortened tables. See account. |
CORS
The API sends CORS headers only on the public section /api/v1/public/*, for example for the status page, the changelog and the widget branch, and only for the approved origins listed in the table. The integration and all authenticated routes send no CORS headers. Call the integration from your server, not from JavaScript in the browser.
GET cart/pickup/{token} follows its own rule. The route is meant for server-to-server calls. In a browser, only the origin of the stored handover address may read the response. Responses carry Cache-Control: no-store and Vary: Origin.
| Setting | Value |
|---|---|
| Paths | api/v1/public/* |
| Origins | null for the widget, which runs in a sandboxed iframe without allow-same-origin and sends Origin: null; the own origins of app and widget; further individually approved origins including the scheme. Entries containing * are discarded. |
| Methods | GET, POST, DELETE, OPTIONS |
| Request headers | Content-Type, Accept |
| Headers readable by scripts | Retry-After |
| Preflight cache lifetime | 600 seconds (max_age) |
| Credentials | not supported (supports_credentials: false) |
Because only Content-Type and Accept are allowed, a preflight from another origin permits neither X-Api-Key nor Authorization. The routes for the chat window are covered in widget endpoints, the remaining public routes in public endpoints.
Pagination
Lists are paginated with the query parameter page, starting at 1. Page size, permitted ranges and response fields differ by route. Where a route checks a range, values outside it return 422; the integration treats values below 1 as 1.
| Route | page | per_page | Response fields |
|---|---|---|---|
GET integration/conversations, integration/leads, integration/appointments | from 1; lower values count as 1 | fixed at 50 | list and page |
GET conversations | 1 to 10000 | 1 to 200, default 50 | conversations, total, page, per_page |
GET catalog | from 1 | 1 to 200, default 50 | products, total, page, per_page; total counts all products, even when search is set |
GET records/{type} | from 1 | 1 to 200, default 50 | entries, total, page, per_page; total respects the status filter |
GET activity | 1 to 1000 | fixed at 25 | entries, actions, total, page, pages; actions lists the action types present, for filtering |
Repeated requests
The API prevents duplicates through IDs in the request body or through the state of the record, not through a dedicated header. Whether a request can be safely repeated after a timeout depends on the route:
| Route | Behaviour on repetition |
|---|---|
POST integration/leads with external_id | The ID may be up to 100 characters. If a lead with this ID already exists, the route returns 200, the existing entry and duplicate_prevented: true; a new lead returns 201 and duplicate_prevented: false. |
POST integration/leads without external_id | Every call creates a new lead. |
POST catalog/import | Matches products by external_id. Importing the same file a second time creates no duplicates. Up to 500 products per call; the response contains new, changed and total. See catalog and shop. |
Confirm, reject or report a proposal (…/tool/{call}/confirm, …/reject, …/report) | A proposal that has already been decided is not executed again; the response carries duplicate: true. A second report on an executed proposal in the widget also returns duplicate: true. |
DELETE keys/{id} | Revoking a second time changes nothing; withdrawn keeps the time of the first revocation. |
POST public/demo | 200 for an existing demo, 201 for a new one. |
POST integration/knowledge/sources | Every call creates a new source. |
POST integration/chat | Without conversation_id, every call starts a new conversation. With conversation_id you continue a conversation that the same assistant started via the integration; any other ID returns 404. While a reply to the conversation is still running, another call returns 409 with reply_in_progress. After 180 seconds a running reply is treated as aborted and the conversation is free again. |
Frequent questions
How can I tell whether an error is caused by my request?
A status from 400 to 499 concerns your request: the code is in error, field errors are under errors or fields. From 500 upwards the error is on the server side; for an unexpected error the message is simply Serverfehler. A 503 with plan_limit_unmeasured can be retried later.
Can I call the integration directly from the browser?
No. The integration sends no CORS headers, and the key would be exposed in the browser. Call /api/v1/integration/… from your server. The chat window has its own widget branch under /api/v1/public/widget/{key}/.
What happens to a request sent over http?
The web server redirects it to https with 301. If it reaches the application unencrypted, the application rejects anything other than GET and HEAD with 403 and https_required. Either way, its content has already travelled unencrypted. Configure the base address with https:// and revoke any key sent this way.
Why does a route return 405 rather than 404?
The path exists, but not for the method you used. The allowed field lists the permitted methods, for example ["POST"] for /api/v1/login. A 404 with route_unknown, by contrast, means that the path does not exist.
How do I avoid duplicate leads when I retry a call?
Include a stable external_id with POST /api/v1/integration/leads. A retry then returns 200 with the existing entry and duplicate_prevented: true instead of creating a second lead.
Ask it yourself, Assistant of this website
Automated assistant