Data security and tenant isolation
How XICBOT keeps accounts apart, checks roles, masks personal data, logs plain-text access and limits outgoing requests to public targets.
In plain language
This page explains how your data is protected. The data of one account is kept apart from all other accounts. Each role may only do certain things. Names and other personal details in chats are hidden. The page says who may see the full text. Each such access is written down. Requests to the outside only go to public targets.
All documentation topics
This page explains how the app keeps one account's data apart from every other account, which role may do what, how personal data in conversations is masked, who can see the plain text, how keys are stored and which targets outgoing requests may reach. It is written for account owners and administrators.
Tenant isolation
Each account is a separate tenant. Isolation lives in the data layer rather than in individual screens: every query on account data is restricted to the account of the current request, and new records are assigned to that account automatically. If no account can be determined, the query returns no rows instead of reading across account boundaries.
Where the account of a request comes from depends on the access path:
| Access path | Account comes from | Response if the account is not active |
|---|---|---|
| App, signed in | the sign-in; with several accounts, the one chosen via Switch account | 403 with tenant_ plus the status, for example tenant_suspended; text “Das Konto ist derzeit nicht aktiv.” |
| Widget | the widget key in the embed code | 404 unknown_widget, the same response as for an unknown, disabled or paused assistant |
| Interface | the API key in the X-Api-Key header | 403 tenant_suspended |
The status is checked again on every request. If an account is suspended, access ends for sessions that are already open as well. The app then shows “This account is locked. Please write to mail@xicbot.de.” A new sign-in to an inactive account is rejected with 403 and recorded under Recent sign-ins with the result blocked. This list shows at most 50 entries, newest first; more in Account and security.
Roles and permissions
Roles are ranked. A function requires a minimum role, and every higher role may use it as well. The Owner role is not assigned in team management. How roles are assigned is covered in Team and roles.
| Role in the app | Key | Rank |
|---|---|---|
| Read | viewer | 10 |
| Edit | editor | 20 |
| Administration | admin | 30 |
| Owner | owner | 40 |
These minimum roles apply to the topics on this page:
| Function | Minimum role |
|---|---|
| Read conversations and download them masked as CSV | Read, plus a confirmed email address |
| API keys and webhooks under Interfaces | Administration |
| Read the Audit log | Administration, plus a confirmed email address |
| Request plain text, including for CSV export via the interface | Owner, plus a confirmed email address |
| Change an assistant's Handover address | Owner |
If a required email confirmation is missing, the interface responds with 403 email_unverified. If the role is missing, it responds with 403 role_too_low and names the required role in the required field; for the Handover address it also returns handoff_email in the field field. The app shows a message that depends on the required role:
| Required | Message in the app |
|---|---|
editor | “With the Read role this area is view-only. Changes need at least the Edit role.” |
admin | “This needs at least the Administration role.” |
owner | “Only the account owner can do this.” |
Switching between accounts
If you belong to several accounts, use Switch account. Roles apply per account: after switching you work with the role you hold in the target account. An account that does not exist and an account you do not belong to produce the same 404 not_found response, so the response does not reveal whether someone else's account exists. A suspended target account returns 403 with tenant_ plus the status. Switching also works from within a suspended account.
A switch is normally recorded in the audit log as tenant.switched, together with the previous account and the role in the new one. Switching is blocked while the operator has taken over the account. More on multiple accounts in Team and roles.
POST /api/v1/tenant/switch
Switches to another account of the same person. Field tenant (required, string of up to 64 characters). Responses: 404 not_found for accounts that belong to someone else or do not exist, 403 with tenant_ plus the status for a suspended target, 403 in_impersonation during a takeover.
- Authentication
- Sign-in token; runs without checking the status of the current account
- Rate limit
- separate rate limit for account switching, see API limits
Sign-in and sessions
Each sign-in receives its own sign-in token valid for 30 days. The app keeps it in the browser's storage (localStorage) under xicbot.marke. Signing out deletes only the token of that device; sign-ins on other devices remain active.
- The device list shows valid sign-ins, at most 50, newest first. Expired tokens are no longer listed.
- Every sign-in other than your own can be ended individually with End, or all at once with End all others. Operator takeover sessions are left in place by End all others; they can be ended individually. API keys also remain valid; their number is normally recorded in the
account.sessions_endedaudit log entry. - Changing the password ends sessions; the number is normally recorded in the
account.password_changedaudit log entry. - Signing in, signing out, ending one session and ending all other sessions normally appear in the audit log as
account.login,account.logout,account.session_endedandaccount.sessions_ended. If this ends an operator takeover, because it is ended with End in its row or someone signs out from within it, the audit log recordsimpersonation.endinstead, see Operator takeover.
Masking in conversations
Conversations are stored as they took place. Masking happens on output: the app shows personal data replaced by placeholders and notes in the conversation “Personal data is masked: email, phone, IBAN and postal address.” The interface lists the kinds found in a conversation in the masked_kinds field.
| Kind | Placeholder | Detection |
|---|---|---|
[E-Mail] | addresses with @ and a domain | |
| Phone | [Telefon] | starts with + and a country code or with 0, at least 7 digits; spaces, slashes, brackets and hyphens count as separators, dots do not |
| IBAN | [IBAN] | two letters and two digits at the start; checked before phone numbers |
| Postal address | [Adresse] | street name with an attached German suffix such as -straße, -str., -weg, -allee, -platz, -gasse, -ring, -damm, -ufer, -chaussee or -steig plus a house number; postcode and town only at the start of a line or after a separator |
The placeholders appear in their German form, including in the English interface. If a text cannot be checked, [nicht maskierbar] is shown in its place: when in doubt, the text is masked. Masking applies to:
- the text of every message in the history
- the conversation title in the list and the detail view
- the content of tool calls, except pure structure fields
- questions in Analytics, which are also cut to 300 characters
- the handover reason in the conversation view
- the question in
gap.detectedand the reason inhandoff.triggeredsent to your webhooks, see API keys and webhooks - the values under Uncovered claims for an answer
- both CSV files, see CSV export
The email sent to the Handover address when a conversation is handed over is not masked: it contains up to the last 200 messages of the conversation verbatim. Only the Owner can change this address.
Searching the conversation list uses the plain text for the owner and the masked text for every other role. A search for a phone number therefore finds nothing for these roles that they are not allowed to see. If such a search matches more than 20,000 conversations, the interface responds with 422 search_too_broad; narrow the date range or filters. More in Conversations.
Requesting plain text
Only the owner can request the plain text of a conversation. For other roles the app shows the note “Only the account owner can request the plain text – with a reason, and every access is logged.”
-
Step 1
Open the conversation
Open the conversation under Conversations.
-
Step 2
Request plain text
Select Request plain text.
-
Step 3
Give a reason
Under Reason for this access, enter why you need the plain text: at least 5 and at most 200 characters. There is no default. If the reason is too short, the app shows “Please give a reason of at least five characters.”
-
Step 4
Confirm
Confirm with Show plain text. The conversation appears unmasked, headed by “Plain text visible – this access is logged.”
Log entry first, data second
Before any plain-text output, the audit log entry conversation.revealed is written with the reason and the number of messages. If the entry cannot be written, the plain text stays hidden and the app reports “Not saved: the entry in the audit log could not be written. Please try again.”
The role is checked again with every request. The plain text applies to this view only: when the conversation is reloaded it appears masked again, and another access needs a new reason. If the role is no longer there, the app shows “Only the account owner can request the plain text.”
List of plain-text access
Each conversation lists the last 20 accesses under Plain-text access, with time, the person's email address and Reason. Every role that can open the conversation sees this list. If there has been no access yet, it reads No plain-text access so far.
POST /api/v1/conversations/{id}/reveal
Returns a conversation in plain text. Field reason (required, 5 to 200 characters). The response matches the detail view, with masked: false. Errors: 422 if the reason is missing or shorter than 5 characters, 403 role_too_low without the owner role, 404 not_found for an unknown conversation, 500 not_logged if the audit log entry fails.
- Authentication
- Sign-in token, owner role
- Rate limit
- shared with the other conversation endpoints, see API limits
CSV export
The app offers two CSV files, both with masked content. Every export writes the audit log entry conversation.exported before the file is produced. It records whether the export is in plain text, the reason and the number of rows, plus the filter used for the list and, for a single conversation, whether it was cut off. If this entry fails for a plain-text export, no file is returned; a masked export continues because it discloses nothing.
| File | Button | Scope | Columns |
|---|---|---|---|
conversations.csv | Filtered conversations as CSV | conversations matching the current filter, up to 5,000; beyond that 422 too_many and “Too many conversations for one export. Please narrow the date range.” | id, created_at, assistant, channel, language, status, messages, title, escalation |
gespraech-<id>.csv | This conversation as CSV | one conversation; very long histories are cut off at 500 messages and 500 tool calls | time, role, text, state, rating, tool_key, tool_state |
- The name of the single-conversation file contains only the conversation ID, not its title.
- If a field starts with
=,+,-,@or a tab, an apostrophe is prepended so that spreadsheet software does not evaluate it as a formula. - Line breaks inside a field are replaced with spaces. The separator is a semicolon.
- In
conversations.csv, the title and the handover reason are masked.
Via the interface, the owner can retrieve a plain-text export using the parameters reveal=1 and reason (5 to 200 characters). Without the owner role the response is 403 role_too_low; without a sufficient reason it is 422 reason_required. If your plan does not include export, both the list and the single-conversation file return 402 plan_feature_missing, masked or not. The response carries the headers x-rows (number of rows), x-masked (1 masked, 0 plain text) and, for the single-conversation file, x-capped (1 if cut off). The app itself downloads masked files only. More in Conversations and API: conversations and management.
GET /api/v1/conversations/export
Filtered conversation list as CSV, up to 5,000 conversations. Optional reveal (0 or 1) and reason (up to 200 characters).
- Authentication
- Sign-in token; plain text only with the owner role
- Rate limit
- see API limits
GET /api/v1/conversations/{id}/export
History of one conversation as CSV, cut off at 500 messages or 500 tool calls. Parameters as for the list.
- Authentication
- Sign-in token; plain text only with the owner role
- Rate limit
- see API limits
Operator takeover
To help with questions about an account, the XICBOT operator can take it over. The takeover uses the highest-ranked login currently working in this account: Owner first, then Administration, Edit and Read, and the oldest login where roles are equal. A member currently working in another account is not considered. The password of that login is neither read nor changed. The takeover runs on a separate sign-in token that expires after 4 hours. The operator starts it with Take over account and can give a reason of up to 200 characters under Reason for the takeover. The start, with the reason and expiry time, is recorded in the operator's action log and in your account's audit log; if either entry cannot be written, the takeover does not begin. The end is usually recorded in both logs as well: when the operator closes the takeover with End takeover or signs out within it, when you end it in the device list, and when it ends because all sessions of the login end, for example when the password is reset via the link, when the operator resets two-factor sign-in or when the login is removed from the team. An ongoing takeover also ends when the login taken over switches to another account itself; its end is then usually recorded in the audit log of the account where it began and in the operator's action log. If both sides end the takeover at the same moment, the end appears only once in each log. Only when it expires on its own after 4 hours is no end entry written; the expiry time is then part of the start entry. An account without any login cannot be taken over.
- In the device list of the login that was taken over, the session appears as Takeover by the operator. It can be ended early there with End; End all others leaves it in place.
- Changes made during the takeover appear in the account's Audit log under the login that was taken over.
- When the current login is queried, the interface reports a running takeover in the
impersonationfield, with the operator's email address (operator) and the expiry time (expires_at).
During a takeover, functions that could lock the login out or weaken it are blocked. The interface responds to them with 403 in_impersonation. The block applies only to requests made from within the takeover; your own sessions are not affected:
| Blocked | Message in the app |
|---|---|
| Change password | “The password cannot be changed during a support session.” |
| Set up, confirm or turn off two-factor sign-in | “This area is locked during an impersonation session.” |
| Add or remove security keys | “This area is locked during an impersonation session.” |
| End all others | “This area is locked during an impersonation session.” |
| Switch account | “This area is locked during an impersonation session.” |
| Operator area | “This area is locked during an impersonation session.” |
During the takeover, the operator sees Takeover with their address in the status bar, and screen readers hear “You are working inside a customer account.”; it ends with End takeover. This deletes the takeover's sign-in token; if it is not ended, it expires after 4 hours. A request to end a takeover when none is running returns 400 no_impersonation.
Passwords, keys and secrets
| Item | Storage | Display |
|---|---|---|
| Password | as a hash | cannot be retrieved |
| Two-factor secret | encrypted | not returned by the interface |
API key, starts with xbot_ | hash only; the first 12 characters remain readable as an identifier | plain text once on creation, in the plaintext_once field |
Webhook secret, starts with whsec_ | encrypted | once on creation, in the secret_once field |
| Shop connection access key | encrypted | only … and the last 4 characters |
- Each account can have at most 10 active API keys at a time. Beyond that the interface responds with 422
too_many_keysand the app with “The maximum number of simultaneous keys has been reached.” - The API key belongs in the
X-Api-Keyheader. If it is passed in the URL as theapi_keyorkeyparameter, the response is 400key_in_url. If the header is missing or its value does not start withxbot_, the response is 401key_missing. - Unknown, revoked and expired keys all get the same 401
invalid_keyresponse, so the response does not reveal whether a key was ever valid.
Creating and revoking keys is described in API keys and webhooks and API authentication, the shop connection in Catalog and shop.
Outgoing requests
Several functions fetch addresses outside XICBOT: webhooks, tools with their own interface, website sources in the knowledge base, the shop connection and sources created via the interface. The same target check applies to all of them:
- Addresses must use
httporhttps. - The host name is resolved for IPv4 and IPv6. Every address found must be on the public internet; private, link-local, reserved and loopback ranges are blocked. IPv4 addresses written in IPv6 notation are checked as well.
- The check runs when the target is saved. Webhooks, tools, website sources and the shop connection are checked again right before each request.
- Webhooks and tools connect only to the addresses that have just been checked, and they do not follow redirects.
- Website sources follow redirects but check each new target on its own.
When saving, the response is 422 with one of the following reasons in the error field. Under Interfaces, the app shows “This address is not permitted as a target.” for all four reasons. Under Triggers › Tools, it gives the reason at the Interface address field, for example “The host name of this address cannot be resolved. Please check the spelling.”; all sentences used there are listed under Which addresses are allowed. The delivery list of a webhook and the test delivery show the individual messages:
| Reason | Meaning | Message for deliveries |
|---|---|---|
url_unreadable | the address cannot be parsed | “The address cannot be read.” |
scheme_not_allowed | neither http nor https | “This address type is not supported.” |
name_unresolvable | the host name resolves to no address | “The host name of the address cannot be resolved.” |
target_not_allowed | at least one address is not on the public internet | “This target is not allowed, for example an internal address.” |
redirect_not_followed | the target redirected (webhooks only) | “The target redirected. Redirects are not followed – please enter the final address.” |
Timeouts and sizes
A tool with its own interface sends its request as a POST with JSON. The timeout is 8 seconds; a tool's deadline_s value is clamped to 1 to 20 seconds, and connecting may take at most 5 seconds. Responses larger than 64 KiB are cut off. If an access key is stored, it is sent as Authorization: Bearer. Webhooks also time out after 8 seconds. More in Tools and Knowledge.
Error responses
Error responses from the interface contain no internal details such as exception type, file, line or stack trace. For status codes of 500 and above, any message text is replaced with “Serverfehler”; a response without a text does not get one, so not_logged reads just {"error":"not_logged"}. Messages about input errors with status codes below 500 are kept so they can be matched to the affected field.
Audit log
The Audit log records who did what in the account: each entry contains the time, the action, the person's email address, the affected item and additional details. It covers a defined set of actions, not every page view and not every read. Logins with at least the Administration role and a confirmed email address can read it. How the list is laid out and how to filter and page through it is described in Team and roles. These actions appear in the audit log, each under its label in the app:
| Area | Actions |
|---|---|
| Sign-in | Signed in, Signed out, Session ended, All other sessions ended, Password changed (with the number of sessions ended), Password reset, Two-factor enabled, Two-factor disabled, Security key added, Security key removed |
| Account | Account created, Account confirmed, Confirmation email resent, Account switched (with the previous account and the role in the new one), Account deleted |
| Team | Person invited, Invitation revoked, Invitation accepted, Role changed, Member removed |
| Assistants | Assistant created, Version published, Version restored, Assistant deleted, Widget key renewed, Check run performed |
| Tools and routing | Tool created, Tool changed (noting whether the minimum role, the target address or the access key changed), Tool deleted, Tool chain capped, Routing rule created, Routing rule changed, Routing rule moved, Routing rule deleted |
| Knowledge | Knowledge source created, Knowledge source changed, Knowledge source re-read, Knowledge source deleted, Knowledge version restored, Knowledge recomputed, Question-answer pair created, Question-answer pair changed, Question-answer pair deleted, Probe question created, Probe question deleted, Probe questions run, Search weights changed, Search weights tuned |
| Conversations and visitors | Conversation content viewed (with the reason and the number of messages), Conversation exported (with details on plain text, reason and row count), Conversation deleted, Record deleted, Attachment stored, Attachment opened, Visitor blocked, Visitor block lifted |
| Legal and data | Retention periods changed, Retention period applied, Data exported, Attachments exported, Data processing agreement accepted, Objection to subprocessor, Report delivery changed |
For many changes the entry is written in the same transaction as the change itself: when a conversation is deleted, for tools and routing rules, for changes to the team, to knowledge sources, question-answer pairs and search weights, when a knowledge version is restored, for the second factor, for security keys and when a visitor is blocked. If the entry cannot be written, the previous state is kept; when a conversation is deleted, the app then reports “Not saved: the entry in the audit log could not be written. Please try again.” Exempt are actions that should not fail because of a logging error: creating and confirming an account, resending the confirmation email, signing in and out, ending sessions, changing or resetting the password, switching accounts, creating or re-reading a knowledge source, recomputing knowledge, running probe questions and check runs, and storing visitor attachments. They succeed even if the entry cannot be written; in that case the entry is missing. A visitor attachment, by contrast, is only delivered if the download is recorded in the audit log. How long entries are kept is described in Retention and deletion.
A takeover by the operator appears in this log with its start and end: as Takeover by the operator started, with the reason (reason) and expiry time (expires_at) under Details, and as Takeover by the operator ended, with ended_by under Details: operator if the operator ended it, otherwise customer. If it ends because the password is reset via the link, or because the login is removed from the team or switches to another account itself, Details also shows the cause as cause: password_reset, member_removed or tenant_switched. The operator's action log records start and end as well; you can also spot a running takeover in the device list, see Takeover by the operator. Whatever is changed during a takeover appears in the audit log under the login that was taken over.
Frequent questions
Can I see who changed something in the account?
Yes, in the Audit log, available from the Administration role with a confirmed email address. It records a defined set of actions, such as sign-ins, changes to the team, assistants, tools and knowledge, plain-text access, exports and deletions, each with the time and the person's email address. A takeover by the operator is listed there as Takeover by the operator started and Takeover by the operator ended; you can also spot a running takeover in the device list.
Who can read conversations in plain text?
The Owner, and only with a reason of at least five characters. Each access is logged before anything is shown and appears in the conversation under Plain-text access. All other roles see email addresses, phone numbers, IBANs and postal addresses masked.
Why does my CSV file show placeholders instead of phone numbers?
The app's CSV files are masked in the same way as the conversation view. A plain-text export is reserved for the owner via the interface, using reveal=1 and a reason; it is recorded in the Audit log.
What happens if I enter an internal address as a webhook?
The address is rejected when you save it, and the app shows “This address is not permitted as a target.” If a host name later points to an internal address, the check before delivery stops the request; the delivery list then shows “This target is not allowed, for example an internal address.”
What does the operator see during a takeover?
The operator works with the permissions of the login that was taken over. Plain text of a conversation is visible only if that login may request it, and then with a reason and an entry under Plain-text access. The operator cannot change the password, two-factor sign-in or security keys.
Ask it yourself, Assistant of this website
Automated assistant