Your content, your AI,
anywhere you need it.
The AskCybex API is OpenAI-compatible. Anything already built for that format works by changing a base URL and a key — no SDK, no rewrite. Connect a CMS, a custom web app, an internal tool, an ERP or a mobile app to a chatbot trained on your own documents.
1 Get your API key
An API key maps to exactly one deployment — one chatbot trained on one set of documents. The key alone decides which documents an answer can come from, so you never pass a deployment id separately.
- Create a deployment and train it In your dashboard go to Document Training, create a deployment, upload your files (PDF, DOCX, TXT, CSV) and click Train. Wait for the status to reach Completed.
- Open the API key page Go to API key in the sidebar, or click here if you're signed in.
- Generate a key for that deployment Each deployment gets its own key. Copy it immediately — it is stored hashed, so it cannot be shown again. If you lose it, delete the key and generate a new one.
- Keep it server-side where you can A key spends your token balance. Our chat widget uses it in the browser by necessity — if you do the same, restrict the deployment by IP in Settings → IP Restriction and watch usage under Reports.
2 Authentication
Send your key whichever way suits the client. All three are equivalent:
Authorization: Bearer YOUR_API_KEY # preferred, used by /api/v1/*
X-API-KEY: YOUR_API_KEY # header alternative
?api_key=YOUR_API_KEY # query/form field — required for SSE,
# because EventSource cannot set headers
Keys are stored as SHA-256 hashes. An invalid or disabled key, or a deployment
that is not published, returns 401.
3 Quickstart
Ask your first question in one request. Pick a language:
curl https://askcybex.com/api/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_DEPLOYMENT_ID",
"messages": [
{ "role": "user", "content": "What is your refund policy?" }
]
}'
// Works with the official OpenAI SDK — only the baseURL changes.
import OpenAI from "openai";
const client = new OpenAI({
apiKey: "YOUR_API_KEY",
baseURL: "https://askcybex.com/api/v1",
});
const res = await client.chat.completions.create({
model: "YOUR_DEPLOYMENT_ID",
messages: [{ role: "user", content: "What is your refund policy?" }],
});
console.log(res.choices[0].message.content);
# pip install openai
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://askcybex.com/api/v1",
)
res = client.chat.completions.create(
model="YOUR_DEPLOYMENT_ID",
messages=[{"role": "user", "content": "What is your refund policy?"}],
)
print(res.choices[0].message.content)
$ch = curl_init('https://askcybex.com/api/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'messages' => [
['role' => 'user', 'content' => 'What is your refund policy?'],
],
'conversation_id' => 'visitor-123',
]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $data['choices'][0]['message']['content']
?? $data['error']['message'];
Multi-turn conversations
Follow-up questions need the earlier turns. Pick one of two styles — don't mix them.
id starts with chatcmpl- and identifies that single completion.
conversation_id starts with conv- and identifies the thread.
Only conversation_id continues a conversation — passing the
chatcmpl- id starts a brand new, empty thread, and the bot will appear to have
forgotten everything.
Style A — resend the messages (OpenAI-native). Stateless, and what the OpenAI SDKs and Open WebUI do automatically:
{
"model": "98",
"messages": [
{ "role": "user", "content": "What is Hogwarts?" },
{ "role": "assistant", "content": "Hogwarts is a school of witchcraft and wizardry..." },
{ "role": "user", "content": "Who are the students there?" }
]
}
Style B — reuse the conversation id. Send only the new question; we rebuild the history server-side:
# Turn 1 — no id sent, so one is minted and returned
curl -X POST https://askcybex.com/api/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"98","messages":[{"role":"user","content":"What is Hogwarts?"}]}'
# response contains BOTH ids — copy the conv- one:
# "id": "chatcmpl-f616e6134ae341316ba712a9" <- NOT this
# "conversation_id": "conv-dc4d05c139d35b910ff4be69" <- this one
# Turn 2 — reuse conversation_id, send only the new question
curl -X POST https://askcybex.com/api/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"98",
"messages":[{"role":"user","content":"Who are the students there?"}],
"conversation_id":"conv-dc4d05c139d35b910ff4be69"}'
If you send both, the messages history wins, so a turn is
never counted twice.
search_query so you can see exactly
what was searched.
This used to need a separate
/chat/completions-contextual endpoint, while the
default one searched for whatever the visitor literally typed. They are the same thing now,
streamed or not.
4 Endpoint reference
One endpoint answers questions. The rest are the things a chat widget needs around a conversation, and one for teaching a chatbot something new. Every one of them takes the same bearer key, and the key is what selects the chatbot — there is no deployment id to pass.
Chat
/api/v1/chat/completions
60 / minAnswers the last user message from your documents, in the standard OpenAI ChatCompletion
shape. Earlier messages are used as history, and the document search is resolved against
them. Set "stream": true for Server-Sent Events.
Beyond the OpenAI fields, the response carries sources (the documents behind
the answer), search_query, conversation_id and
chat_log_id. A stock OpenAI client ignores all four.
messagesthe conversation; the last user turn is the questionmessagea plain string, instead of messagesconversation_idthread id; the server rebuilds history for itstreamtoken-by-token SSE instead of one JSON replyinclude_chunksreturn the retrieved passages — tens of KB per turnvisitor_emaila captured lead, stored against this turn/api/v1/chat/completions
SSE60 / minThe same endpoint for browsers. EventSource cannot set an
Authorization header, so the key travels in the query string.
const es = new EventSource(
"https://askcybex.com/api/v1/chat/completions?stream=1" +
"&api_key=" + YOUR_API_KEY +
"&message=" + encodeURIComponent(question) +
"&conversation_id=" + conversationId
);
es.onmessage = (e) => {
if (e.data === "[DONE]") return;
const chunk = JSON.parse(e.data);
const choice = chunk.choices[0];
if (choice.delta.content) output.textContent += choice.delta.content;
if (choice.finish_reason) {
es.close();
rate(chunk.chat_log_id); // /widget/feedback
remember(chunk.conversation_id);
}
};
/api/v1/models
120 / minReturns the one chatbot your key maps to, as an OpenAI model object. Clients such as Open WebUI call this to fill their model picker; it also makes a good connection test.
Widget
/api/v1/widget/config
120 / minEverything a widget needs before its first question: the chatbot's name, the starter questions its owner wrote, any pre-generated Q&A pairs, and whether to offer a human when the bot cannot help.
/api/v1/widget/feedback
120 / minRecord whether an answer helped. Send the chat_log_id from the answer and
feedback (1 = helpful, 0 = not). Thumbs-down answers
appear on the owner's Corrections screen, where they can write a replacement that is then
served instead — free, and even when the account is out of credit.
/api/v1/widget/handoff
10 / min“The bot could not help — have a person email me.” Offer it
after a refusal and not before, so it never competes with an answer the chatbot
could have given. The enquiry is recorded as a lead and the owner is notified. Available
only when handoff_enabled comes back true from the widget config.
Documents
/api/v1/documents
20 / minTeach the chatbot something. Send a content string with a name,
or a multipart file — pdf, csv, txt or docx. This is how the WooCommerce
plugin keeps a catalogue in sync, and how you would push anything your own system
generates.
Training runs by default. Send "train": false when
pushing several documents and train on the last one. replace defaults to true,
because a catalogue push is the whole catalogue — appending would leave last week's
discontinued products answering questions for ever.
200stored and trained — the chatbot knows this now202stored, training queued — it does not know it yet201stored; training was not requested5 Use it inside tools you already run
Because the API speaks the OpenAI format, most AI tooling connects without any code. For Open WebUI:
- Settings → Connections → OpenAI APIAdd a new connection.
- Base URL
https://askcybex.com/api/v1 - API keyYour deployment key.
- Save, then pick your deployment from the model listIt appears by deployment id.
The same pattern works for LangChain, LlamaIndex, agent frameworks, ERP and CRM add-ons, and any internal tool that lets you set an OpenAI base URL.
6 Rate limits, errors & billing
| Endpoint | Limit |
|---|---|
/chat/completions | 60 requests / minute |
/models, /widget/config, /widget/feedback | 120 requests / minute |
/documents | 20 requests / minute |
/widget/handoff | 10 requests / minute — it sends mail |
Every response carries X-RateLimit-Limit and
X-RateLimit-Remaining. Exceeding a limit returns 429.
| Status | Meaning | What to do |
|---|---|---|
401 | Invalid or disabled key, or the deployment isn't published | Check the key; publish the deployment |
402 | Token balance exhausted | Top up your balance |
404 | Deployment not found | Confirm the key belongs to a trained deployment |
429 | Rate limited | Back off and retry |
{"error": {"message", "type", "code"}} with a real HTTP status. Read
error.message to display and error.code to branch on — the
code is stable, the wording is not.
The message never reveals why in a way that embarrasses the site owner: an account out of credit tells the visitor the assistant is temporarily unavailable, not that somebody forgot to top up.
error.code tells you the truth: no_tokens,
account_disabled, ip_restricted, bad_key,
not_trained, backend_down, bad_request.
Older endpoints answered
HTTP 200 with a bare error string. They are
gone; if you have code checking for that shape, it needs updating.
Billing. Answers consume tokens from your account balance. When a question cannot be grounded in your documents, the refusal is returned before the language model is called — so a “Sorry, I don’t know” costs you nothing.
Try every endpoint in the browser
The explorer is loaded with the live spec. Click Authorize, paste your key, and send real requests against your own deployment.