Widget endpoints
All 14 widget endpoints under /api/v1/public/widget/{key}: settings, conversation, answer stream, consent, data export, deletion, tool calls and attachments.
In plain language
The chat window talks to the service through its own endpoints. These endpoints need no sign-in. The key of the window decides the assistant. An ID ties each call to one chat. The page describes each endpoint with fields, answer and errors. This includes sending questions, rating answers and deleting chats. Files and withdrawing consent are covered too.
All documentation topics
The chat window talks to the service through 14 endpoints under https://app.xicbot.de/api/v1/public/widget/{key}. They require no sign-in: the widget key in the path selects the assistant, and the conversation ID returned when a conversation starts ties every further call to that conversation. This page describes each endpoint with its request fields, response and errors. How the chat window uses them is shown in Chat in the widget; endpoints outside the widget are listed under Public endpoints.
Overview
The paths in the table follow https://app.xicbot.de/api/v1/public/widget/{key}. The address, the error format and the rules for calls from the browser are described in API basics, the throttle counters in API limits.
| Method | Path after {key} | Purpose | Throttle |
|---|---|---|---|
| GET | / | Fetch the widget settings | widget |
| POST | /conversation | Start a conversation | widget |
| GET | /conversation/{conversation} | Load the history | widget |
| POST | /send | Send a question, answer as an event stream | widget-send |
| POST | /conversation/{conversation}/revoke | Withdraw consent | widget |
| POST | /conversation/{conversation}/tool/{call}/confirm | Confirm a tool call | widget-send |
| POST | /conversation/{conversation}/tool/{call}/reject | Reject a tool call | widget |
| POST | /conversation/{conversation}/tool/{call}/report | Report the outcome of a form fill | widget |
| POST | /conversation/{conversation}/message/{message}/rating | Rate an answer | widget |
| GET | /conversation/{conversation}/export | Export the stored data | widget |
| DELETE | /conversation/{conversation} | Delete the conversation | widget |
| POST | /conversation/{conversation}/attachment | Upload an attachment | widget-send |
| GET | /conversation/{conversation}/attachment | List attachments | widget |
| GET | /conversation/{conversation}/attachment/{attachment} | Download an attachment | widget |
Common rules
Key in the path
The key {key} is the value of the data-xicbot attribute in the embed code: 16 random bytes written as 32 characters from 0 to 9 and a to f. It is not a secret, since it appears in the source of every page that shows the widget. The service checks the key before every endpoint and otherwise responds as follows:
| Status | error | Meaning |
|---|---|---|
| 404 | unknown_widget | No assistant with this key, the widget is switched off or the assistant is not active. |
| 404 | unknown_widget | The account the assistant belongs to is not active. |
| 404 | demo_expired | The demo the widget belongs to has expired. |
A new key is created by POST /api/v1/assistants/{id}/key with the admin role, see API authentication. The service logs the change first. If that fails, it responds with 500 and not_logged, and the current key stays in place. Otherwise the new value is returned in key, and the old key yields unknown_widget from then on. Replace the embed code on every page afterwards, see Public key.
Throttles and caps
Two layers limit the endpoints. The named throttle counts per visitor, formed from the visitor’s IP address (the first 64 bits for IPv6) and the widget key: widget allows 60 and widget-send 12 calls per minute, see Limits per visitor. Behind it, the service checks the widget caps, whose values belong to the account.
- The call cap applies to settings, start, history, withdrawal, rating, data export, deletion, to confirming, rejecting and reporting a tool call, and to uploading, listing and downloading attachments. The defaults are 60 calls per minute and 2000 per day.
- The question cap applies only to
POST send. The defaults are 10 questions per minute and 200 per day. - Each cap counts on the visitor and, if the call names a conversation, also on that conversation. The conversation is taken from the path or from
conversation_idin the body. Either counter can reject; counting only happens once neither has rejected. - The windows are 60 seconds and 86,400 seconds. A cap below 1 is switched off.
When a cap rejects a call, the response is 429 with this body and the Retry-After header:
| Field | Content |
|---|---|
error | too_many_requests |
limit_per_min | Per-minute value of the rejecting cap, as a number |
limit_per_day | Per-day value of the same cap |
wait | Seconds until the next attempt; Retry-After carries the same value |
Conversation ID
Starting a conversation returns conversation_id, a UUID. POST send and every endpoint with {conversation} in the path look up the conversation by this ID, the assistant of the key and the widget channel. If any of these does not match, the response is 404 with not_found. A conversation of another assistant or from another channel is therefore out of reach through the widget. The IDs of messages, tool calls and attachments are UUIDs as well and are only looked up within the conversation named in the path.
Calls from the browser
The CORS rules under /api/v1/public/ admit the origin null. The chat window runs in a frame with sandbox="allow-scripts" and without allow-same-origin, and such a frame sends Origin: null. Which sites may start a conversation is decided not by CORS but by the domain list when a conversation starts. Methods, headers and the preflight lifetime are listed in API basics.
Errors
Errors arrive as JSON with a code in error; some endpoints add further fields, listed in the tables below. Validation of the request fields responds with 422, a text in message and a list per field in errors, see Error format. Evaluate the code, not the text. Once the answer stream of POST send has started, it reports errors as events rather than as a status.
Fetching the settings
GET /api/v1/public/widget/{key}/
Returns the appearance, texts and switches of the widget. The chat window calls it on load and builds itself from the result. Counts against the call cap.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
| Query parameter | Required | Rule |
|---|---|---|
host | no | Host name of the embedding page. A full URL is reduced to its host name, lowercased; anything beyond 120 characters is cut off. |
preview | no | Boolean. With true the service does not record the embedding. |
language | no | Language preference. The first two letters count; de and en are valid. |
With host and without preview, the service records the time and host name of the embedding, at most once every 300 seconds for the same host name. This endpoint does not check host against the domain list; that only happens when a conversation starts.
language only affects the texts the service supplies itself: note, limit_notice and the default consent text. The greeting and outreach messages stay as written in the assistant. Without a valid preference, en applies if the active version of the assistant is English, otherwise de.
The response is a JSON object with these fields:
| Field | Content |
|---|---|
title | Widget title; the assistant name if no title is set |
color, position, vertical | Colour and corner of the widget as strings |
offset_x, offset_y, width, height, scale, radius | Offsets, width, height and corner radius in pixels, scale in percent (100 means unchanged), each as an integer |
avatar | URL of the avatar image; an empty string means no image |
language | Language of the active version, de if there is none |
greeting | Greeting of the active version, otherwise empty |
outreach_rules | Proactive outreach rules; the chat window evaluates them in the browser |
note | Machine notice: “You are chatting with an automated assistant.” |
limit_notice | Limits notice: “Note: This assistant is a program, it can be wrong and it does not replace personal advice from our team.” |
consent | true if the assistant requires consent before the first message |
consent_text | Custom consent text or the default text in the selected language; null if no consent is required |
attachments | true if visitors may attach files |
attachment_max_bytes | 5242880, that is 5 MiB per file |
attachment_types | image/png, image/jpeg, image/webp, application/pdf |
attachment_max_count | 5 attachments per conversation |
forms | Forms the chat window may fill in, see below |
ready | true once the assistant has an active version |
The default consent text is “I agree that my conversation is stored and processed in order to answer my question.” The attachment limits are part of the response so that the chat window can reject a file that is too large before uploading it.
Field in forms | Content |
|---|---|
tool | Key of the tool |
selector | Selector of the form on the page |
page | Page the form is on; empty if no particular page is set |
fields | List of fields with name and type |
submit_allowed | true if the chat window may also submit the form |
forms holds at most 10 entries, ordered by tool key: only active form-fill tools that the public role may use. It contains no values; the chat window only writes into the listed fields. Setup is described in Form prefill, the rules behind outreach_rules in Outreach rules.
Starting a conversation
POST /api/v1/public/widget/{key}/conversation
Creates a conversation in the widget channel and returns its ID. Response 201 with conversation_id. Counts against the call cap.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
| Field | Required | Rule |
|---|---|---|
host | no | String, at most 120 characters. Host name of the embedding page; a full URL is reduced to its host name. Empty or null counts as missing. |
visitor | no | String, at most 64 characters. Visitor identifier from the chat window; an empty value is not stored. |
viewport | no | One of handy, tablet, desktop, breit. |
consent | if consent is required | Boolean. Only the JSON value true counts as consent; 1 or "1" pass validation but are not enough. |
language | no | String, at most 35 characters. The same language preference as when fetching the settings; the first two letters count, de and en are valid. Without consent_text, it selects the consent text the check value is computed over; if the text shown no longer applies, it selects the language of the current text in the response. The chat window sends the same preference here as when fetching the settings. |
consent_text | no | String, at most 2000 characters. The consent text as the chat window showed it; the chat window sends it along. It only counts if consent is required and it is not empty. It is compared with the current text in both languages; runs of whitespace count as a single space, and leading and trailing whitespace does not count. If it matches, the check value is computed over the text in that language. |
The service checks in this order and stops at the first failure:
-
Step 1
Validate the fields
Invalid fields return 422 with
messageanderrors. -
Step 2
Active version
If the assistant has no active version, the response is 422 with
no_version. -
Step 3
Call cap
If the cap is reached, 429 with
too_many_requestsfollows. -
Step 4
Domain list
If
hostdoes not match the domain list, the response is 403 withhost_not_allowed. -
Step 5
Block list
If the visitor’s IP address is blocked, 403 with
blockedfollows. -
Step 6
Consent
If the assistant requires consent and
consent: trueis missing, the response is 422 withconsent_missing. If aconsent_textsent along matches no current consent text, 422 withconsent_changedfollows;consent_textthen carries the current text in the language selected bylanguage. These checks run before creation: without the required consent, the service does not create a conversation. -
Step 7
Create
Only now does the service create the conversation with status
openand respond with 201.
If the assistant’s domain list is empty, any site may start a conversation. If it has entries, host must be present and either equal an entry or end with a dot followed by an entry: the entry example.com also covers shop.example.com. Entries are separated by a comma or a line break and compared in lower case. You maintain the list under Allowed domains.
Blocked visitors can no longer create a conversation; the block applies before a conversation is created. How to block visitors is described in Blocking visitors.
If the assistant requires consent, the conversation stores the time in consent_at and a check value in consent_hash. The check value is a SHA-256 over the consent text the visitor sees in the selected language, the privacy notice version and the host name. That is the assistant’s custom text, otherwise the default text for the language. If you send the wording shown in consent_text, the language whose text it matches applies. Without it, language selects the language as when fetching the settings; without a valid preference, en applies if the active version is English, otherwise de. In that case, send the same preference at the start as when fetching the settings: if the default text applies, the check value would otherwise record the wording of a different language than the one shown. Consent is thus tied to the conversation and not to a value in the browser. More in Consent.
After creating the conversation, the service fires the conversation.started webhook with conversation_id, assistant_id and channel.
| Status | error | Meaning |
|---|---|---|
| 422 | message, errors | A field is invalid. |
| 422 | no_version | The assistant has no active version. |
| 429 | too_many_requests | Call cap reached. |
| 403 | host_not_allowed | host is missing or does not match the domain list. |
| 403 | blocked | The visitor’s IP address is blocked. |
| 422 | consent_missing | Consent required, consent is not true. Nothing was created. |
| 422 | consent_changed | The consent_text sent along matches no current consent text; consent_text in the response carries the current text. Nothing was created. |
Sending a question
POST /api/v1/public/widget/{key}/send
Stores the visitor’s question and responds with an event stream. The conversation ID is sent in the body, not in the path. Counts against the question cap, not the call cap.
- Authentication
- None
- Rate limit
widget-send, 12 per minute per visitor, plus the question cap
| Field | Required | Rule |
|---|---|---|
conversation_id | yes | ID from the start, at most 64 characters. |
text | yes | The question, 1 to 4000 characters. |
suggested | no | Boolean. true if the visitor picked a suggested follow-up question; the service records this on the question. |
If the account has a length cap below 4000 characters, that cap applies. The question is stored before the stream begins. If the conversation is closed, the stored question reopens it: if it had been handed over (escalated_at set), it becomes escalated with a new escalated_at, otherwise open; resolved_at is cleared. No event is sent for this, and a rejected question reopens nothing. At most one answer runs per conversation; the service claims the conversation with a conditional update. If a run does not end cleanly, the claim releases the conversation after 180 seconds.
| Status | error | Meaning |
|---|---|---|
| 422 | message, errors | A field is invalid. |
| 429 | too_many_requests | Question cap reached. |
| 404 | not_found | No conversation of this assistant in the widget channel with this ID. |
| 403 | consent_revoked | Consent has been withdrawn. |
| 403 | consent_missing | The assistant requires consent and the conversation has none, for example because the requirement was switched on after the start. |
| 403 | blocked | The visitor’s IP address or this conversation is blocked. |
| 422 | no_version | The version the conversation started with no longer exists. |
| 409 | reply_in_progress | An answer is already running for this conversation. |
| 413 | input_too_long | The question exceeds the account’s length cap; limit states the cap, length the length. |
Shell command bash
curl -N https://app.xicbot.de/api/v1/public/widget/0123456789abcdef0123456789abcdef/send \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
-d '{"conversation_id":"9d1f6c2a-3b4e-4f5a-8c7d-1e2f3a4b5c6d","text":"I have a question about my order."}'Event stream
After the checks, the endpoint responds with status 200, Content-Type: text/event-stream; charset=utf-8 and Cache-Control: no-cache, no-store. Each event consists of an event: line with the name, a data: line with a JSON object and a blank line. Non-ASCII characters and slashes appear unescaped in the JSON. The chat window sends Accept: text/event-stream for this call. How it renders the events is described in Answer stream.
| Event | Data | Meaning |
|---|---|---|
start | conversation_id, question_id (null) | First event of every stream. |
text | t | A piece of the answer. Append the pieces in the order they arrive. |
tool | Tool call in brief form | The assistant used a tool or proposes a step that needs confirmation (needs_confirmation). Fields under Tool calls. |
live | live (true), since | A staff member has taken over the conversation; since is the time of the takeover. |
error | code, plus scope and retry_after or limit | The answer is blocked, see Special cases. |
model_switch | reason (transient) | After a transient fault, the answer is produced another way; the stream continues. |
fallback | reason (transient or error), transient | The answer failed; the stored fallback reply follows as text. |
revoked | reason (consent_revoked) | Consent was withdrawn during the run; the answer is discarded. |
citations | citations | The sources the answer uses; only if there are any. For an answer from a question and answer pair, exactly one citation with the pair's question. |
contact_offer | offered (true) | The assistant offers to put the visitor in touch. |
follow_ups | questions, dropped, checked, reason | Up to 3 suggested follow-up questions; only if any remain. checked states whether they were checked against the knowledge, dropped how many were removed in the process. Without a check, reason is no_knowledge (no knowledge stored) or no_tenant_context, otherwise null. |
handoff | handoff (true), reason | Handover to a person is recommended. reason is assistant, conversation_limit or, after the topic check, a text naming the topics involved, for example Gesperrtes Thema: …; without such a text, enforcement. |
redaction | text, reason (claims or notice), with claims also count and, where applicable, parts | Replaces the text shown so far entirely with text. If tool calls interrupted the answer, parts may carry the same text in pieces split at those points; empty pieces are dropped. |
verification | state, claims, total | Result of the source check: skipped, no_claims, verified, unverified or no_source. |
enforcement | state (clear or enforced), topics, handover | Result of the topic check. |
end | state, message_id, refused, citations | Last event of the stream. |
citations, contact_offer, follow_ups, handoff with assistant, redaction, verification and enforcement only arrive when the answer runs to its regular end. A shortened stream looks like this:
Source code text
event: start
data: {"conversation_id":"9d1f6c2a-3b4e-4f5a-8c7d-1e2f3a4b5c6d","question_id":null}
event: text
data: {"t":"I am happy to help "}
event: text
data: {"t":"with your order."}
event: verification
data: {"state":"no_claims","claims":[],"total":0}
event: enforcement
data: {"state":"clear","topics":[],"handover":false}
event: end
data: {"state":"done","message_id":"4b8e2f10-6c3d-4a5e-9f7b-2d1c0e9a8b76","refused":false,"citations":[]}Special cases
In each of these cases the question has been stored before the stream begins.
- Takeover: If a staff member has taken over the conversation,
startis followed only byliveand byendwithstateset toliveandmessage_idset tonull. The staff member writes the answer, see Taking over. - Quota exhausted:
errorfollows withcodeset toquota_exhausted,scopeset tomonthandretry_afterin seconds until the start of next month, then the fallback reply astextandendwithblocked. The status stays 200; a 429 for the quota only exists in the integration API channel. See Quota exhausted. - Conversation cap: If the visitor has already asked as many questions in this conversation as the account cap allows,
errorfollows withconversation_limitandlimit, a notice astext,handoffwithconversation_limitandendwithblocked. The default is 60 questions; the value 0 switches the cap off. - Fault: During a transient fault,
model_switchmay arrive and the answer continues. If it fails,fallback, the fallback reply astextandendwithfallbackfollow. - Withdrawal during the run: For conversations with consent, the run checks at most once per second, and once more at the end, whether consent has been withdrawn. If so, it discards the answer and sends
revokedandendwithrevoked. - Answer from a question and answer pair: If the question matches an active pair, no model is asked.
startis followed by the pair's answer in a singletext,citationswith exactly one citation (the pair's question asdocument,chunk_idmade ofqa:and the pair's ID) andenforcement; if the question touches a restricted topic or a taboo,redactionwithnotice, a notice as anothertextandhandoffmay follow. Finallyendarrives withdone. There is noverification,follow_upsorcontact_offerin this case. See Question and answer pairs. - Post-processing:
redactionreplaces the text shown. Withclaims, unsupported statements are replaced by the placeholders[nicht belegt]or[not backed]; withnotice, the topic check has rewritten the text. Afterenforcement, a notice may follow as anothertext, as may a notice about unsupported statements if the assistant may only answer from its knowledge.
state in end | Meaning |
|---|---|
done | The answer ran to its regular end. |
fallback | The fallback reply stands in the conversation instead of the answer. |
revoked | Consent was withdrawn during the run; the answer is discarded. |
blocked | Quota exhausted or conversation cap reached. |
live | A staff member has taken over the conversation. |
message_id is the ID of the stored answer; you use it to rate the answer. If nothing was stored, the value is null. refused is true only with done, when the assistant declined the question. citations lists the sources of the run.
Loading the history
GET /api/v1/public/widget/{key}/conversation/{conversation}
Returns the history so far, so that the chat window resumes where the visitor left off after the page reloads. Counts against the call cap.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
The response contains messages and taken_over. messages consists of the latest 50 messages and the latest 50 tool calls, sorted together in ascending order of time. taken_over is true while a staff member has taken over the conversation. If the conversation is unknown, the response is 404 with not_found.
| Entry | Fields |
|---|---|
| Visitor question | role (user), text, citations |
| Assistant answer | role (assistant), text, citations, id, rating (null, 0 or 1) |
| Staff message | role (operator), text, citations, id |
| Tool call | id (w followed by the call ID), role (tool), tool with the call in brief form |
Withdrawing consent
POST /api/v1/public/widget/{key}/conversation/{conversation}/revoke
Withdraws consent for this conversation. The visitor needs no account for this, only the conversation ID. No body is required. Counts against the call cap.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
The response is {"revoked": true, "run_aborted": …}. run_aborted is true if the conversation was claimed for an answer at that moment; a running answer is then discarded and its run ends with revoked. The withdrawal is set once: a second call returns the same response and does not change the time. More in Withdrawing consent.
| Endpoint after withdrawal | Result |
|---|---|
POST send | 403 with consent_revoked |
| Rate an answer | 403 with consent_revoked |
| Upload an attachment | 422 with consent_revoked |
| History, data export, deletion | remain available |
Withdrawal, data export and deletion remain open to blocked visitors as well; the service checks the block list only when starting, sending and uploading an attachment.
| Status | error | Meaning |
|---|---|---|
| 429 | too_many_requests | Call cap reached. |
| 404 | not_found | Conversation unknown. |
| 422 | without_consent | The conversation was started without consent; there is nothing to withdraw. |
Rating an answer
POST /api/v1/public/widget/{key}/conversation/{conversation}/message/{message}/rating
Stores the rating of an assistant answer. {message} is message_id from the end event or id from the history. Response {"message": {"id": …, "rating": …}}. Counts against the call cap.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
| Field | Required | Rule |
|---|---|---|
rating | yes, may be null | 1 for Yes, 0 for No on “Was this helpful?”; null removes the rating. |
The cap is checked before the fields. The service stores the time together with the rating; null clears both. How the chat window shows the buttons is described in Rating.
| Status | error | Meaning |
|---|---|---|
| 429 | too_many_requests | Call cap reached. |
| 422 | message, errors | rating is missing or not null, 0 or 1. |
| 404 | not_found | Conversation unknown or the message does not belong to this conversation. |
| 403 | consent_revoked | Consent has been withdrawn. |
| 422 | not_rateable | The message is not an assistant answer. |
Data export
GET /api/v1/public/widget/{key}/conversation/{conversation}/export
Returns everything stored for this conversation as JSON. The visitor needs no account for this, only the conversation ID. Counts against the call cap; if the conversation is unknown, the response is 404 with not_found.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
| Field | Content |
|---|---|
conversation | started, host, message_count, consent_at, consent_revoked_at |
messages | All messages, ascending, with role, text and time |
records.tool_calls | Tool calls in brief form |
records.requests | Contact requests with name, email, phone, subject, time |
records.appointments | Appointments with starts_at, type, name, email, phone, status, time |
records.attachments | Attachments with the same fields as in the list |
records.carts | Carts with items, subtotal_cents, discount_cents, coupon_code, shipping_cents, total_cents, currency, status, time |
controller | Name of the account, that is the operator of the site running the widget |
retention_days | Retention period for conversations in days as set on the account, or null |
Times use the ISO 8601 format. More on the visitor’s rights in Access request.
Deleting a conversation
DELETE /api/v1/public/widget/{key}/conversation/{conversation}
Deletes the conversation. The visitor needs no account for this, only the conversation ID. Response {"deleted": true, "messages": n} with the number of deleted messages. Counts against the call cap.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
The deletion is logged first: the log entry with the trigger visitor and the counts is written in the same database transaction as the deletion and before it. If no entry can be written, the service deletes nothing and responds with 500 and not_logged. What is deleted along with the conversation is described in What goes with a conversation.
| Status | error | Meaning |
|---|---|---|
| 429 | too_many_requests | Call cap reached. |
| 404 | not_found | Conversation unknown, including after it has been deleted. |
| 500 | not_logged | The log entry could not be written; nothing was deleted. Try again later. |
Tool calls
When the assistant proposes a step that changes something, or calls a read-only tool whose approval is with confirmation, the stream carries a tool event with state set to proposed and needs_confirmation set to true. The step only runs once the visitor confirms it: the confirm endpoint is the only way a changing tool runs in the widget; the answer run itself only executes reading tools. The cards in the chat window are described in Tool cards, the tools themselves in Tools.
The event, the history, the data export and the endpoints in this section show a call in the same brief form:
| Field | Content |
|---|---|
id | ID of the call, a UUID |
key | Key of the tool |
kind | Kind of tool |
state | State, see below |
preview | Preview of the step for the card |
input | Inputs of the call |
result | Result after execution |
report | Report of a form fill, otherwise empty |
error | In the widget the fixed German text “Der Schritt konnte nicht ausgeführt werden.”, null without an error |
needs_confirmation | true in state proposed |
chain_capped | true if the call failed at the limit of 5 calls per answer |
created_at | Time of the call |
The widget does not expose the duration or approval path of a call, nor the note and email_notified fields of the result. At most 5 tool calls run per answer; a further one ends with failed and chain_capped set to true.
state | Meaning |
|---|---|
proposed | Proposed, waiting for confirmation or rejection. |
confirmed | Confirmed, execution is running. |
executed | Executed; the result is in result. |
rejected | Rejected by the visitor. |
expired | The proposal is older than 60 minutes and can no longer be confirmed. |
failed | Failed, for example because the tool has since been removed or the role, plan or required fields no longer fit. |
timeout | Execution exceeded its time limit. |
Confirming a call
POST /api/v1/public/widget/{key}/conversation/{conversation}/tool/{call}/confirm
Confirms a proposed step and executes it. No body. Response {"call": …, "duplicate": …}. Counts against the call cap.
- Authentication
- None
- Rate limit
widget-send, 12 per minute per visitor, plus the call caps
First the service sets a proposal older than 60 minutes to expired. It then moves the call from proposed to confirmed with a conditional update; of two simultaneous clicks only one wins. On the click it checks again whether the tool still exists, whether role and plan allow it and whether the required fields are filled; otherwise the call ends with failed. Execution ends with executed, timeout or failed.
duplicate is true if the call had already been decided or another click won; the step then does not run a second time. call shows the current state in every case, including expired.
Rejecting a call
POST /api/v1/public/widget/{key}/conversation/{conversation}/tool/{call}/reject
Rejects a proposed step. No body. Response {"call": …, "duplicate": …} with state set to rejected; a proposal older than 60 minutes becomes expired instead. Counts against the call cap.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
| Status | error | Meaning |
|---|---|---|
| 429 | too_many_requests | Call cap reached (confirm and reject). |
| 404 | not_found | Conversation unknown or the call does not belong to this conversation. |
Reporting a form fill
POST /api/v1/public/widget/{key}/conversation/{conversation}/tool/{call}/report
Reports what the loader script did on the customer's page with an executed form-fill call. Response {"report": …, "duplicate": …}. Counts against the call cap.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
| Field | Required | Rule |
|---|---|---|
gefuellt | no | List of at most 20 field names of at most 80 characters each: fields that were filled |
fehlend | no | List as above: fields that were not found on the page |
gesperrt | no | List as above: fields that could not be filled |
abgesendet | no | Boolean: the form was submitted |
gefunden | no | Boolean: the form was found on the page |
The body is JSON. Without a JSON header the service reads the raw body up to 16384 bytes and decodes it as JSON; this lets the loader script send the report as text/plain with keepalive. Such a request needs no preflight and survives leaving the page, for example when the form navigates away on submit.
The report is a statement from the customer's page. Only field names that appear in the instruction of this call are kept. It is stored as report with filled, missing, blocked, submitted and form_found; submitted is only true if the instruction included submitting and abgesendet is true. The first report per call is accepted; any further one gets duplicate set to true and the report already stored. What the form fill enters is described in Form prefill.
| Status | error | Meaning |
|---|---|---|
| 429 | too_many_requests | Call cap reached. |
| 422 | invalid_report | Body is not a JSON object, or larger than 16384 bytes without a JSON header. |
| 422 | – | A field breaks its rule; the response carries message and errors. |
| 404 | not_found | Conversation or call unknown, or the call is not a form fill. |
| 409 | not_executed | The call is not in the executed state. |
Attachments
Visitors can attach images and PDF files to a conversation if the assistant allows it; this is switched on under Visitor files. How the visitor experiences this in the widget is described under Attachments in the chat. Besides the named throttle, all three endpoints count against the call cap.
| Limit | Value |
|---|---|
| Size per file | 5 MiB (5242880 bytes), at least 1 byte |
| Attachments per conversation | 5 |
| Types | image/png, image/jpeg, image/webp, application/pdf |
The service determines the type from the file content, not from the extension or the browser's claim: PNG, JPEG, WebP and PDF are recognised by their first bytes, everything else is rejected. It also measures the size on the received file.
Uploading an attachment
POST /api/v1/public/widget/{key}/conversation/{conversation}/attachment
Accepts a file as multipart/form-data in the field file. Response 201 with the attachment. Counts against the call cap.
- Authentication
- None
- Rate limit
widget-send, 12 per minute per visitor, plus the call caps
| Field | Content |
|---|---|
id | Attachment ID (UUID) |
name | Sanitised file name |
mime | The type detected from the content |
bytes | Size in bytes |
sha256 | SHA-256 of the content, hexadecimal |
lead_id | ID of the contact request the attachment belongs to, or null |
time | Upload time, ISO 8601 |
The service strips slashes, backslashes, quotes and control characters from the file name, keeps only the last path segment and truncates to 120 characters. If nothing is left, the file is named anhang.png, anhang.jpg, anhang.webp or anhang.pdf.
If the conversation already has a contact request, the attachment is linked to the most recent one right away; lead_id then carries its ID. If the request only comes later, it takes over all attachments of the conversation whose lead_id is still empty. The upload is usually recorded in the activity log with type and size; if that entry cannot be written, the attachment is stored anyway and the response is 201.
The checks run in the order of the table; the first one that applies determines the response. The four rejections from checking the file (upload_failed, too_large, too_many and type_not_allowed) also carry max_bytes, types and max_per_conversation so the widget can tell the visitor the limits.
| Status | error | Meaning |
|---|---|---|
| 429 | – | Named throttle widget-send reached. |
| 429 | too_many_requests | Call cap reached. |
| 403 | attachments_disabled | The assistant does not accept files. |
| 404 | not_found | Conversation unknown. |
| 422 | consent_revoked | Consent was withdrawn; the conversation accepts nothing more. |
| 403 | blocked | The visitor’s IP address or this conversation is blocked. |
| 422 | file_missing | The request contains no usable file in the file field. |
| 422 | upload_failed | The file transfer failed. |
| 413 | too_large | File larger than 5 MiB, or empty. |
| 422 | too_many | The conversation already has 5 attachments. |
| 415 | type_not_allowed | Content is neither PNG, JPEG, WebP nor PDF. |
anhang.sh bash
curl -X POST \
https://app.xicbot.de/api/v1/public/widget/0123456789abcdef0123456789abcdef/conversation/9d1f6c2a-3b4e-4f5a-8c7d-1e2f3a4b5c6d/attachment \
-F 'file=@schaden.jpg'Listing attachments
GET /api/v1/public/widget/{key}/conversation/{conversation}/attachment
Returns {"attachments": […]} with all attachments of the conversation, ascending by time, each entry with the same fields as on upload. Counts against the call cap; if the conversation is unknown, the response is 404 with not_found.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
Downloading an attachment
GET /api/v1/public/widget/{key}/conversation/{conversation}/attachment/{attachment}
Returns the file itself as a download. The attachment must belong to this conversation. Counts against the call cap.
- Authentication
- None
- Rate limit
widget, 60 per minute per visitor, plus the call caps
| Header | Value |
|---|---|
Content-Type | The detected type, otherwise application/octet-stream |
Content-Length | Size in bytes |
Content-Disposition | attachment with filename in ASCII and filename* in UTF-8 |
X-Content-Type-Options | nosniff |
Content-Security-Policy | default-src 'none'; sandbox |
Cache-Control | private, no-store, max-age=0 |
Referrer-Policy | no-referrer |
| Status | error | Meaning |
|---|---|---|
| 429 | too_many_requests | Call cap reached. |
| 404 | not_found | Conversation unknown or the attachment does not belong to this conversation. |
| 410 | file_missing | The record exists but the file is missing; the response also carries measured set to false. |
Frequent questions
Why does the API answer a wrong key with 404 rather than 403?
An unknown key, a switched-off widget, an inactive assistant and an inactive account look the same from outside: 404 with unknown_widget. Someone guessing a key cannot tell from the response whether the guess was right. The only exception is an expired demo, which reports demo_expired.
Why does sending a question return status 200 even when the quota is exhausted?
The answer stream has already started at that point. The error event with code set to quota_exhausted reports the situation, followed by the fallback answer and end with blocked. Evaluate the events, not the status.
What happens to an answer in progress when the visitor withdraws consent?
The run checks during the answer and at its end whether consent was withdrawn. It then discards the answer and sends revoked and end with revoked. The conversation accepts no further questions or attachments.
May the form-fill report be sent more than once?
Yes, but only the first report per call is stored. Every further one gets duplicate set to true and the report already stored.
Does the list of allowed domains protect against calls from outside the browser?
No. The list keeps the widget off other sites, but host is supplied by the caller. Throttles, the caps for calls and questions and blocking individual visitors act against misuse.
Ask it yourself, Assistant of this website
Automated assistant