API keys and webhooks
Create API keys with scopes, limits and expiry, set up webhooks for ten events, verify signatures and track deliveries.
In plain language
On this page of the app you create keys. With a key, your own systems can call the API. You also create webhooks there. A webhook reports events to an address of your system. The page explains who sees and uses this area. It shows how a message is delivered and signed. It also says when data is deleted.
All documentation topics
Under Interfaces you create API keys that let your own systems call the integration API, and webhooks through which XICBOT reports events to an address on your side. The page lives at https://app.xicbot.de/schnittstellen, in the Administration group of the menu. The basic concepts of the API are covered in API basics.
Who can see and use the page
The Interfaces menu item appears from the Administration role upwards; Owner includes it. With Read or Edit the item is missing, and the API answers 403 with the code role_too_low. Anyone who opens the page anyway sees only “This needs at least the Administration role.”, without forms or lists. Roles are explained in Team and roles.
Keys and webhooks belong to a feature that only the Custom plan includes. Without it, the page shows the sentence “Your … plan does not include this feature. See Billing for the overview.”, with your plan name in place of the ellipsis, and the Go to billing link instead of the forms; if no plan name is known, the sentence reads “Your plan does not cover this. See Billing for the overview.” Keys and webhooks from before a plan change remain listed below, and the sentence “Existing keys and webhooks stay visible here: keys can be revoked and webhooks deleted. As long as the plan does not include the feature, the API rejects these keys and webhooks receive no deliveries.” is added. Revoke, Deliveries and Delete remain; Send test, Turn off and Turn on are not shown. Through the API, creating, changing and testing answer 402 with plan_feature_missing, and so does every call with such a key; the list, the delivery log, revoking and deleting work without the feature. Once the plan includes the feature again, the same keys and webhooks continue to work; events from the time in between are not delivered later. Which plans include the feature is shown under Pricing and Plans and quotas.
- Only the Owner role can create a key with the Read conversations scope, because that scope returns messages in plain text. For other roles the checkbox is disabled. The API rejects such an attempt with
403,role_too_lowand"required": "owner", and the app reports “Only the account owner can do this.” - Creating a key, creating a webhook and Send test share one limit of 10 calls per minute per signed-in person. Beyond that the API answers
429and the app shows “Too many attempts. Please wait a moment.” - If the account is not active, calls from this page return
403with a code of the formtenant_<status>; themessagefield reads “Das Konto ist derzeit nicht aktiv.” (the account is currently not active). The app shows “This account is locked. Please write to mail@xicbot.de.”
API keys
An API key starts with xbot_ followed by 48 random characters. “A key is shown in plain text exactly once. Only its fingerprint is stored.” Besides the SHA-256 fingerprint, the first 12 characters are kept as the Identifier. A lost key therefore cannot be recovered, only replaced.
Creating a key
-
Step 1
Enter a name
Under Name enter a label that lets you recognise the key later, for example the system that uses it. At most 80 characters.
-
Step 2
Set limit and expiry
Requests per minute caps how often this key may call per minute and is prefilled with 120. Expires in days sets an expiry date counted from creation; “Empty means no expiry.”
-
Step 3
Choose scopes
Under Scopes tick at least one scope. Without a name or a scope, Create a new key stays disabled.
-
Step 4
Copy the key
The Your new key card appears with the note “Copy it now. After closing it cannot be retrieved again.” Copy key puts it on the clipboard, after which Key copied is shown; if the clipboard cannot be reached, you see The clipboard is not available here. Select the value and copy it manually. Close removes it from view. Name, scopes and expiry are cleared; the limit keeps its value.
| Field | Required | Values | Default |
|---|---|---|---|
| Name | yes | 1 to 80 characters | – |
| Requests per minute | yes in the app, no in the API | 1 to 6000 | 120 |
| Expires in days | no | 1 to 3650 | empty: no expiry |
| Scopes | yes, at least one | see Scopes | – |
Scopes
A key can only call the routes whose scope it carries; “Writing is a separate scope. Without it a key only reads.” GET /api/v1/integration/status needs no scope. Which routes each scope opens and how the API rejects a missing scope is described in API authentication.
What each route returns is described in API integration.
The key list
The list shows the account's keys, newest first. As long as none exists, it reads “No key created yet.”
| Column | Content |
|---|---|
| Name | the label you entered |
| Identifier | the first 12 characters followed by …, for example xbot_Ab12Cd3… |
| Scopes | the scope labels, separated by commas |
| Last used | time of the last request, accurate to about a minute; never while the key has not been used |
| Expires | expiry time or no limit |
| Status | valid, expired or revoked |
| Action | Revoke, as long as the key has not been revoked |
Revoking a key
Revoke asks for confirmation: “Revoke this key? Requests using it will be rejected from then on.” Only a second Revoke takes effect; Cancel leaves the key unchanged. The key then stays in the list with the status revoked and cannot be made valid again; requests using it get 401 and invalid_key. Expired keys can be revoked as well.
Using a key
Send the key in the X-Api-Key header to routes under /api/v1/integration. A key in the URL, that is ?api_key= or ?key=, is rejected with 400 and key_in_url. Details on authentication are in API authentication, on limits in API limits.
Shell command bash
curl -s https://app.xicbot.de/api/v1/integration/status \
-H "X-Api-Key: $XICBOT_API_KEY"How the API checks a key step by step, and which response belongs to which step, is described in API authentication; the per-minute and daily caps are in API limits.
Webhooks
A webhook is a target address in your system to which XICBOT sends a signed POST request for the events you select. “Every delivery carries a signature in the X-XICBOT-Signature header. Failed deliveries are retried.” As long as none exists, the section reads “No webhook set up yet.”
Creating a webhook
-
Step 1
Enter the target address
Under Target address enter your receiver's address, for example
https://example.com/xicbot-webhook, at most 2048 characters.httpandhttpsare accepted; we recommendhttps. Which targets are rejected is listed under Permitted target addresses. -
Step 2
Choose events
Under Events tick at least one of the ten events. Without an address or an event, Create webhook stays disabled.
-
Step 3
Store the secret
The Your signing secret card appears with the sentence “Use it to verify the signature of an incoming delivery.” The secret starts with
whsec_followed by 48 characters. Copy secret puts it on the clipboard, Close removes it from view.
Events
Next to the label shown in the app is the technical name that arrives in the event field and the X-XICBOT-Event header. Which fields data carries depends on the event; identifiers arrive as strings.
| Event | Label | When | Fields in data |
|---|---|---|---|
conversation.started | Conversation started | The widget opens a new conversation. | conversation_id, assistant_id, channel |
conversation.ended | Conversation ended | A conversation is closed in the app, from the Edit role upwards. | conversation_id, assistant_id |
handoff.triggered | Handover triggered | A conversation is handed over to your team. | conversation_id, reason (masked as in the app, up to 300 characters), queue (from the matching rule, otherwise null) |
lead.created | Enquiry created | The assistant creates an enquiry in a conversation for the first time, or an enquiry arrives via POST /api/v1/integration/leads. | id, conversation_id (null via the API); via the API also source |
appointment.requested | Appointment requested | The assistant records an appointment request in a conversation for the first time. | id, conversation_id |
appointment.confirmed | Appointment confirmed | An appointment is set to confirmed in the app, from the Edit role upwards. | id, starts_at |
knowledge.indexed | Knowledge source indexed | A source processed in the background, such as a website, has been indexed or refreshed, or a source from POST /api/v1/integration/knowledge/sources is ready. | source_id, documents, chunks; via the API also reason set to null |
knowledge.failed | Knowledge source failed | The same cases when indexing fails. | source_id, reason; via the API also documents, chunks |
gap.detected | Knowledge gap detected | A completed answer from the assistant names no source from your knowledge, even if the search found something: it declines (refused) or answers without a source (uncited). An answer for which the assistant called a tool does not count. The question has at least 12 characters and the account already holds knowledge. | conversation_id, assistant_id, question (masked as in analytics, up to 300 characters), kind (refused or uncited) |
budget.exhausted | Budget exhausted | A charge lifts the current month's usage to or above the account's monthly quota. The transition is reported, not the state; without a quota set, the event does not occur. | month, budget, used |
Capturing the same enquiry or appointment request again does not trigger a second event. Visitor text in question and reason goes out masked: email addresses, phone numbers, account numbers and street addresses appear as placeholders such as [E-Mail]; masking happens before shortening. The plain text of a conversation is only available through a key with the conversations scope, which only the owner can create. There is also the probe event; it only comes from Send test, cannot be subscribed to and carries a note field in data. For conversations, handovers and knowledge see Conversations, Appointments and cases and Knowledge and answers.
Test and deliveries
Each webhook card shows the target address, the subscribed events and the state valid or disabled, followed by Consecutive failures with count and limit, for example 0 / 20.
- Send test immediately delivers a request with the
probeevent, without retries and even to a disabled webhook. Below it, Test delivered appears with your receiver's status code, or Test failed with the text from the delivery reasons, otherwise with the status code or the connection error message. The page then shows this webhook's deliveries. A test counts towards Consecutive failures like any other delivery. - Deliveries expands the webhook's last 50 deliveries, newest first, with the columns Time, Events, Attempt, Status and Duration. On success, Status shows the status code; on a failure it shows the text from the delivery reasons, otherwise the status code or the connection error message. Duration is given in milliseconds. With no entries it reads “No delivery yet.”
- For each delivery a SHA-256 hash of the body sent is stored, not the content. Retries of one event appear as separate rows with an increasing Attempt.
Disabling, enabling, deleting
The card's third button toggles the webhook: it reads Turn off for a running webhook and Turn on for a disabled one. A disabled webhook receives no events, and retries already scheduled are dropped. Enabling it resets Consecutive failures to 0.
Delete asks first: Delete this webhook? Deliveries to this address stop immediately, and the signing secret cannot be restored. The target address appears below it, with the buttons Cancel and Delete. Only the confirmation removes the webhook together with its delivery log; pending deliveries are dropped. The address and the secret cannot be changed. The subscribed events can be changed via PUT /api/v1/webhooks/{id}, not in the app.
Delivery and signature
Structure of a delivery
Each delivery is a POST with a JSON body of four fields: event, at (time of the event in ISO 8601, the same for every attempt), tenant (your account's identifier) and data. Non-ASCII characters and slashes are not escaped.
Source code json
{
"event": "lead.created",
"at": "2026-09-28T10:15:00+00:00",
"tenant": "<account-id>",
"data": {
"id": "<enquiry-id>",
"conversation_id": "<conversation-id>"
}
}| Header | Content |
|---|---|
Content-Type | application/json |
X-XICBOT-Event | technical name of the event |
X-XICBOT-Delivery | one UUID per event and webhook, the same for every attempt |
X-XICBOT-Attempt | attempt number, starting at 1 |
X-XICBOT-Signature | t=<Unix time>,v1=<HMAC-SHA256 as hex> |
User-Agent | XICBOT-Webhook/1 |
Verifying the signature
The signature is an HMAC-SHA256 over the timestamp t, a dot and the unmodified request body. The key is the entire secret including whsec_. Verify the raw body as it arrives, not JSON re-encoded from it: a different field order or escaped characters produce a different signature.
- Split the
X-XICBOT-Signatureheader at the commas and readtandv1. - Compute HMAC-SHA256 with the secret over
t, a dot and the raw body, and encode it as hex. - Compare the result with
v1in constant time, not with a plain equality check. - Limit the age of the delivery via
t, for example to five minutes, so a captured delivery cannot be replayed later. - Respond within 8 seconds with a status from 200 to 299 and do heavy work afterwards.
verify-signature.mjs javascript
import { createHmac, timingSafeEqual } from 'node:crypto';
// Checks X-XICBOT-Signature against the raw body (Buffer or string).
export function verifySignature(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
String(header ?? '').split(',').map((part) => part.split('=', 2)),
);
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) {
return false; // missing or too old
}
const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');
const given = Buffer.from(String(parts.v1 ?? ''), 'hex');
const wanted = Buffer.from(expected, 'hex');
return given.length === wanted.length && timingSafeEqual(given, wanted);
}webhook.php php
<?php
// Read the raw body, not $_POST and not re-encoded JSON
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_XICBOT_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);
$t = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $t . '.' . $body, getenv('XICBOT_WEBHOOK_SECRET'));
$fresh = abs(time() - $t) <= 300;
if (!$fresh || !hash_equals($expected, (string) ($parts['v1'] ?? ''))) {
http_response_code(400);
exit;
}
http_response_code(204);
$event = json_decode($body, true);As a cross-check, the SHA-256 hash of the raw body matches the delivery's content_hash field in GET /api/v1/webhooks/{id}/deliveries.
Retries
Events are delivered in the background. A response with status 200 to 299 within 8 seconds counts as success. Redirects with status 300 to 399 are not followed and count as failures. After a failure, a second attempt follows after about 30 seconds and a third after about 120 seconds; if that fails too, the event is not delivered any further. Background processing is described in Background jobs.
- Every attempt is signed afresh:
tchanges andX-XICBOT-Attemptgoes up.atandX-XICBOT-Deliverystay the same for every attempt of an event, so you can recognise a retry byX-XICBOT-Delivery. A test gets a new identifier each time it is sent. - If the webhook is disabled or deleted by the time of an attempt, or the plan no longer includes the feature, the delivery is dropped without further attempts and without an entry in the delivery log.
- If the target address check only fails at sending time, for example because the host name now points to an internal address, the attempt is logged, retried and counted towards Consecutive failures just like a receiver that does not respond. If the signing secret cannot be read, the attempt is logged and retried but does not count towards Consecutive failures.
Permitted target addresses
The address is checked on creation and before every send. http and https are accepted. The host name has to resolve to a public address; private and reserved ranges such as 10.0.0.0/8 or 192.168.0.0/16 are rejected. If the host name points to several addresses, each of them has to be public; the connection is made only to the checked addresses. On creation the app reports “This address is not permitted as a target.”, and the API answers 422 with one of the reasons url_unreadable, scheme_not_allowed, name_unresolvable or target_not_allowed in the error field.
Reasons for failed deliveries
These codes appear in the reason field of a test and in the error field of the delivery log. The app shows the following texts for them:
| Code | Text shown in the app |
|---|---|
redirect_not_followed | “The target redirected. Redirects are not followed – please enter the final address.” |
secret_unreadable | “The signing secret could not be read.” |
url_unreadable | “The address cannot be read.” |
scheme_not_allowed | “This address type is not supported.” |
name_unresolvable | “The host name of the address cannot be resolved.” |
target_not_allowed | “This target is not allowed, for example an internal address.” |
http_<status> | your receiver's status code, for example 500 |
| other text | the connection error message, cut to 240 characters, for example on a timeout |
Managing via the API
The same operations are available under /api/v1. Authenticate with the session token of a person holding at least the Administration role, not with an API key: an API key manages neither keys nor webhooks. How to obtain a session token is explained in API authentication. Errors come back as JSON, usually with a code in the error field; field errors are listed in the errors field.
Key endpoints
GET /api/v1/keys
Returns the account's keys, newest first, in keys, plus the allowed scopes in scopes and the maximum number of valid keys in limit_active.
- Authentication
- Session token, role Administration or Owner
POST /api/v1/keys
Creates a key. Fields: name (required, up to 80 characters), scopes (required, at least one scope), limit_per_min (1 to 6000, 120 if omitted), expires_in_days (1 to 3650, no expiry if omitted). Response 201 with key and the plain text in plaintext_once.
- Authentication
- Session token, role Administration or Owner; with the
conversationsscope Owner only - Rate limit
- 10 calls per minute per person, shared with the other create and test calls
DELETE /api/v1/keys/{id}
Revokes the key. It is kept, with the time in withdrawn, and cannot be made valid again; a second call changes nothing. Response 200 with key.
- Authentication
- Session token, role Administration or Owner
Shell command bash
curl -s https://app.xicbot.de/api/v1/keys \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "ERP sync", "scopes": ["leads", "leads.write"], "limit_per_min": 60, "expires_in_days": 365}'| Field in the key object | Content |
|---|---|
id | key identifier |
name | label |
prefix | the first 12 characters |
scopes | list of scopes |
limit_per_min | requests per minute |
last_used_at | last use in ISO 8601, otherwise null |
expires_at | expiry in ISO 8601, otherwise null |
withdrawn | time of revocation, otherwise null |
valid | true while neither revoked nor expired |
Webhook endpoints
GET /api/v1/webhooks
Returns the account's webhooks, newest first, in webhooks, plus the subscribable events in events and the disabling threshold in failure_limit.
- Authentication
- Session token, role Administration or Owner
POST /api/v1/webhooks
Creates a webhook. Fields: url (required, up to 2048 characters, a valid URL) and events (required, at least one event). Response 201 with webhook and the secret in secret_once. A target that is not permitted yields 422 with the reason in error.
- Authentication
- Session token, role Administration or Owner
- Rate limit
- 10 calls per minute per person, shared with the other create and test calls
PUT /api/v1/webhooks/{id}
Changes events (at least one event) and is_active. With is_active: true the failure counter restarts at 0 and disabled_at is cleared. Address and secret cannot be changed here.
- Authentication
- Session token, role Administration or Owner
DELETE /api/v1/webhooks/{id}
Deletes the webhook along with its delivery log. Response {"deleted": true}.
- Authentication
- Session token, role Administration or Owner
POST /api/v1/webhooks/{id}/test
Sends a test with the probe event immediately, even to a disabled webhook and without retries. Response test with delivered and reason; status and duration_ms are added when a request was actually sent.
- Authentication
- Session token, role Administration or Owner
- Rate limit
- 10 calls per minute per person, shared with the other create and test calls
GET /api/v1/webhooks/{id}/deliveries
Returns the last 50 deliveries, newest first, with event, content_hash (SHA-256 of the body sent), attempt, status, duration_ms, error and at.
- Authentication
- Session token, role Administration or Owner
| Field in the webhook object | Content |
|---|---|
id | webhook identifier |
url | target address |
events | subscribed events |
is_active | true while enabled |
failures_in_row | consecutive failures |
disabled_at | time of automatic disabling, otherwise null |
secret_hash | the first 12 characters of the secret's SHA-256 hash |
Error responses
| Status | Code | Meaning |
|---|---|---|
402 | plan_feature_missing | The feature is not part of the plan; also feature, plan, plan_name and available_in. Applies to creating, changing and testing, not to the list, the delivery log, revoking and deleting. |
403 | role_too_low | Role too low; required names admin, or owner for the conversations scope. |
403 | no_tenant, tenant_<status> | No account assigned, or the account is not active. |
404 | not_found | The key or webhook does not exist in this account, for example because the webhook has been deleted in the meantime. The app shows “This entry no longer exists. Reload the page to see the current state.” |
422 | too_many_keys | 10 valid keys reached; limit names the limit. |
422 | url_unreadable, scheme_not_allowed, name_unresolvable, target_not_allowed | The target address is not permitted. |
422 | field error | A required field is missing or a value is out of range; the details are in the errors field. The app shows “That did not work. Please check your entries.” |
429 | – | More than 10 create or test calls per minute. |
Retention and deletion
- The delivery log is deleted after the log retention period that applies to your account; if it is set to 0, this deletion does not take place. Deleting a webhook deletes its log too.
- Revoked and expired keys remain in the list; individual keys cannot be deleted.
- The account's data export contains keys, webhooks and the delivery log, but without key hashes and without signing secrets.
- When the account is deleted, keys, webhooks and the delivery log are deleted.
Retention periods and the export are covered in Retention and deletion, the protection of credentials in Data security.
Frequent questions
I did not copy the key. Can it be shown again?
No. Only its fingerprint is stored. Create a new key, switch your system over and revoke the old one with Revoke.
How do I change a webhook's target address?
The address cannot be changed. Create a webhook with the new address, store the new secret in your receiver and then delete the old webhook. A webhook's events can be changed via PUT /api/v1/webhooks/{id}.
Why does my webhook show disabled?
Either it was switched off by hand, or 20 attempts in a row failed; in that case disabled_at is set. Check the reason under Deliveries, fix it, test with Send test and re-enable it with Turn on.
Why is the Read conversations checkbox disabled?
This scope returns messages in plain text. Only the Owner role can create a key with it; with the Administration role, ask the owner to do so.
Can an event arrive more than once?
Yes, if your receiver processes the delivery but does not answer with 200 to 299 within 8 seconds: another attempt follows. All attempts carry the same identifier in X-XICBOT-Delivery; use it and process the same event only once.
Ask it yourself, Assistant of this website
Automated assistant