Skip to content
XICBOT

Settings

Deutsch
Documentation · API reference

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.

Last updated

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.

MethodPath after {key}PurposeThrottle
GET/Fetch the widget settingswidget
POST/conversationStart a conversationwidget
GET/conversation/{conversation}Load the historywidget
POST/sendSend a question, answer as an event streamwidget-send
POST/conversation/{conversation}/revokeWithdraw consentwidget
POST/conversation/{conversation}/tool/{call}/confirmConfirm a tool callwidget-send
POST/conversation/{conversation}/tool/{call}/rejectReject a tool callwidget
POST/conversation/{conversation}/tool/{call}/reportReport the outcome of a form fillwidget
POST/conversation/{conversation}/message/{message}/ratingRate an answerwidget
GET/conversation/{conversation}/exportExport the stored datawidget
DELETE/conversation/{conversation}Delete the conversationwidget
POST/conversation/{conversation}/attachmentUpload an attachmentwidget-send
GET/conversation/{conversation}/attachmentList attachmentswidget
GET/conversation/{conversation}/attachment/{attachment}Download an attachmentwidget

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:

StatuserrorMeaning
404unknown_widgetNo assistant with this key, the widget is switched off or the assistant is not active.
404unknown_widgetThe account the assistant belongs to is not active.
404demo_expiredThe 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_id in 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:

FieldContent
errortoo_many_requests
limit_per_minPer-minute value of the rejecting cap, as a number
limit_per_dayPer-day value of the same cap
waitSeconds 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 parameterRequiredRule
hostnoHost name of the embedding page. A full URL is reduced to its host name, lowercased; anything beyond 120 characters is cut off.
previewnoBoolean. With true the service does not record the embedding.
languagenoLanguage 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:

FieldContent
titleWidget title; the assistant name if no title is set
color, position, verticalColour and corner of the widget as strings
offset_x, offset_y, width, height, scale, radiusOffsets, width, height and corner radius in pixels, scale in percent (100 means unchanged), each as an integer
avatarURL of the avatar image; an empty string means no image
languageLanguage of the active version, de if there is none
greetingGreeting of the active version, otherwise empty
outreach_rulesProactive outreach rules; the chat window evaluates them in the browser
noteMachine notice: “You are chatting with an automated assistant.”
limit_noticeLimits notice: “Note: This assistant is a program, it can be wrong and it does not replace personal advice from our team.”
consenttrue if the assistant requires consent before the first message
consent_textCustom consent text or the default text in the selected language; null if no consent is required
attachmentstrue if visitors may attach files
attachment_max_bytes5242880, that is 5 MiB per file
attachment_typesimage/png, image/jpeg, image/webp, application/pdf
attachment_max_count5 attachments per conversation
formsForms the chat window may fill in, see below
readytrue 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 formsContent
toolKey of the tool
selectorSelector of the form on the page
pagePage the form is on; empty if no particular page is set
fieldsList of fields with name and type
submit_allowedtrue 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
FieldRequiredRule
hostnoString, 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.
visitornoString, at most 64 characters. Visitor identifier from the chat window; an empty value is not stored.
viewportnoOne of handy, tablet, desktop, breit.
consentif consent is requiredBoolean. Only the JSON value true counts as consent; 1 or "1" pass validation but are not enough.
languagenoString, 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_textnoString, 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:

  1. Step 1

    Validate the fields

    Invalid fields return 422 with message and errors.

  2. Step 2

    Active version

    If the assistant has no active version, the response is 422 with no_version.

  3. Step 3

    Call cap

    If the cap is reached, 429 with too_many_requests follows.

  4. Step 4

    Domain list

    If host does not match the domain list, the response is 403 with host_not_allowed.

  5. Step 5

    Block list

    If the visitor’s IP address is blocked, 403 with blocked follows.

  6. Step 6

    Consent

    If the assistant requires consent and consent: true is missing, the response is 422 with consent_missing. If a consent_text sent along matches no current consent text, 422 with consent_changed follows; consent_text then carries the current text in the language selected by language. These checks run before creation: without the required consent, the service does not create a conversation.

  7. Step 7

    Create

    Only now does the service create the conversation with status open and 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.

