Developer guide
Widget embedding, configuration, the questionnaire schema and the partner REST API — everything you need for integration.
Overview
Base URL: https://api.chkuiskolopi.ge/api
There are two kinds of access:
- Widget endpoints — no authorization needed; a
public_key(in body/query) is enough to identify the bot. The widget uses these automatically. - Partner API — for programmatic bot management; requires the header
Authorization: Bearer <access_token>(you obtain the token via authorization). An expired/invalid token →401.
Most bots are managed without code, from the portal — you need the API only if you want your own automation.
Quick start
- Create a bot in the portal and take its
public_key(visible in the embed-code modal). - Paste one line at the end of your site's
<body>:
<script src="https://api.chkuiskolopi.ge/api/scrap/widget.js?key=PUBLIC_KEY"></script>
- Optionally test the bot directly over the API:
curl -X POST -H "Content-Type: application/json" \
-d '{"partner_key":"PUBLIC_KEY","query":"გამარჯობა","session_id":"test-1","deviceInfo":{"device_name":"curl","ip_address":""}}' \
https://api.chkuiskolopi.ge/api/scrap/widget/query/
Embedding the widget
The script creates the floating button (bottom-right corner) and the chat panel by itself. No server-side setup, CSS or extra code is needed.
Session
- The session is stored in the visitor's
localStorageunder the keyck_session_<first 8 chars of the key>. - Lifetime — 1 hour (refreshed on activity). After expiry a new session starts.
- Conversation history follows the session — within one session the bot remembers the previous exchanges.
CORS
The widget endpoints (/api/scrap/widget/…) accept any Origin — the key travels in the body and no cookies are used. Partners do not need to configure CORS.
The domain list (allowed domains) — the security gate
- The
public_keyis visible in the page HTML — by design (it is a public identifier). Abuse is stopped by the domain list. - An empty list = the widget works on every site.
- A non-empty list → on each request the server checks
Origin/Referer: the host must match exactly or be a subdomain. Failure →403 {"error":"domain_not_allowed"}. - Entries are normalised on save:
https://Example.com/x→example.com; a comma-separated list is accepted too. - The key of a deactivated bot (
is_active=false) →401 Invalid partner_key.
The portal's test window: a domain-locked bot returns 403 domain_not_allowed in the portal preview (the preview runs on a different domain) — that is the protection working, not a bug. Remove the domain temporarily for testing.
Widget configuration (config)
A bot's config is a JSON object. It is edited from the portal, by asking Ostati, or via PATCH. Changes are merged into the existing object — send only the key you want to change, the rest stays untouched. Before every change the previous state is saved automatically (restorable).
Greeting — greeting
- The text the widget shows as the bot's bubble on the panel's first open.
- It appears locally — nothing is sent to the server and it is never billed.
- If the bot also has a questionnaire: first the greeting, then the tree starts.
The Enter key — enter_sends
- Default: Enter = new line; the message is sent only with the "Send" button.
"enter_sends": true→ Enter sends, Shift+Enter = new line. Applies on mobile too.
Behaviours — behaviors
An object of boolean switches; whatever is not listed is off:
| Key | What it enables |
|---|---|
typing | A "typing…" animation while waiting for the answer |
processing | On long (>100 chars) questions an interim bubble "processing the information ⏳" |
reset | A new session via the text "new chat"/"start over"/"/reset" or the ↺ button (fully client-side) |
debounce | Messages sent within ~2.5 s are merged into one question |
voice | The 🎤 button — speech to text (Georgian); the text lands in the field and the visitor sends it themselves |
file_upload | The 📎 button — uploading a document (PDF/DOCX/XLSX/CSV/TXT) or an image (JPG/PNG/WEBP); the content (OCR included) accompanies the next question |
Voice is paid: transcription is charged to the partner's balance at the moment of transcription. On a disabled behaviour the corresponding endpoint returns 403; balance < $1 → 402. While recording, 10 s of silence → auto-stop and transcription; pure silence is not transcribed and costs nothing; one recording is at most 15 minutes.
Look — theme
Colours and shape are passed to the widget as CSS variables. Without a theme — the default blue/white look. All fields are optional; an invalid value is simply ignored.
| Group | Keys |
|---|---|
| Colours (hex or rgb/rgba) | accent (main — button/header/Send/user bubble), on_accent, panel, bot_bg/bot_text, user_bg/user_text, field_bg/field_text/field_border/line, muted |
| Shape (px or %) | radius (panel), bubble_radius, field_radius, btn_radius, launcher_radius (50%=circle, 28px=pill, 12px=square) |
| Floating button | launcher_icon (an emoji) or launcher_logo (an image URL, via logo upload). If a logo is set — the icon is ignored |
| Branding | hide_branding: true → the "powered by chkuiskolopi.ge" link is hidden (white-label) |
Markdown rendering in answers
The widget automatically renders the bot's answers as Markdown — for every bot, no config needed: bold/italic, headings (#–######), bullet and numbered lists, quotes (>), inline code and code blocks, links [text](https://…) (new tab; http/https/mailto), paragraphs.
Security: the text is HTML-escaped first and formatted after — <script> and other HTML never execute.
Tip: write in the bot's instruction: "Format answers with Markdown: key terms in bold, options as a bullet list, sections with headings, long answers in paragraphs."
The questionnaire — the button-tree schema
Stored in config.questionnaire. The tree runs entirely in the widget (in the browser) — only one ordinary text question reaches the server, at the end of the questionnaire. Without a questionnaire the widget is a free-form chat.
Step schema
"step_key": {
"text": "the bot's message at this step",
"buttons": [
{ "label": "button caption", "value": "stored value", "next_step": "next_step_key" }
],
"show_stop_button": true,
"allow_custom_input": false
}
| Field | Meaning |
|---|---|
text | Shown in the bot's bubble when the step opens |
buttons[].label | Written on the button and added as the user's bubble when chosen |
buttons[].value | Stored among the answers |
buttons[].next_step | The key of the next step; "END" or empty → finish |
show_stop_button | true → a grey "Stop" button is added |
allow_custom_input | true → the text field is enabled; typed text becomes the answer and control moves to the first button's next_step (if there is no button — END) |
Reserved "_" keys
Keys starting with an underscore inside questionnaire are fields, not steps:
| Key | Meaning | Default |
|---|---|---|
_final_prompt | The closing instruction sent to the bot together with the answers at the end of the questionnaire | "Help the visitor based on these answers." |
_stop_prompt | Same, for a questionnaire abandoned via "Stop" | "The visitor stopped the questionnaire. Help based on the answers so far." |
Never start a step name with "_" — the engine will not treat it as a step.
Engine behaviour
- The root step: the key that no
next_steppoints to. The tree must have exactly one root. - The questionnaire starts on the panel's first open, once. If the bot also has a
greeting— first the greeting, then the tree. - On a step with buttons the text field is disabled (except with
allow_custom_input: true). - An unknown
next_step→ the tree ends early — watch out for broken links. - On finish the widget assembles one text (the list of questions and answers +
_final_prompt) and sends it as an ordinary question — the bot answers with its own instruction and knowledge files. - On "Stop": if there are answers — they are sent with
_stop_prompt; if not — free chat is enabled.
Full example (an imaginary café bot)
{
"questionnaire": {
"start": {
"text": "გამარჯობა! რით დაგეხმაროთ?",
"buttons": [
{ "label": "მენიუ მაინტერესებს", "value": "menu", "next_step": "menu_type" },
{ "label": "მაგიდის დაჯავშნა", "value": "booking", "next_step": "guests" }
],
"show_stop_button": true
},
"menu_type": {
"text": "რა გირჩევნიათ?",
"buttons": [
{ "label": "ყავა და დესერტი", "value": "coffee", "next_step": "END" },
{ "label": "საუზმე", "value": "breakfast", "next_step": "END" }
],
"show_stop_button": true
},
"guests": {
"text": "რამდენი სტუმარი იქნებით? (შეგიძლიათ აკრიფოთ)",
"buttons": [
{ "label": "2", "value": "2", "next_step": "END" },
{ "label": "4", "value": "4", "next_step": "END" }
],
"allow_custom_input": true,
"show_stop_button": true
}
}
}
The partner REST API
Authorization
Body: {email, password, deviceInfo:{device_name, ip_address}, source:"partner"} → email verification starts. An account registered with "source":"partner" receives the $5 gift on verification and is returned to the portal.
Body: {email, password} → {access_token, refresh_token}. Every partner endpoint requires the Authorization: Bearer <access_token> header; on expiry → 401, log in again.
→ {api_key} — the old shared key. POST /user/api-key/regenerate/ creates a new one and invalidates the old — this only breaks widgets embedded with the legacy key; per-agent public_keys are unaffected. For new integrations always use the bot's public_key.
Bots (agents)
The agent object: {id, name, public_key, system_prompt, enabled_tools, model_tier, is_active, allowed_domains, config, telegram_connected, telegram_bot_username, facebook_connected, facebook_page_name, …}
→ {"tools":[{id, label, description}]} — the catalogue of enableable tools: web search, memory, the virtual screen/browser, power-level switching and hand-offs to the experts. enabled_tools: [] = the full base set without hand-offs; a non-empty list = only the listed ones. Unknown IDs are silently ignored.
→ {"agents":[…]} — only your own bots.
Body: {name, system_prompt, model_tier, enabled_tools, allowed_domains?, config?} → 201 + agent.
model_tier:cheap|medium|strong(any other value → silentlycheap). This is the starting level for a new session — a level the visitor picks during the conversation wins until the session ends.allowed_domains: a list or comma-separated text; the host is extracted from URLs automatically.config: object only → any other type gives400 config_must_be_object.
curl -X POST -H "Authorization: Bearer $T" -H "Content-Type: application/json" \
-d '{"name":"Consultant","system_prompt":"You are...","enabled_tools":[],"model_tier":"cheap"}' \
https://api.chkuiskolopi.ge/api/scrap/partner/agents/create/
Partial update — only the sent fields change. config is merged into the existing object (send only the key you are changing); before every change the previous state is saved automatically. is_active: false → the widget stops. → 200 + agent · 404 if the bot is not found.
Body: {recipient: "email"} → the bot moves to the recipient's account. Irreversible. Errors: recipient_required, agent_not_found, recipient_not_found, recipient_not_unique, cannot_transfer_to_self.
Knowledge files
→ {files:[{id, file_name, char_count, created_at}], total_chars, max_total_chars: 30000, max_files: 5}
multipart, field file. Only the text is kept — the original does not stay. Types: PDF, DOCX, XLSX, TXT, CSV, MD, JSON, LOG. A scanned PDF yields no text → extraction_failed.
curl -X POST -H "Authorization: Bearer $T" -F "file=@catalog.pdf" \
https://api.chkuiskolopi.ge/api/scrap/partner/agents/7/files/upload/
→ {ok: true, total_chars}. Irreversible (the original is not stored).
Error codes: file_required · too_many_files (max 5) · file_too_large (max 5MB) · unsupported_type · extraction_failed · knowledge_limit_exceeded (30,000 characters total; the response includes used, file_chars, limit).
Logo upload
multipart, field file — PNG/JPG/WEBP/GIF, ≤ 1MB → {"url": "https://…"}. Put the returned URL into config.theme.launcher_logo. Errors: file_required, unsupported_type, file_too_large, upload_failed.
Channels — Telegram and Messenger
Body: {bot_token: "the @BotFather token"}. The server validates the token and wires the bot automatically — no other step needed. → {ok: true, telegram_connected: true, telegram_bot_username}. The token is never returned in responses. Errors: bot_token_required, invalid_bot_token, setwebhook_failed, telegram_unreachable, agent_not_found.
→ {ok: true, telegram_connected: false}.
Body: {page_token: "Page Access Token"} (from your verified Meta business app). → {ok: true, facebook_connected: true, facebook_page_name, webhook_url, verify_token} — put those two values into your Meta app's Webhooks section (Callback URL + Verify Token) and subscribe the page's messages, messaging_postbacks events. The page token is never returned in responses. Errors: page_token_required, invalid_page_token, facebook_unreachable, agent_not_found.
→ {ok: true, facebook_connected: false}.
Balance and top-up
Body: {} (Bearer) → {balance} (USD).
Body: {amount: ≥10} (USD) → a payment link: from Georgia — by card in GEL (at the National Bank rate), from abroad — in USD (PayPal). Look for payment_url or approval_url in the response and redirect the user. 400 — the amount is below 10 USD.
Widget endpoints (no authorization)
→ the widget JavaScript — already carrying that bot's questionnaire, greeting, behaviours and theme.
Body: {partner_key, query, session_id, deviceInfo:{device_name, ip_address}}
→ 200: {response, DocumentLinks, chat_id, …} — the main field is response (the bot's answer, Markdown text); the rest are auxiliary fields and may be empty.
Errors: 400 partner_key is required · 401 Invalid partner_key · 402 balance < $1 · 403 domain_not_allowed.
/scrap/widget/transcribe/ and /scrap/widget/upload/ are the widget's internal endpoints (the 🎤 and 📎 buttons use them automatically): they work only when the corresponding behaviour is enabled (403 when off), voice is paid, files ≤ 6MB — documents and images (images and scanned PDFs are read by OCR).
Error reference
| Code | When | What to do |
|---|---|---|
400 | Bad request — a field is missing or has the wrong type (config_must_be_object, Min amount 10 USD…) | Fix the body per the error in the response |
401 | Invalid/expired token, or Invalid partner_key (including the key of a deactivated bot) | Log in again, or check the key and the bot's active flag |
402 | Balance exhausted (the widget works on balance ≥ $1) | Top up |
403 | domain_not_allowed — the site is not on the domain list; or the endpoint of a disabled behaviour | Add the domain / enable the behaviour |
404 | Bot/file not found (or belongs to someone else) | Check the id |
413 | The file is too large | Shrink the file |
502 | An external channel (Telegram/Facebook) is temporarily unreachable | Retry in a little while |
Limits reference
| What | Limit |
|---|---|
| Knowledge files per bot | 5 files · each ≤ 5MB · total text ≤ 30,000 characters |
| Widget session | 1 hour of inactivity (refreshed on activity) |
| Widget 🎤 voice | one recording ≤ 15 min; auto-stop after 10 s of silence; pure silence is free |
| Telegram voice | ≤ 3 minutes — on longer ones the bot asks for a shorter message (cost 0) |
| Messenger audio | ≤ ~2MB |
| Widget 📎 files | ≤ 6MB (PDF/DOCX/XLSX/CSV/TXT/JPG/PNG/WEBP) |
| Logo | ≤ 1MB (PNG/JPG/WEBP/GIF) |
| Balance | the widget works at ≥ $1; minimum top-up $10; $5 gift on registration |
Best practices
- Order Markdown: tell the bot in the instruction to format answers with Markdown — the widget renders it beautifully.
- Set a greeting: it is free and gives the bot its first impression.
- Lock the domains: before going live, put your sites on the domain list — the key is public and this is the only gate.
- Write config partially: PATCH works by merge — send only the key you are changing and you can never wipe the questionnaire/theme by accident.
- Keep knowledge compact: the bot reads the whole knowledge basket on every answer — text without "water" gives better answers.
- Test in the portal: while editing a bot the page has a live preview — changes apply instantly, no reload.
Still have a question? The portal's Ostati answers technical questions too, or write to us: info@chkuiskolopi.ge