Sign in with an admin account to manage the chatbot configuration.
Admin Console
{{KNOWLEDGE_BASE}} — that is where the knowledge base is substituted in at request time.
| Questions per chat | Chats | Price per chat | Total |
|---|
The chatbot answers customer messages from Freshchat. You send us the conversation, we send back the reply.
The exchange is asynchronous. You send us a question and we acknowledge it immediately; the answer arrives a few seconds later as a separate call to your webhook. We do not hold the connection open while the model is working.
Send the full conversation every time, oldest message first. The last message is the one to answer; everything before it is context the bot needs for follow-up questions like “and how much is that?”.
POST https://silknet.containai.io/api/v1/freshchat/chat
X-API-Key: <your inbound key>
Content-Type: application/json
{
"conversation_id": "a15c3f78-959e-4d42-bd43-bd3b059300fa",
"contents": [
{
"parts": [
{ "text": "user said: გამარჯობა" },
{ "text": "agent said: გამარჯობა! რით შემიძლია დაგეხმაროთ?" },
{ "text": "user said: რა ღირს ულიმიტო ინტერნეტი?" }
]
}
]
}
| Field | Required | Notes |
|---|---|---|
conversation_id | yes | The Freshchat conversation id. Any non-empty string. |
contents[0].parts[] | yes | The conversation so far, oldest first. One entry per message. |
category | no | The category we sent you earlier in this chat, echoed back. |
subcategory | no | The subcategory we sent you earlier, echoed back. |
Prefix each message with user said: or agent said: so
we know who is speaking. A message with neither prefix is treated as coming from
the customer.
202 Accepted
{ "status": "accepted", "conversation_id": "a15c3f78-..." }
202 means “received, we will call you back”. It is not the answer.
| Status | Meaning |
|---|---|
202 | Accepted — the answer will arrive at your webhook |
400 | Malformed body: missing conversation_id, empty contents, or no usable messages |
401 | Missing or wrong X-API-Key |
500 | Server misconfiguration on our side |
We POST to the webhook URL you gave us:
POST <your webhook>
apikey: <your outbound key>
Content-Type: application/json
{
"conversation_id": "a15c3f78-959e-4d42-bd43-bd3b059300fa",
"answer": "ულიმიტო ინტერნეტი 30 დღით ღირს 35 ლარი.",
"answer_number": 3,
"command": null
}
| Field | Present | Meaning |
|---|---|---|
conversation_id | always | The id you sent us |
answer | always | The text to show the customer |
answer_number | always | Which question this answers — 1, 2, 3… |
command | always | null, "transfer_to_operator" or "end_chat" |
category | sometimes | Ticket category of the inquiry |
subcategory | sometimes | Ticket subcategory of the inquiry |
answer_number counts questions, not messages. The
third message in a chat is usually the second question.
command is on every answer and tells you what to do with the chat.
| Value | Meaning | What to do |
|---|---|---|
null | An ordinary answer | Post answer to the customer |
"transfer_to_operator" | The chat needs a person — the bot could not answer, the customer asked for one, the chat has passed 20 questions, or the request failed outright on our side | Post answer, then route the chat to an operator |
"end_chat" | Two causes, one command: the customer keeps writing about things unrelated to Silknet after being told once that we only cover Silknet topics, or sends a second offensive message after being asked once to stop. The second has nothing to do with the topic, so a chat can close on someone who never went off-topic. | Post answer, then close the conversation. Nobody is waiting for an operator. |
Route on command, never on the text of answer.
The wording changes with the customer’s language and with the reason —
someone who asks for an operator is not told the bot could not find an
answer. command is a fixed value and does not vary.
Some answers carry two extra fields classifying the inquiry. The values come
from the Ticket: labels in the knowledge base and are sent
verbatim, so they can be written straight into a ticket.
"category": "მობილური/საინფორმაციო ტარიფები/პირობები/აქციები",
"subcategory": "eSIM მომსახურება"
null when the message has no topic — most often a bare greeting.
category = $json.category applied on every answer is always
correct. There is no accumulation rule to implement, and no answer can
clear a category you already hold — including one an operator set by hand, which
we echo untouched and never check against our own list.
Still send it back to us on later requests: that tells us the inquiry is already classified so we can stop. If you never send it back, nothing breaks — we simply try on each of the first three questions.
A chat closed with end_chat carries
ბოტი/საუბარი არ შედგა with subcategory
სხვაგან მოხვდა/არამიზნობრივი მომართვა — but only when the
conversation never had a real category. A customer who asked a genuine question
and then went off-topic, or turned abusive, keeps the category their
question produced.
Both closes file that same label — the off-topic one and the abuse one. Silknet chose it in their own document, which is why an abuse close lands under a subcategory that reads “landed elsewhere / non-targeted inquiry”. Counting that subcategory therefore counts off-topic closes and abuse closes together, and nothing in the payload separates them.
The bot replies in the language the customer writes in — Georgian, Russian or English. Nothing in this API changes: the same fields, the same shapes.
Detection reads the customer’s own messages and needs nothing from you. Georgian typed in Latin letters (“gamarjoba, ra girs paketi?”) is treated as Georgian, not English. Georgian is used whenever the evidence is unclear.
Two things stay Georgian in every language, deliberately:
category and subcategory — labels for your ticketing system, not text for the customer.Prices, currency and USSD codes are reproduced character for character in every language.
Respond as soon as you have received the request — not when your workflow has finished posting to Freshchat. A reply taking longer than 10 seconds counts as a failed delivery and we retry, up to 3 attempts, waiting 1s, then 4s, then 16s.
In n8n this is the Webhook node’s Respond setting: choose Immediately, not When Last Node Finishes.
conversation_id + answer_number uniquely identifies one
answer and is identical on every retry of it. Use the pair to drop repeats:
Remove Duplicates node
Keep Items Where: Value Is New
Value to Dedupe On: {{ $json.conversation_id }}-{{ $json.answer_number }}
Both fields must be in the key. Deduplicating on
conversation_id alone suppresses every answer after the first —
the customer gets one reply and then silence.
{
"conversation_id": "ex-A",
"answer": "eSIM-ის საფასური მიმდინარე აქციით 5 ₾-ია...",
"answer_number": 1,
"command": null,
"category": "მობილური/საინფორმაციო ტარიფები/პირობები/აქციები",
"subcategory": "eSIM მომსახურება"
}
{
"conversation_id": "ex-B",
"answer": "გამარჯობა! მე სილქნეტის ციფრული ასისტენტი ვარ. ამჟამად სატესტო რეჟიმში ვმუშაობ და მზად ვარ, დაგეხმაროთ.",
"answer_number": 1,
"command": null,
"category": null,
"subcategory": null
}
A greeting has no topic, so both are null. The keys are still present.
{
"conversation_id": "ex-C",
"answer": "ულიმიტო ინტერნეტ პაკეტის გასააქტიურებლად აკრიფეთ *019#...",
"answer_number": 2,
"command": null,
"category": "მობილური/საინფორმაციო ტარიფები/პირობები/აქციები",
"subcategory": "eSIM მომსახურება"
}
answer_number is 2 — the second question, not the third message. We did not reclassify here: the category is the one from answer 1, sent back unchanged.
{
"conversation_id": "ex-D",
"answer": "გასაგებია. გადაგამისამართებთ ოპერატორთან, გთხოვთ დაელოდოთ.",
"answer_number": 1,
"command": "transfer_to_operator",
"category": null,
"subcategory": null
}
{
"conversation_id": "ex-E",
"answer": "აღნიშნულ საკითხზე დახმარებას ვერ გაგიწევთ. ვინაიდან თქვენი მოთხოვნა სილქნეტის სერვისებს არ უკავშირდება.",
"answer_number": 3,
"command": "end_chat",
"category": "ბოტი/საუბარი არ შედგა",
"subcategory": "სხვაგან მოხვდა/არამიზნობრივი მომართვა"
}
If something breaks after we have already acknowledged, we still call you rather than leaving the customer in silence. The payload is the same shape as a normal transfer, so there is nothing different to do:
{
"conversation_id": "ex-F",
"answer": "ამ საკითხის დასაზუსტებლად ოპერატორთან დაგაკავშირებთ.",
"answer_number": 1,
"command": "transfer_to_operator",
"category": null,
"subcategory": null
}
One other message is worth recognising: if the bot cannot produce a reply at
all, answer is
ბოდიში, დაფიქსირდა ტექნიკური ხარვეზი, სცადეთ ხელახლა and the
customer is asked to try again.
That message carries command: null.
The two failure paths differ. A request that fails outright — a
configuration problem, an unexpected error — is escalated for you:
the answer is the operator sentence and command is
"transfer_to_operator". A request where the model could
not produce a usable reply returns the sentence above with no
command, because nothing asked for a person.
This is the one case where command alone does not tell
you what to do, which is why the sentence is fixed and safe to match
on. Whether it should escalate instead is a policy choice.
Two separate keys, not interchangeable:
| Key | Direction | Sent as |
|---|---|---|
| Inbound | you → chatbot | X-API-Key header |
| Outbound | chatbot → you | apikey header |