StatuserrorMeaning
422message, errorsA field is invalid.
422no_versionThe assistant has no active version.
429too_many_requestsCall cap reached.
403host_not_allowedhost is missing or does not match the domain list.
403blockedThe visitor’s IP address is blocked.
422consent_missingConsent required, consent is not true. Nothing was created.
422consent_changedThe 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
FieldRequiredRule
conversation_idyesID from the start, at most 64 characters.
textyesThe question, 1 to 4000 characters.
suggestednoBoolean. 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.

StatuserrorMeaning
422message, errorsA field is invalid.
429too_many_requestsQuestion cap reached.
404not_foundNo conversation of this assistant in the widget channel with this ID.
403consent_revokedConsent has been withdrawn.
403consent_missingThe assistant requires consent and the conversation has none, for example because the requirement was switched on after the start.
403blockedThe visitor’s IP address or this conversation is blocked.
422no_versionThe version the conversation started with no longer exists.
409reply_in_progressAn answer is already running for this conversation.
413input_too_longThe 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.

EventDataMeaning
startconversation_id, question_id (null)First event of every stream.
texttA piece of the answer. Append the pieces in the order they arrive.
toolTool call in brief formThe assistant used a tool or proposes a step that needs confirmation (needs_confirmation). Fields under Tool calls.
livelive (true), sinceA staff member has taken over the conversation; since is the time of the takeover.
errorcode, plus scope and retry_after or limitThe answer is blocked, see Special cases.
model_switchreason (transient)After a transient fault, the answer is produced another way; the stream continues.
fallbackreason (transient or error), transientThe answer failed; the stored fallback reply follows as text.
revokedreason (consent_revoked)Consent was withdrawn during the run; the answer is discarded.
citationscitationsThe 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_offeroffered (true)The assistant offers to put the visitor in touch.
follow_upsquestions, dropped, checked, reasonUp 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.
handoffhandoff (true), reasonHandover 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.
redactiontext, reason (claims or notice), with claims also count and, where applicable, partsReplaces 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.
verificationstate, claims, totalResult of the source check: skipped, no_claims, verified, unverified or no_source.
enforcementstate (clear or enforced), topics, handoverResult of the topic check.
endstate, message_id, refused, citationsLast 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, start is followed only by live and by end with state set to live and message_id set to null. The staff member writes the answer, see Taking over.
  • Quota exhausted: error follows with code set to quota_exhausted, scope set to month and retry_after in seconds until the start of next month, then the fallback reply as text and end with blocked. 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, error follows with conversation_limit and limit, a notice as text, handoff with conversation_limit and end with blocked. The default is 60 questions; the value 0 switches the cap off.
  • Fault: During a transient fault, model_switch may arrive and the answer continues. If it fails, fallback, the fallback reply as text and end with fallback follow.
  • 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 revoked and end with revoked.
  • Answer from a question and answer pair: If the question matches an active pair, no model is asked. start is followed by the pair's answer in a single text, citations with exactly one citation (the pair's question as document, chunk_id made of qa: and the pair's ID) and enforcement; if the question touches a restricted topic or a taboo, redaction with notice, a notice as another text and handoff may follow. Finally end arrives with done. There is no verification, follow_ups or contact_offer in this case. See Question and answer pairs.
  • Post-processing: redaction replaces the text shown. With claims, unsupported statements are replaced by the placeholders [nicht belegt] or [not backed]; with notice, the topic check has rewritten the text. After enforcement, a notice may follow as another text, as may a notice about unsupported statements if the assistant may only answer from its knowledge.
state in endMeaning
doneThe answer ran to its regular end.
fallbackThe fallback reply stands in the conversation instead of the answer.
revokedConsent was withdrawn during the run; the answer is discarded.
blockedQuota exhausted or conversation cap reached.
liveA 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.

EntryFields
Visitor questionrole (user), text, citations
Assistant answerrole (assistant), text, citations, id, rating (null, 0 or 1)
Staff messagerole (operator), text, citations, id
Tool callid (w followed by the call ID), role (tool), tool with the call in brief form

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 withdrawalResult
POST send403 with consent_revoked
Rate an answer403 with consent_revoked
Upload an attachment422 with consent_revoked
History, data export, deletionremain 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.

StatuserrorMeaning
429too_many_requestsCall cap reached.
404not_foundConversation unknown.
422without_consentThe 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
FieldRequiredRule
ratingyes, may be null1 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.

