Public API endpoints
Sign-up, sign-in, password reset, status, enquiries, demos, invitations and cart pickup: every API endpoint you can call without a session.
In plain language
The endpoints on this page need no sign-in. You use them to create an account and to sign in. You also reset a lost password there. You accept an invitation. Other endpoints show the state of the service and its changes. The contact form and the demo belong here too. Your shop uses one of these endpoints to fetch a cart.
All documentation topics
The endpoints on this page work without a session and without a widget key. They cover everything that happens before sign-in – creating an account, confirming the address, signing in, resetting a password, accepting an invitation – and what can be read from outside an account: service status, changelog, accessibility statement, enquiries, demos and the pickup of a cart by your shop. Widget endpoints are described in Widget endpoints, authentication for every other endpoint in API authentication.
Overview
| Method | Path | Purpose | Limit per origin (counter) |
|---|---|---|---|
| POST | /api/v1/register | Create an account and tenant | 5 per minute, 20 per hour (register) |
| POST | /api/v1/verify | Confirm the email address | 30 per minute (auth-token) |
| POST | /api/v1/login | Sign in, including the second step | Failed attempts: 5 per minute and 20 per hour per address and origin; wrong credentials also count towards 30 per minute and 300 per hour per origin |
| POST | /api/v1/password/forgot | Request a password reset link | 20 per hour (password-forgot), plus 3 per hour per address and origin |
| POST | /api/v1/password/reset | Set a new password | 30 per minute (auth-token) |
| GET | /api/v1/public/status | Service status | 30 per minute (status) |
| GET | /api/v1/public/changelog | Changelog | 30 per minute (status) |
| POST | /api/v1/public/enquiry | Send an enquiry | 5 per minute (enquiry) |
| POST | /api/v1/public/demo | Create a demo | 2 per minute (demo), at most 3 new demos per IP address within 24 hours |
| GET | /api/v1/public/demo/{id} | State of a demo | 30 per minute (status) |
| GET | /api/v1/public/accessibility | Accessibility statement | 30 per minute (status) |
| GET | /api/v1/invitation/{token} | Look up an invitation | 20 per minute (invitation) |
| POST | /api/v1/invitation/accept | Accept an invitation with a new account | 20 per minute (invitation) |
| GET | /api/v1/cart/pickup/{token} | Pick up a cart | 60 per minute (cart-pickup) |
Common rules
Every path starts with https://app.xicbot.de/api/v1/; the app calls them from that origin. Send JSON with Content-Type: application/json and Accept: application/json. Errors below /api/ are returned as JSON, unknown paths included. Field validation responds with 422, a text in message and, for each field, a list of messages in errors.
Rate limits
Requests are counted per origin: an IPv4 address on its own, an IPv6 address truncated to its /64 network. An IPv4 address in IPv6 notation such as ::ffff:192.0.2.10 counts as IPv4. The per-minute and per-hour limits are separate counters, and both must have room left. Endpoints with the same counter share it: status covers service status, changelog, demo lookup and accessibility together, auth-token covers confirmation and password reset, invitation covers both invitation endpoints. Once a limit is reached, the endpoint answers 429 with a Retry-After header in seconds.
Sign-in and password requests also keep counters of their own. Their 429 carries {"error": "too_many_attempts", "retry_in": …}, where retry_in gives the seconds until the next attempt is allowed. Limits for all other endpoints are listed in API limits.
Error responses
| Status | Body | Meaning |
|---|---|---|
| 404 | {"error": "route_unknown"} | The path does not exist. |
| 405 | {"error": "method_not_allowed", "allowed": […]} | The path exists but not with this method; allowed lists the permitted methods. |
| 422 | message and errors, or {"error": "…"} | Invalid fields, an invalid token or a request refused on its merits. |
| 429 | Retry-After header; for sign-in and password requests too_many_attempts with retry_in | Limit reached. |
| 500 and above | message reads Serverfehler | Unexpected server error. Exception class, file, line and stack trace are removed. Only an existing message is replaced; a response without message does not get one. |
Calls from the browser
From a browser on another site, only the endpoints below /api/v1/public/ can be read, and only from the admitted origins; origins, methods, headers and the preflight lifetime are listed in API basics. Sign-up, sign-in, password and invitation endpoints are closed to other origins; cart pickup sets its own rules, as described further down.
Create an account
POST /api/v1/register
Creates an account together with a new tenant named after the company, makes the account its owner and sends a confirmation link to the address. Responds 201 with tenant (id, name, slug), user (id, email, role) confirmation_pending: true and mail_sent (whether the message with the confirmation link is queued). The account gets the role owner, and the tenant's slug is derived from the company name.
- Authentication
- None
- Rate limit
- 5 per minute and 20 per hour per origin
| Field | Required | Rule |
|---|---|---|
email | yes | valid address according to the RFC, at most 190 characters |
password | yes | at least 12 characters, including letters and digits |
password_confirmation | yes | same as password |
company | yes | at most 160 characters; becomes the tenant name |
name | no | at most 120 characters |
locale | no | de or en, de if omitted; sets the language of the confirmation email |
The address is normalised before it is stored: surrounding whitespace removed, Unicode normalisation form C, lower case, and without the suffix after a plus sign. For providers that ignore dots in the local part, the dots are removed as well. Where a provider serves the same mailbox under two domains, the address counts as the same under both. Anna+Test@example.com therefore becomes anna@example.com. A quoted local part is left unfolded. If the normalised address is already taken, including by a deleted account, the endpoint responds with 422 and {"error": "validation", "fields": {"email": ["validation.unique"]}}. In the app this step is called Create account, and the password rule reads At least 12 characters, including letters and digits.
POST /api/v1/verify
Confirms the address with the token from the link: {"token": "…"}, at most 120 characters. Responds 200 with {"confirmed": true}. An unknown token, one already used or one older than 48 hours yields 422 with invalid_token. In the app, this is the Confirm your address page.
- Authentication
- None
- Rate limit
- 30 per minute per origin, counter
auth-token
Sign in
POST /api/v1/login
Signs in with address and password and returns a token that you send to the other endpoints as Authorization: Bearer …. The address is normalised the same way as at sign-up. Responds 200 with token, expires_at and user.
- Authentication
- None
- Rate limit
- Failed attempts: 5 per minute and 20 per hour per address and origin; wrong credentials also count towards 30 per minute and 300 per hour per origin
| Field | Required | Content |
|---|---|---|
email | yes | address of the account, at most 190 characters |
password | yes, unless challenge is sent | password of the account |
totp | no | code from the authenticator app, at most 10 characters |
challenge | in the second step | challenge from the 428 response, at most 80 characters |
webauthn | no | object with id (at most 1400 characters), client_data_json, authenticator_data and signature (at most 8000 characters each) |
locale | no | de or en; on a successful sign-in it becomes the account language, which is used for example for invitations you send and for the email after a password change. Any other value leaves the current language in place; sign-in does not fail because of it |
Second factor
-
Step 1
Send the password
If the account has a second factor and
totpis missing, the endpoint responds with 428.erroristotp_requiredwhen an authenticator app is set up, otherwisewebauthn_required. The response also carrieschallenge,expires_inset to 600 seconds andmethods, which lists the factors you can use in the second step:totp,webauthnor both. If the account only has security keys and sign-in with keys is not set up on the server, this step already returns 503webauthn_unconfiguredinstead of 428. If an authenticator app is set up, you can sendtotptogether with the password and skip the second step; on an account without an app, a code sent along yieldsbad_totp. -
Step 2
Prepare the security key
If security keys are registered and sign-in with keys is set up on the server, the response contains a
webauthnobject withchallenge,rp_id,allow(the permitted credential IDs) andtimeout, which is at least 30. Use it to request confirmation on the key in the browser. -
Step 3
Send the proof
Send
email,challengeand eithertotporwebauthn, without the password and from the same IP address as in the first step. The challenge is valid for 600 seconds and is discarded on the fifth failed attempt; a wrong code, a rejected key and a request from a different IP address all count as failed attempts. Such a request gets 422challenge_expired, but the challenge remains usable from the original address. Once it has expired or been used up, the endpoint also answers 422challenge_expired, and you start again with the password.
| Status | error | Meaning |
|---|---|---|
| 200 | – | signed in; token, expires_at and user |
| 422 | bad_credentials | wrong address or password |
| 428 | totp_required, webauthn_required | second factor required |
| 422 | bad_totp | wrong code |
| 422 | bad_webauthn | key rejected; reason states why, for example kennung_unbekannt |
| 422 | challenge_expired | challenge expired or used up, or request from a different IP address |
| 422 | webauthn_not_offered | key signature on a challenge without a key path (methods without webauthn); not a failed attempt |
| 503 | webauthn_unconfigured | the service does not accept security keys because sign-in with keys is not set up on the server; for an account with security keys only, already in response to the password |
| 403 | tenant_paused, tenant_suspended, tenant_deleted | tenant paused, suspended or deleted; reported only after valid credentials and second factor |
| 429 | too_many_attempts | too many failed attempts; retry_in in seconds |
Field in user | Content |
|---|---|
id, email, name, role, locale | basic account data |
totp_active | authenticator app set up |
webauthn_keys | number of registered security keys |
email_verified | address confirmed |
operator | account of the platform operator |
available, available_at | whether the account currently accepts handovers from conversations, and since when; if it does not, available_at is null |
The token is valid for 30 days; the exact time is in expires_at. It is named after the browser identification in User-Agent, at most 190 characters, or sitzung if none was sent. Every failed attempt counts against the counter for address and origin: wrong credentials, a wrong code, a rejected key, and an expired challenge or one sent from a different IP address. The counter for the origin alone counts only wrong credentials. A successful sign-in resets the counter for address and origin, but not the one for the origin alone. On a 429 the app shows Too many attempts. Please wait a moment., and for a blocked tenant This account is locked. Please write to mail@xicbot.de. Sessions and the second factor are covered in Sign-in and access and Account security.
Reset a password
POST /api/v1/password/forgot
Requests a link for a new password. Fields: email (required, at most 190 characters) and locale (de or en; the account language applies if omitted). The response is 200 with {"ok": true} whether or not the account exists. Only accounts that have a password receive an email.
- Authentication
- None
- Rate limit
- 20 per hour per origin; 3 per hour per address and origin; at most 10 emails per hour per mailbox
The link points to https://app.xicbot.de/kennwort/neu?token=…&sprache=en and is valid for 60 minutes. Each newly sent link replaces the previous one. At most ten links per hour go out to the same mailbox, whatever network they are requested from; beyond that the response stays 200, but no email is sent, no new link is created and the last link sent remains valid. Only the two per-origin limits answer 429: the one for address and origin combined (3 per hour, counting every request whether or not the account exists) with too_many_attempts and retry_in, and the one for the origin alone (20 per hour) with a Retry-After header. In the app, the entry point and the button are labelled Forgot your password? and Request link.
POST /api/v1/password/reset
Sets the new password. Fields: token (at most 120 characters), password and password_confirmation, following the same rules as at sign-up. Responds 200 with {"ok": true, "sessions_ended": true}: the link has been used up, and every token the account received from sign-in or an invitation has been deleted, which ends its open sessions. The tenant's API keys are separate and stay in place. An unknown or expired token yields 422 invalid_token. The button in the app is labelled Save password.
- Authentication
- None
- Rate limit
- 30 per minute per origin, counter
auth-token
Service status and changelog
GET /api/v1/public/status
Returns state, measured, parts and time. parts normally lists nine parts of the service, each with name and state; if the measurement fails as a whole, it contains a single part total with unknown. The response is cached for up to 20 seconds. The HTTP status follows the overall state in state.
- Authentication
- None
- Rate limit
- 30 per minute per origin, counter
status
state | Meaning | HTTP |
|---|---|---|
operational | every part measured and fine | 200 |
degraded | all parts measured, at least one impaired | 503 |
unknown | at least one part not measured | 503 |
GET /api/v1/public/changelog
Returns the changelog with measured, reason, version (the newest version) and entries. Each entry has version, date, kind, title and points. kind is feature, fix, security or note; any other value appears as note. Entries lacking a version, date or title are dropped. Sorting is by date, newest first, and by version within the same date. If the changelog cannot be read, the endpoint answers 503 with measured: false, a short German reason in reason and an empty entries array.
- Authentication
- None
- Rate limit
- 30 per minute per origin, counter
status
Enquiry
POST /api/v1/public/enquiry
Accepts an enquiry as sent by the form on xicbot.de. The enquiry is stored and delivered by email. Responds 202 with {"accepted": true}; the sender learns nothing beyond that, neither an ID nor the delivery state.
- Authentication
- None
- Rate limit
- 5 per minute per origin
| Field | Required | Rule |
|---|---|---|
name | yes | 2 to 120 characters |
email | yes | valid address according to the RFC, at most 190 characters |
company | no | at most 160 characters |
topic | no | a key from the topic table |
package | no | start, pro or custom: the plan whose card in the price band the enquiry came from. It appears in the email as the first line of the “Project” block; any other value yields 422 |
details | no | object with keys from the details table only; other keys yield 422 |
message | no | 10 to 5000 characters |
language | no | de or en, de if omitted |
homepage | no | trap field for bots, must stay empty; at most 200 characters |
topic | Topic in the email |
|---|---|
website-assistant | Website assistant |
shop-assistant | Shop assistant |
booking-assistant | Booking assistant |
lead-assistant | Lead assistant |
support-assistant | Support assistant |
tool-control | Tool control |
chatbot-replacement | Replace an existing chatbot |
other | Something else |
Key in details | Label in the email | Rule |
|---|---|---|
website | Website or shop | at most 190 characters |
platform | Shop system | at most 120 characters |
sources | Sources for answers | at most 300 characters |
calendar | Appointments today | at most 300 characters |
goal | Needed from a prospect | at most 300 characters |
ticket_system | Support requests today | at most 300 characters |
systems | Systems to connect | at most 300 characters |
actions | Actions in them | at most 300 characters |
gap | Missing from the current chat | at most 300 characters |
volume | Enquiries per month | lt-100 (under 100), 100-500 (100 to 500), 500-2000 (500 to 2,000), gt-2000 (over 2,000), unknown (Not sure) |
timeframe | Start | asap (As soon as possible), quarter (In 1–3 months), half-year (In 3–6 months), open (Not decided yet) |
Demo
POST /api/v1/public/demo
Creates a time-limited demo for a public website: a tenant with an assistant that answers only from that website's content, and a widget that runs only on that website. The website is read in the background. Fields: url (required, http or https, at most 500 characters), email (at most 190 characters, stored with the demo) and language (de or en, de if omitted; the assistant's language). Responds 201 with a demo object, or 200 if a demo for the same website is already running.
- Authentication
- None
- Rate limit
- 2 per minute per origin; at most 3 new demos per IP address within 24 hours
What counts is the host of the address: lower case, without www. and without the scheme's default port. https://www.example.com/contact and https://example.com/ therefore lead to the same demo for example.com; a non-default port stays part of the host. If an active demo for this host is already running, you get it back with existing: true; nothing new is created and nothing counts against the daily limit. The demo widget is restricted to the host without its port.
| Status | error | Meaning |
|---|---|---|
| 201 | – | demo created |
| 200 | – | a demo for this host was already running (existing: true) |
| 422 | invalid_url | address without a usable host |
| 422 | scheme_not_allowed, url_unreadable, name_unresolvable, target_not_allowed | scheme other than http or https, unreadable address, name that does not resolve or target not permitted |
| 429 | demo_ip_limit | three demos from this IP address within 24 hours already |
| 503 | demo_capacity | 50 demos are running at the same time |
| 500 | demo_unconfigured | demos are not set up on the server |
Field in demo | Content |
|---|---|
id | ID of the demo for the lookup |
host | host the demo applies to |
expires_at | expiry, 72 hours after creation |
expired | true once the period is over |
widget_key | public key of the demo widget |
assistant | id, name and status of the assistant |
knowledge | reading progress with status, documents and chunks; new and running mean “still in progress”, not “empty” |
existing | on creation only: whether the demo existed already |
job | when a new demo is created only: ID of the job that reads the website |
GET /api/v1/public/demo/{id}
Returns the current state as demo with the fields above, without existing and job. An expired demo is not an error: the response stays 200 with expired: true and still states the expiry. 404 not_found if the ID does not belong to a demo or the demo is no longer active.
- Authentication
- None
- Rate limit
- 30 per minute per origin, counter
status
Accessibility statement
GET /api/v1/public/accessibility
Returns the app's accessibility statement as an accessibility object with the text fields standard, conformance, checked_on, version, method, scope and feedback and the lists measures and limitations. The page https://app.xicbot.de/barrierefreiheit reads it without signing in. If no statement is configured, the endpoint answers 503 accessibility_missing instead of returning empty text.
- Authentication
- None
- Rate limit
- 30 per minute per origin, counter
status
Accept an invitation
An invitation from a team arrives as the link https://app.xicbot.de/einladung?token=…&sprache=en and is valid for 168 hours. The token has 48 characters; only its SHA-256 value is stored. How invitations are created and what each role may do is covered in Team and roles.
GET /api/v1/invitation/{token}
Shows what a token stands for: tenant (the tenant name), email, role (viewer, editor or admin), expires_at and account_exists, which tells whether an account for this address exists already; a deleted account does not count. If the invitation is unknown, expired, accepted or withdrawn, the endpoint answers 404 invalid_token.
- Authentication
- None
- Rate limit
- 20 per minute per origin, counter
invitation
POST /api/v1/invitation/accept
Accepts the invitation with a new account. Fields: token (at most 120 characters), password and password_confirmation following the sign-up rules, name (at most 120 characters) and locale (de or en, de if omitted). The address counts as confirmed. Responds 201 with token (valid for 30 days), expires_at, user (id, email, name, role, locale) and tenant (id, name). 422 invalid_token if the invitation is no longer open or the inviting account has been deleted; 409 login_required with email if an account for the address exists already and has not been deleted. A deleted account with this address is restored and starts like a new one: with the password from this request, without two-factor authentication, without security keys and without earlier tokens. Without a log entry neither the account nor the membership is created, the invitation stays open, and the response is 500 with just {"error":"not_logged"}, without message.
- Authentication
- None
- Rate limit
- 20 per minute per origin, counter
invitation
Pick up a cart
When the assistant puts a cart together during a conversation, the cart tool gives the visitor an address for your shop as next_url: the handover address configured there with ?token=… appended, or &token=… if the address already contains a query. Until then, the cart is held by XICBOT. The widget shows the link Continue, which opens next_url in a new tab. Your shop uses the token to read the cart and builds its own cart from it. If the assistant changes the open cart, the token stays the same; if it puts the cart together from scratch, a new cart with a new token is created. Setup is described in Catalog and shop and Tools.
GET /api/v1/cart/pickup/{token}
Returns the cart for the token. The token consists of 40 letters and digits and is the only means of access. The endpoint only reads: it changes neither the cart nor its expiry and can safely be repeated. Responses with status 200, 404 and 410 carry Cache-Control: no-store.
- Authentication
- None; the token in the path
- Rate limit
- 60 per minute per origin
| Field | Content |
|---|---|
items | line items with external_id, title, amount and unit_price_cents; price_source is live (just read from the shop) or stand (taken from the catalogue) and is missing in older carts |
subtotal_cents, discount_cents, shipping_cents, total_cents | amounts in cents |
coupon_code | redeemed coupon code |
currency | currency of the cart |
created_at, updated_at | creation and last change |
expires_at | end of the pickup period |
| Status | error | Meaning |
|---|---|---|
| 200 | – | the cart |
| 404 | unknown_cart | unknown or malformed token, or the tenant is not active |
| 410 | cart_expired | the token was valid, but the pickup period is over |
The period is 7 days from the last change; whenever the assistant changes the cart during the conversation, it starts again. The response contains no ID of the tenant, the conversation or the handover and nothing about the visitor: holding the token gives access to the cart, not to the conversation.
Shell command bash
curl https://app.xicbot.de/api/v1/cart/pickup/<token> \
-H 'Accept: application/json'Frequent questions
Why does the status endpoint return 503 while the app is reachable?
The endpoint answers 200 only when every part was measured and is fine. If one is impaired, state is degraded; if one was not measured, it is unknown. parts shows which part it is.
Does the password request reveal whether an account exists?
No. The response is 200 with {"ok": true} whether or not the account exists and whether or not an email goes out. The limits that answer 429 count every request, regardless of whether the account exists.
How long is a token from sign-in or an invitation valid?
30 days; the exact time is in expires_at. Setting a new password through /api/v1/password/reset deletes every token of the account at once; API keys are not affected.
Can a page pick up the cart directly in the browser?
Only from the origin of the handover address configured in the assistant's cart tool. Any other page is refused. The intended caller is the shop's server.
What happens if the same website is requested as a demo twice?
If an active demo for the host is still running, it is returned with 200 and existing: true. No second demo is created, and the request does not count towards the limit of three demos per IP address.
Ask it yourself, Assistant of this website
Automated assistant