Integration endpoints
Using an API key: read conversations, enquiries, appointments and metrics, create enquiries and sources, stream chat replies as server-sent events.
In plain language
These endpoints connect your own systems to the service. You need a key for this. With it you read chats, requests, appointments and figures. You can also create requests and knowledge sources. You can ask an assistant questions. The answer then comes back piece by piece. Each endpoint checks whether the key may use it.
All documentation topics
The integration API connects your own systems to XICBOT. With an API key you read conversations, enquiries, appointments and metrics, create enquiries and knowledge sources, and put questions to an assistant, with the reply streamed back as server-sent events. Every endpoint lives under /api/v1/integration/ and checks the key's scope on its own.
Overview
The base address is https://app.xicbot.de/api/v1/integration/. Each request carries the key in the X-Api-Key header; keys start with xbot_. Creating a key, choosing its scopes and letting it expire is covered in Keys and webhooks, the authentication scheme in Authentication. These endpoints sit outside the app's session login, so a session token does not open them.
| Endpoint | Scope | Label in the app |
|---|---|---|
GET /status | none | – |
GET /conversations, GET /conversations/{id} | conversations | Read conversations |
GET /leads | leads | Read enquiries |
POST /leads | leads.write | Create enquiries |
GET /appointments | appointments | Read appointments |
GET /analytics | analytics | Read analytics |
POST /knowledge/sources | knowledge.write | Create knowledge sources |
POST /chat | chat | Use chat |
Writing is a scope of its own: a key with leads reads enquiries but cannot create any. Under Scopes the app puts it this way: *Writing is a separate scope. Without it a key only reads.*
Errors every endpoint shares
Before an endpoint does its actual work, the API checks the key, the account, the plan, the rate caps and the scope. Errors come back as JSON with an error field.
| Status | error | Meaning |
|---|---|---|
400 | key_in_url | The key was sent as api_key or key in the query string. The request is turned away before the key is even looked up; the note field says where it belongs. |
401 | key_missing | The X-Api-Key header is missing, empty or does not start with xbot_. |
401 | invalid_key | Unknown, revoked or expired key. All three cases get the same answer. |
403 | tenant_suspended | The key's account is not active. |
402 | plan_feature_missing | The account's plan does not include the feature for keys and webhooks, for example after a plan change. feature, plan, plan_name and available_in name the feature, the plan and the plans that include it. The key stays valid and works again once the plan includes the feature; the request does not count towards any cap. |
429 | too_many_requests | The key's per-minute or daily cap has been reached, see Rate limits. |
403 | scope_missing | The key lacks the scope this endpoint requires, such as leads or chat. The required field names it. |
403 | read_only | The endpoint writes, and the key lacks the write scope (leads.write or knowledge.write). The required field names it. |
422 | – | An input failed field validation. The response carries message and, under errors, the problems per field. |
500 | – | Server error. message then reads Serverfehler; class name, file, line and stack trace are left out of the response. |
Rate limits
Every key has a per-minute cap, which you enter under Requests per minute when creating it, and a daily cap from your account settings; both are counted per key. Once either is reached, the endpoint answers 429 with too_many_requests. Values, time windows and the response fields are in API limits.
Paging
The lists of conversations, enquiries and appointments return 50 entries per page. Conversations and enquiries are ordered by creation time, newest first; appointments by start time, latest first. Pick the page with page, counting from 1; a missing or smaller value counts as 1, and a value that is not a whole number is rejected with 422. The response echoes the page in page and gives no total. Keep reading until a page holds fewer than 50 entries.
Status
GET /api/v1/integration/status
Tells you what the key may do and reads no data. Use it to test a connection without needing any scope.
- Authentication
X-Api-Keyheader, no scope- Rate limit
- The key's per-minute and daily caps, see Rate limits
| Field | Content |
|---|---|
alive | true once the request has passed the checks |
tenant | Your account ID |
scopes | The key's scopes as a list |
limit_per_min | The key's per-minute cap |
Shell command bash
curl -s https://app.xicbot.de/api/v1/integration/status \
-H "X-Api-Key: $XICBOT_KEY"
# {"alive":true,"tenant":"…","scopes":["leads","leads.write"],"limit_per_min":120}Conversations
GET /api/v1/integration/conversations
Lists the conversations in your account across all channels, 50 per page, newest first. What a conversation looks like in the app is described in Conversations.
- Authentication
X-Api-Keyheader, scopeconversations- Rate limit
- The key's per-minute and daily caps, see Rate limits
| Parameter | Required | Content |
|---|---|---|
page | no | Whole number, page from 1, default 1 |
Field in conversations[] | Content |
|---|---|
id | Conversation ID |
assistant_id | Assistant ID |
channel | widget (chat window on a website), api (started via POST /chat) or test (test in the app) |
title | Built from the first question, at most 120 characters, ending in … when shortened |
status | open, closed or escalated |
message_count | Number of messages |
created | Start time as an ISO 8601 timestamp |
GET /api/v1/integration/conversations/{id}
Returns one conversation with its history: up to 200 messages in chronological order. An ID from another account finds nothing and gets the same answer as an unknown one.
- Authentication
X-Api-Keyheader, scopeconversations- Rate limit
- The key's per-minute and daily caps, see Rate limits
| Field | Content |
|---|---|
conversation | The same fields as in the list |
messages[].role | user (question from a visitor or caller), assistant (the assistant's reply) or operator (a message from a person on your team) |
messages[].text | Message text in plain text |
messages[].time | Timestamp in ISO 8601 |
| Status | error | Meaning |
|---|---|---|
404 | not_found | No conversation with this ID in your account. |
Enquiries
Enquiries are contact requests. They arise in a conversation with an assistant or through this API. When an assistant creates an enquiry through a tool, that also fires the lead.created webhook, with a different payload from the one here, see Webhooks triggered. In the app you handle enquiries under Appointments and cases.
GET /api/v1/integration/leads
Lists the enquiries in your account, 50 per page, newest first. Choose the page with page.
- Authentication
X-Api-Keyheader, scopeleads- Rate limit
- The key's per-minute and daily caps, see Rate limits
Field in leads[] | Content |
|---|---|
id | Enquiry ID |
name, email, phone | Contact details, each of which may be empty |
subject | What the enquiry is about |
source | Origin; enquiries created through the API carry api unless you send something else |
status | new (new), seen (seen) or done (done) |
created | Creation time in ISO 8601 |
POST /api/v1/integration/leads
Creates an enquiry. It starts with status new and fires the lead.created webhook.
- Authentication
X-Api-Keyheader, scopeleads.write- Rate limit
- The key's per-minute and daily caps, see Rate limits
| Field | Required | Rule |
|---|---|---|
subject | yes | Text, at most 1000 characters |
name | no | Text, at most 120 characters |
email | no | Valid email address, at most 190 characters |
phone | no | Text, at most 60 characters |
source | no | Text, at most 60 characters; defaults to api |
external_id | no | Your own ID, at most 100 characters; prevents duplicates |
| Status | Response | Meaning |
|---|---|---|
201 | lead, duplicate_prevented: false | Enquiry created, webhook fired. |
200 | lead, duplicate_prevented: true | An enquiry with the same external_id already existed. You get that one back; no second one is created and no webhook fires. |
422 | message, errors | subject is missing or a field rule is broken. |
Appointments
GET /api/v1/integration/appointments
Lists the appointments in your account, 50 per page, ordered by start time, latest first. How appointments come about is described in Appointments and cases.
- Authentication
X-Api-Keyheader, scopeappointments- Rate limit
- The key's per-minute and daily caps, see Rate limits
Field in appointments[] | Content |
|---|---|
id | Appointment ID |
type | Name of the appointment type |
starts_at | Start time in ISO 8601 |
duration_min | Duration in minutes |
name, email | Contact details |
status | requested (requested), confirmed (confirmed) or cancelled (cancelled) |
Metrics
GET /api/v1/integration/analytics
Returns the metrics for a period across all assistants combined. Conversations from the test dialog and check conversations are not counted, as in analytics. What each metric means is explained in Analytics.
- Authentication
X-Api-Keyheader, scopeanalytics- Rate limit
- The key's per-minute and daily caps, see Rate limits
| Parameter | Required | Content |
|---|---|---|
period | no | 7d, 30d or 90d; defaults to 30d |
zone | no | IANA time zone such as Europe/Berlin (default) or America/New_York. Older names that browsers still report, such as Asia/Calcutta or Europe/Kiev, are accepted and calculated under today's name (Asia/Kolkata, Europe/Kyiv). Upper and lower case do not matter; the zone is calculated and reported under its usual spelling, so Europe/Berlin for europe/berlin. The fixed offsets Etc/GMT+1 to Etc/GMT+12 and Etc/GMT-1 to Etc/GMT-14 are accepted with the sign used by the time zone database: Etc/GMT+1 is one hour behind UTC. An offset such as +01:00 does not count as a zone. |
The window includes the current day and ends at the next midnight in the given zone: 7d covers today and the six days before it.
| Field | Content |
|---|---|
period, zone | As requested; zone holds the usual spelling, so Europe/Berlin for europe/berlin, and for an older zone name today's name, so Asia/Kolkata for Asia/Calcutta |
from, to | Window boundaries as UTC timestamps |
days | Number of days in the window |
assistant | null: the figures cover all assistants |
cards | The metrics, one object per key |
| Field of a metric | Content |
|---|---|
value | The value, shares as percentages; null when not measured |
measured | true when a value exists |
reason | With measured: false, the reason: no_conversation, no_reply, no_search, no_probe, no_enforcement_check, no_tool_call or no_rating |
counter, denominator | For shares, the numerator and the denominator |
| Group | Keys in cards |
|---|---|
| Volume | conversations, messages, answered, escalations, abstentions, messages_per_conversation |
| Conversation outcome | escalation_rate, self_resolved, frustration_rate, unanswered, abandoned, abandon_rate |
| Answer quality | citation_rate, coverage, retrieval_mrr, retrieval_hit_rate, enforcement_rate, hard_topic_rate, blocked_rate |
| Speed and usage | reply_ms_p50, reply_ms_p95, units |
| Tools | tool_calls, tool_executed, tool_rejected, tool_errors |
| Results | leads, appointments, carts |
| Visitor ratings | visitor_ratings, visitor_helpful_rate, visitor_rating_coverage |
| Status | error | Meaning |
|---|---|---|
422 | unknown_timezone | Unknown zone. The API does not silently fall back to the default. |
422 | – | period is something other than 7d, 30d or 90d (message, errors). |
Creating knowledge sources
POST /api/v1/integration/knowledge/sources
Creates a knowledge source from text or from a website address. Text is ingested right away; an address is fetched by a background job. How sources feed into answers is covered in Knowledge.
- Authentication
X-Api-Keyheader, scopeknowledge.write- Rate limit
- The key's per-minute and daily caps, see Rate limits
| Field | Required | Rule |
|---|---|---|
title | yes | Text, at most 200 characters |
text | yes, if url is missing | Text, at most 400000 characters |
url | yes, if text is missing | Address using http or https, at most 500 characters. If both url and text are sent, url wins. |
draft | no | true stores the text as a draft that feeds into no answer. Not allowed together with url. |
published_at | no | Publication date. If it lies in the future, the text only feeds into answers from then on. Not allowed together with url. |
| Status | Response | Meaning |
|---|---|---|
201 | source with id, type: text, status, chunks | Text processed. status is ready or failed. A failed ingest also answers 201: the source exists, only its ingest failed. |
202 | source with id, type: website, status: new | Address accepted; the fetch runs in the background. |
422 | error: url_unreadable | The address cannot be parsed, for example because scheme or host is missing. |
422 | error: scheme_not_allowed | A scheme other than http or https. |
422 | error: name_unresolvable | The host name does not resolve. |
422 | error: target_not_allowed | The target is not a public address. |
422 | message, errors | A field rule is broken, for example draft together with url. |
402 | error: plan_limit_reached | A count limit of your plan is reached; limit names it (knowledge_sources, documents or chunks), allowed and used give the figures. Nothing is created. See Plan gate and plan limits. |
503 | error: plan_limit_unmeasured | The stock could not be counted; nothing is created. Please try again later. |
Whether a website fetch succeeded is reported by the knowledge.indexed and knowledge.failed webhooks. On failure, reason holds a named cause such as no_text_extracted, url_unreachable, website_too_large or redirect_off_origin; other causes show up as ingest_failed. The fetch gets up to three attempts, and every failed attempt sends knowledge.failed. You may therefore receive several events for the same source_id, including a knowledge.indexed after a knowledge.failed; the most recent one counts.
Chat
POST /api/v1/integration/chat
Puts a question to an assistant. Without conversation_id a new conversation is started in the api channel. The reply arrives as a server-sent event stream (text/event-stream), produced the same way as in the app.
- Authentication
X-Api-Keyheader, scopechat- Rate limit
- The key's caps, plus your account's monthly quota
| Field | Required | Rule |
|---|---|---|
assistant_id | yes | Assistant ID, at most 64 characters. The reply uses its latest version. |
conversation_id | no | Continue a conversation, at most 64 characters. Only conversations this assistant started through this API. |
text | yes | The question, 1 to 4000 characters. If your account has a lower maximum length, that one applies. |
| Status | error | Meaning |
|---|---|---|
404 | not_found | Unknown assistant, or the conversation belongs to another assistant or is not in the api channel. |
422 | no_version | The assistant has no version yet. |
409 | reply_in_progress | A reply is already running in this conversation. |
413 | input_too_long | The question exceeds your account's maximum length; limit and length give both numbers. |
429 | quota_exhausted | The monthly quota is used up. scope is month, along with month, budget, used and retry_after: the seconds until the start of next month, also sent as the Retry-After header. |
422 | – | A field rule is broken (message, errors). |
The quota is explained in Plans and quotas. A conversation runs at most one reply at a time; if a run breaks off, the conversation is free again after 180 seconds. A conversation marked as done in the app can be continued too, and the question reopens it: if it had been handed over (escalated_at set), it becomes escalated with a new escalated_at and is back in the inbox; otherwise it becomes open; resolved_at is cleared. No event is sent for this, and a rejected question reopens nothing. A new conversation records as its origin the host from the Origin or Referer header if your call sends one; otherwise the origin stays empty.
Event stream
The response has the type text/event-stream; charset=utf-8 and is delivered uncached (Cache-Control: no-cache, no-store). Each event is an event: line with its type and a data: line with a JSON object, followed by a blank line. The ID of a new conversation arrives in the first event, start; keep it for follow-up questions.
Shell command bash
curl -N https://app.xicbot.de/api/v1/integration/chat \
-H "X-Api-Key: $XICBOT_KEY" \
-H "Content-Type: application/json" \
-d '{"assistant_id": "…", "text": "What are your opening hours?"}'
event: start
data: {"conversation_id":"…","question_id":null}
event: text
data: {"t":"We are open Monday "}
event: text
data: {"t":"to Friday."}
event: end
data: {"state":"done","message_id":"…",…}| Event | Fields | Meaning |
|---|---|---|
start | conversation_id, question_id | Start of the reply; names the conversation. |
text | t | The next chunk of reply text. Notices added afterwards and the fallback reply also arrive as text. |
citations | citations with number, chunk_id, document, heading, excerpt | The sources a finished reply points to by their number in square brackets; excerpt holds at most 300 characters of the passage. For a reply from an active question and answer pair, exactly one citation arrives: number 1, chunk_id made of qa: and the pair's ID, the pair's question as document and heading set to null; the reply itself does not name this number. |
follow_ups | questions, dropped, checked, reason | Suggested follow-up questions after a finished reply, at most three. With checked: true every suggestion was checked against your knowledge and dropped counts the discarded ones; with checked: false, reason gives the cause, for example no_knowledge. |
contact_offer | offered | The assistant offers to take contact details. |
handoff | handoff, reason | The conversation is handed to a person. reason is for example assistant, conversation_limit or enforcement when the check against the assistant's boundaries hands over. |
verification | state, claims, total | Result of checking hard facts against the sources, after a finished reply. state is no_claims, verified, unverified, no_source (there was nothing to check against) or, for an empty reply, skipped; claims lists unsupported passages and total gives their overall number. After a reply from a question and answer pair this event is not sent, because there the text is the maintained source itself. |
redaction | text, reason, count, parts | The reply was corrected after the fact; text holds the entire new wording. With reason: claims, placeholders replace unsupported facts, [nicht belegt] when the reply language is German or undetermined and [not backed] otherwise; count gives their number. If a tool card interrupted the reply, parts is added: the same corrected text in the pieces between the cards, which together give exactly text; if it cannot be split that way, parts is missing. With reason: notice, the required notice about a boundary was rewritten, without count. |
enforcement | state, topics, handover | Result of checking the reply against the assistant's boundaries, after a finished reply: state is clear or enforced, topics lists the topics touched and handover says whether the conversation is handed over. |
tool | id, key, kind, state, preview, input, result, report, error, duration_ms, approval_path, needs_confirmation, chain_capped, created_at | The assistant called a tool. needs_confirmation is true when state is proposed. |
model_switch | from, to, reason | Switch to a different tier during the reply. |
fallback | reason, transient | The reply could not be produced; transient says whether the error was temporary. The assistant's fallback reply follows as text. |
error | code, limit | A block before the call, for example conversation_limit once the maximum number of questions per conversation is reached; limit gives that number. The operator sets the maximum for each account individually in the Operations area, see Tenants and limits. A notice follows as text, with conversation_limit also a handoff, and finally end with state: blocked. |
live | live, since | A person on your team has taken over the conversation; the assistant does not answer. The stream then ends with end and state: live. |
revoked | reason | Consent was withdrawn during the reply; reason is consent_revoked. The text produced so far is discarded and not stored. |
end | see below | End of the stream. |
Field in end | Content |
|---|---|
state | done, aborted, fallback, blocked, revoked or live. With live, only message_id follows, set to null. |
message_id | ID of the stored reply; null if none was stored |
verification_state, enforcement_state | Outcome of the two checks, with the values from verification and enforcement; skipped when the check did not run, for example after a block or a fallback reply; verification_state is also skipped for a reply from a question and answer pair |
refused | true when a finished reply was a refusal |
duration_ms, ttft_ms | Reply duration and time to the first text chunk, in milliseconds |
tokens_in, tokens_out | Units used for input and output; both 0 for a reply from a question and answer pair, because no model is asked there |
throttled, reply_limit | Whether the reply ran throttled because of the quota, and the maximum reply length in units |
citations | The sources of the reply |
Webhooks triggered
The endpoints of this API fire the following webhooks, provided you have set up a receiver for the event, see Keys and webhooks. All events with every trigger, including those outside the integration, are listed under Events.
| Event | Label in the app | Trigger | Payload |
|---|---|---|---|
lead.created | Enquiry created | POST /leads creates a new enquiry | id, conversation_id set to null, source |
knowledge.indexed | Knowledge source indexed | Text ingested or website fetch succeeded | source_id, documents, chunks; for text also reason set to null |
knowledge.failed | Knowledge source failed | Ingest failed | source_id, reason; for text also documents and chunks |
lead.created | Enquiry created | POST /chat: the assistant creates an enquiry through a tool | id, conversation_id |
appointment.requested | Appointment requested | POST /chat: the assistant creates an appointment through a tool | id, conversation_id |
handoff.triggered | Handover triggered | POST /chat: the reply hands a conversation that has not been handed over yet to a person | conversation_id, reason, queue (null if no routing rule applied) |
gap.detected | Knowledge gap detected | POST /chat: a finished reply cites no source and the assistant made no tool call while producing it, even if the search found passages in your knowledge; replies from a question-and-answer pair, replies with a tool call (even one that was only proposed), questions under 12 characters and accounts without knowledge report nothing | conversation_id, assistant_id, question (at most 300 characters), kind (refused or uncited) |
budget.exhausted | Budget exhausted | POST /chat: the usage of a reply reaches the monthly quota for the first time | month, budget, used |
Frequent questions
Why does the API answer 401 when the key looks right?
Check that the key is in the X-Api-Key header and starts with xbot_. A revoked or expired key answers invalid_key, exactly like an unknown one. A key in the query string gets 400 with key_in_url.
How do I avoid duplicate enquiries when my system retries?
Send your own ID in external_id with POST /leads. A retry with the same ID answers 200 with duplicate_prevented: true and returns the existing enquiry.
Can I call the API straight from the browser?
Not from another domain: the integration endpoints send no CORS headers. Call them from your backend, which is also where the key stays.
How do I continue a conversation through the API?
Take conversation_id from the start event of the first reply and send it with the next question to POST /chat. Only conversations of the same assistant in the api channel can be continued; while a reply is still running you get 409.
How do I find out whether a website was ingested as a knowledge source?
POST /knowledge/sources with url answers straight away with 202 and status new. The outcome of the fetch arrives through the knowledge.indexed or knowledge.failed webhook, with the source ID in source_id.
Ask it yourself, Assistant of this website
Automated assistant