StatuserrorMeaning
429too_many_requestsCall cap reached.
422message, errorsrating is missing or not null, 0 or 1.
404not_foundConversation unknown or the message does not belong to this conversation.
403consent_revokedConsent has been withdrawn.
422not_rateableThe 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
FieldContent
conversationstarted, host, message_count, consent_at, consent_revoked_at
messagesAll messages, ascending, with role, text and time
records.tool_callsTool calls in brief form
records.requestsContact requests with name, email, phone, subject, time
records.appointmentsAppointments with starts_at, type, name, email, phone, status, time
records.attachmentsAttachments with the same fields as in the list
records.cartsCarts with items, subtotal_cents, discount_cents, coupon_code, shipping_cents, total_cents, currency, status, time
controllerName of the account, that is the operator of the site running the widget
retention_daysRetention 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.

StatuserrorMeaning
429too_many_requestsCall cap reached.
404not_foundConversation unknown, including after it has been deleted.
500not_loggedThe 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:

FieldContent
idID of the call, a UUID
keyKey of the tool
kindKind of tool
stateState, see below
previewPreview of the step for the card
inputInputs of the call
resultResult after execution
reportReport of a form fill, otherwise empty
errorIn the widget the fixed German text “Der Schritt konnte nicht ausgeführt werden.”, null without an error
needs_confirmationtrue in state proposed
chain_cappedtrue if the call failed at the limit of 5 calls per answer
created_atTime 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.

stateMeaning
proposedProposed, waiting for confirmation or rejection.
confirmedConfirmed, execution is running.
executedExecuted; the result is in result.
rejectedRejected by the visitor.
expiredThe proposal is older than 60 minutes and can no longer be confirmed.
failedFailed, for example because the tool has since been removed or the role, plan or required fields no longer fit.
timeoutExecution 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
StatuserrorMeaning
429too_many_requestsCall cap reached (confirm and reject).
404not_foundConversation 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
FieldRequiredRule
gefuelltnoList of at most 20 field names of at most 80 characters each: fields that were filled
fehlendnoList as above: fields that were not found on the page
gesperrtnoList as above: fields that could not be filled
abgesendetnoBoolean: the form was submitted
gefundennoBoolean: 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.

StatuserrorMeaning
429too_many_requestsCall cap reached.
422invalid_reportBody 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.
404not_foundConversation or call unknown, or the call is not a form fill.
409not_executedThe 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.

LimitValue
Size per file5 MiB (5242880 bytes), at least 1 byte
Attachments per conversation5
Typesimage/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
FieldContent
idAttachment ID (UUID)
nameSanitised file name
mimeThe type detected from the content
bytesSize in bytes
sha256SHA-256 of the content, hexadecimal
lead_idID of the contact request the attachment belongs to, or null
timeUpload 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.

StatuserrorMeaning
429–Named throttle widget-send reached.
429too_many_requestsCall cap reached.
403attachments_disabledThe assistant does not accept files.
404not_foundConversation unknown.
422consent_revokedConsent was withdrawn; the conversation accepts nothing more.
403blockedThe visitor’s IP address or this conversation is blocked.
422file_missingThe request contains no usable file in the file field.
422upload_failedThe file transfer failed.
413too_largeFile larger than 5 MiB, or empty.
422too_manyThe conversation already has 5 attachments.
415type_not_allowedContent 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
HeaderValue
Content-TypeThe detected type, otherwise application/octet-stream
Content-LengthSize in bytes
Content-Dispositionattachment with filename in ASCII and filename* in UTF-8
X-Content-Type-Optionsnosniff
Content-Security-Policydefault-src 'none'; sandbox
Cache-Controlprivate, no-store, max-age=0
Referrer-Policyno-referrer
StatuserrorMeaning
429too_many_requestsCall cap reached.
404not_foundConversation unknown or the attachment does not belong to this conversation.
410file_missingThe 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.

Something still unclear?

Tell us which page and which step you mean. We reply personally.

Enquiry in three steps

  1. 1. Topic
  2. 2. Your case
  3. 3. Contact

What is this about?

One click is enough, everything else is optional.

Tell us briefly about your case

Everything in this step is optional. It helps us reply with something concrete straight away instead of asking back.

Enquiries per month, roughly (optional)
When should it start? (optional)
How can we reach you?

Name and email address are enough. We reply personally.

We use your details to handle your enquiry. More on this in the privacy policy.