RoxanAI API Documentation
Three API surfaces sit under one domain: content moderation and async processing for external developers (key-authenticated), the chat widget API (origin-checked, no key), and knowledge base management for building and maintaining a chatbot's content from the tenant panel.
Every route below also answers at its unversioned form (drop /v1) for existing integrations —
chat widgets already embedded on customer sites keep working unchanged. New integrations should use
/api/v1/....
Authentication
The Moderation API and Webhook API are authenticated with one fixed header. The Chat Widget API doesn't use this mechanism at all — see its own section below.
X-API-KEY: {your_api_key}
| Case | Status | Body |
|---|---|---|
| Header not sent | 401 | {"{"} "error": "API key is required", "message": "..." {"}"} |
| Invalid key | 401 | {"{"} "error": "Unauthorized", "message": "..." {"}"} |
The Moderation and Webhook APIs draw down a shared request counter called RemainingRequests.
The Chat Widget API has its own separate quota (chatbot credits), covered in its own section — the two never combine.
Error format
errorId is included on some error responses and absent on others — don't rely on it being
there. Key your error handling on the error field and the HTTP status code instead.
{"{"}
"error": "Not Found",
"message": "...",
"errorId": "AB12CD34"
{"}"}
{"{"}
"error": "Validation Error",
"message": "One or more validation errors occurred.",
"errors": { "{ }" }
{"}"}
| Status | Cause |
|---|---|
| 400 | ArgumentException |
| 401 | UnauthorizedAccessException / invalid API key |
| 403 | ForbiddenException (includes quota exhausted) |
| 404 | NotFoundException / KeyNotFoundException |
| 409 | InvalidOperationException |
| 422 | ValidationException |
| 500 | Any other unhandled error |
Moderation API
Checks a piece of text for policy violations.
🔑 Requires X-API-KEYSubmits a text for policy review and returns the result immediately (synchronous).
| Field | Type | Description | |
|---|---|---|---|
| text | string | required | The text to check |
| Field | Type | Description |
|---|---|---|
| approved | bool | Whether the text passed (no violations) |
| violations | string[] | List of violated policies; empty when approved |
| confidence | double | The model's confidence in the decision |
| rejectionReason | string? | Why it was rejected, if it was; null otherwise |
| 200 | Check completed (approved or not) |
| 401 | API key missing or invalid |
| 403 | Quota exceeded — request quota used up |
curl -X POST https://roxanai.ir/api/v1/moderation/check \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{"text": "sample text to check"}'
{"{"}
"approved": false,
"violations": ["hate_speech"],
"confidence": 0.94,
"rejectionReason": "..."
{"}"}
Webhook / Async API
The same content check, but asynchronous: the text is queued and the result is pushed to the webhook URL configured on your account.
🔑 Requires X-API-KEYQueues a text for processing and immediately returns a tracking ID.
| Field | Type | |
|---|---|---|
| text | string | required |
| Field | Type |
|---|---|
| status | "ok" |
| request_id | string |
| message | string |
If no webhook URL is configured for this API key, the request is rejected with "Configuration Error" — set your webhook URL in account settings before calling this endpoint.
curl -X POST https://roxanai.ir/api/v1/webhook/check-async \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{"text": "sample text"}'
{"{"}
"status": "ok",
"request_id": "a1b2c3...",
"message": "Request queued..."
{"}"}
Tracks the status of an async request using the ID returned by check-async.
| Field | Type | Description |
|---|---|---|
| requestId | string | — |
| status | enum | Queued · Processing · Sent · Failed · WaitingForRetry |
| createdAt | datetime | — |
| completedAt | datetime? | null until completion |
| result | object? | Only populated after completion — same shape as the Moderation API |
| errorMessage | string? | — |
Unknown / invalid request ID → 404
curl https://roxanai.ir/api/v1/webhook/status/a1b2c3 \
-H "X-API-KEY: your_api_key"
{"{"}
"requestId": "a1b2c3",
"status": "Sent",
"createdAt": "2026-08-30T09:12:00Z",
"completedAt": "2026-08-30T09:12:04Z",
"result": {"{"} "approved": true, ... {"}"},
"errorMessage": null
{"}"}
Paginated history of this account's requests. Requires a tenant panel session, not X-API-KEY — use the panel to view history rather than calling this from your integration.
| Parameter | Type | Default |
|---|---|---|
| page | int | 1 |
| pageSize | int | 20 |
How results are delivered
Once processing finishes, the payload below is POSTed to your registered webhook URL (no signature/HMAC header in the current implementation).
{"{"}
"request_id": "a1b2c3",
"timestamp": "2026-08-30T09:12:04Z",
"result": {"{"}
"approved": true,
"violations": [],
"confidence": 0.12,
"rejectionReason": null
{"}"}
{"}"}
result here matches the same camelCase shape as the direct /api/v1/moderation/check response — approved, violations, confidence, rejectionReason.
| Attempt | Delay |
|---|---|
| 1 | 30 seconds |
| 2 | 2 minutes |
| 3 | 10 minutes |
| 4 | 30 minutes |
| 5 (final) | 1 hour |
After 5 failed attempts the final status becomes Failed and no further retry is made — the result is then only recoverable via GET status/{"{requestId}"}.
Must start with https:// and must not resolve to a local or private host/IP (localhost, 127.0.0.1, the 10.x/172.16-31.x/192.168.x ranges, and similar) — such addresses are rejected during the connection test.
Chat Widget API
The API behind your chat widget. Built to be called from a visitor's browser, not for server-to-server calls from an external developer.
🌐 No API key — allowed only by the chatbot's registered Origin/RefererThese endpoints need no API key at all — instead they check the request's Origin/Referer against that chatbot's registered website URL. That means they only work from inside the site the widget is installed on — not as a general-purpose API for your backend to call. This section's usage quota (chat credits) is entirely separate from the Moderation/Webhook quota.
Installing the widget on a site
Most integrators never call the endpoints below directly — they just add the widget to their site and it calls these for them. Three ways to do that:
1. Plain HTML — works on any site or CMS
Paste this once, right before the closing </body> tag of your site's template:
<script src="https://roxanai.ir/widget/chatbot.js" data-slug="your-chatbot-slug"></script>
No extra <div> needed — the script draws its own chat bubble and panel. Appearance (colors, bot name, welcome message, position) comes from what's configured in the panel, not from the tag.
2. WordPress plugin
Install the RoxanAI Chatbot plugin, then under Settings → RoxanAI Chatbot enter your server address and slug and activate it. No code editing required.
3. Programmatic iframe — advanced
For opening the chat from your own button or event instead of the default bubble. Load this at page load (not on click):
<script src="https://roxanai.ir/widget/frame.js" data-slug="your-chatbot-slug" data-position="bottom-right" data-hide-bubble="true"></script>
This exposes window.$roxanai.toggle() globally — call it from any button to open or close the chat, with no default bubble shown. data-position is the only thing this tag controls; every other appearance setting still comes from the panel.
If a website URL is set for this chatbot, the widget only loads on that domain and its subdomains. Installing the same snippet on a different domain gets a silent 403 on the config request — the script logs a warning to the browser console and simply never draws anything, with no visible error for the site's visitors. Leaving the website URL unset lets the widget load anywhere.
Sends a message to the chatbot and returns the full (non-streamed) answer.
| Field | Type | |
|---|---|---|
| question | string | required |
| sessionToken | string | required |
| visitorName | string? | optional |
| visitorPhone | string? | optional |
| Field | Type | Description |
|---|---|---|
| answer | string | — |
| sessionToken | string | — |
| remainingCredits | int | Chatbot's remaining credit balance |
| noContextFound | bool | Retrieval found no relevant content |
| botMessageId | int? | Used with the rate endpoint |
403 {"{ code: \"credit_exhausted\" }"} if the chatbot is out of chat credit.
curl -X POST https://roxanai.ir/api/v1/knowledgebase/chat/acme-support \ -H "Content-Type: application/json" \ -H "Origin: https://acme.com" \ -d '{"question": "What does the basic plan cost?", "sessionToken": "..."}'
{"{"}
"answer": "The basic plan is billed monthly...",
"sessionToken": "sess_...",
"remainingCredits": 482,
"noContextFound": false,
"botMessageId": 9931
{"}"}
Same request, but the answer streams back token by token as Server-Sent Events.
| event | payload |
|---|---|
| token | {"{ text }"} |
| done | {"{ botMessageId, sessionToken, remainingCredits, noContextFound }"} |
| error | {"{ message, partial }"} |
Records a user's thumbs up/down on a bot answer.
| Field | Type |
|---|---|
| rating | bool |
Message not found → 404 · success → 200
Other endpoints in this group (not detailed in this version)
- GET /api/v1/knowledgebase/widget/{"{slug}"} — widget display settings
- GET /api/v1/knowledgebase/chat/{"{slug}"}/live/availability — whether a human operator is available
- POST /api/v1/knowledgebase/chat/{"{slug}"}/live/request — request to connect to an operator
- POST /api/v1/knowledgebase/chat/{"{slug}"}/live/offline-message — leave a message when no operator is available
Knowledge Base Management
Everything a tenant uses to build and maintain their chatbot's content — setup, document upload, manual text, and widget appearance.
🔑 Requires X-API-KEY, except widget settings below (tenant panel session only)Creates the tenant's chatbot / knowledge base for the first time.
| Field | Type | Validation | |
|---|---|---|---|
| companyName | string | required | max 200 chars |
| welcomeMessage | string? | optional | max 500 chars |
| toneInstructions | string? | optional | — |
| websiteUrl | string? | optional | valid http(s) URL, max 500 chars |
Response: 200 OK with the full KnowledgeBaseDto (see GET /KnowledgeBase), including the newly assigned slug.
curl -X POST https://roxanai.ir/api/v1/knowledgebase/setup \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{"companyName": "Acme Inc.", "welcomeMessage": "Hi, how can I help?"}'
Returns the current tenant's knowledge base, including every document and all widget settings.
| Field | Type |
|---|---|
| id | int |
| slug | string |
| companyName | string |
| welcomeMessage | string? |
| toneInstructions | string? |
| websiteUrl | string? |
| isActive | bool |
| documents | Document[] |
| widgetPrimaryColor | string |
| widgetPosition | string |
| widgetBotName | string? |
| widgetSubtitle | string? |
| widgetPlaceholder | string? |
| fallbackMessage | string? |
| outOfScopeMessage | string? |
| connectionErrorMessage | string? |
| widgetHeaderColor | string |
| widgetBackgroundColor | string |
| widgetTextColor | string |
| widgetLogoUrl | string? |
| leadCaptureEnabled | bool |
There's no separate document-count field — use documents.length.
{"{"}
"id": 12,
"title": "Pricing FAQ",
"fileName": "pricing.docx",
"sourceType": 1,
"status": 2,
"chunkCount": 14,
"wordCount": 860,
"errorMessage": null,
"createdAt": "2026-08-20T10:00:00Z",
"processedAt": "2026-08-20T10:00:42Z"
{"}"}
sourceType (number): 0=ManualText, 1=WordDocument, 2=WebsitePage.
status (number): 0=Pending, 1=Processing, 2=Completed, 3=Failed.
404 with {"{ error: \"not_found\" }"} if this tenant has no knowledge base yet.
Issues a new public slug and retires the old one. No body. Response: 200 OK with the updated KnowledgeBaseDto.
Every embedded widget snippet and shared chat link uses the old slug. Once regenerated, they stop resolving — update the embed code on the tenant's site right after calling this.
Uploads a document to be chunked and embedded into the knowledge base.
| Field | Type | |
|---|---|---|
| file | IFormFile | required |
| File type | .docx only |
| Hard cap (HTTP layer) | 5 MB |
| Business-rule cap (default) | 100 KB |
| Max files per knowledge base | 5 (default) |
| Max words per document | 2,000 (default) |
These business-rule limits are admin-configurable and can differ from the defaults shown.
curl -X POST https://roxanai.ir/api/v1/knowledgebase/12/upload \ -H "X-API-KEY: your_api_key" \ -F "file=@pricing.docx"
{"{"}
"id": 13,
"title": null,
"fileName": "pricing",
"originalFileName": "pricing.docx",
"contentType": "application/octet-stream",
"fileSizeBytes": 48213,
"wordCount": 0,
"contentText": null,
"objectKey": "knowledge-base/{"{userId}"}/12/{"{guid}"}-pricing.docx",
"sourceUrl": null,
"sourceType": 1,
"status": 0,
"chunkCount": 0,
"errorMessage": null,
"createdAt": "2026-08-30T10:18:22Z",
"processedAt": null
{"}"}
fileName has no extension — the original name is in originalFileName.
wordCount and contentText stay empty until processing finishes.
202 means queued, not processed — poll GET /knowledgebase and watch status flip to 2 (Completed) or 3 (Failed).
400 {"{ error: \"invalid_file\" }"} if no file was sent.
Fetches a web page, extracts its text, and embeds it — synchronously, unlike upload.
| Field | Type | |
|---|---|---|
| url | string | required |
Limit: 10 links per knowledge base (default), 2,000 words per page (default). Re-submitting an unchanged URL returns the existing document at no cost.
{"{"}
"id": 14,
"title": "Pricing — Acme",
"fileName": "Pricing — Acme",
"contentType": "text/html",
"wordCount": 340,
"contentText": "...",
"sourceUrl": "https://acme.com/pricing",
"sourceType": 2,
"status": 2,
"chunkCount": 9,
"createdAt": "2026-08-30T10:19:04Z",
"processedAt": "2026-08-30T10:19:25Z"
{"}"}
Returns 200, not 202 — the call blocks until embedding finishes, so status is already 2 (Completed).
Create, edit, or remove a manual text document — content typed directly into the panel rather than uploaded or crawled.
| Field | Type | |
|---|---|---|
| title | string | required, max 200 |
| description | string | required — this becomes the document's content, max 500 words |
| Field | Type |
|---|---|
| id | int |
| title | string |
| description | string |
| status | int (enum) |
| chunkCount | int |
| wordCount | int |
| createdAt | datetime |
| processedAt | datetime? |
status is a number: 0=Pending, 1=Processing, 2=Completed, 3=Failed.
Up to 50 manual text documents per knowledge base. DELETE only removes manual-text
documents (its chunks go with it) and returns 204 No Content.
404 {"{ error: \"not_found\" }"} if the slug isn't yours.
PUT-only: 404 if documentId doesn't exist.
403 credit_exhausted if the chatbot is out of chat credit.
Updates the widget's appearance and copy — colors, position, bot name, fallback messages, lead capture.
Unlike the rest of Knowledge Base Management, this endpoint only accepts a tenant panel session — an API key gets redirected to the login page instead of a normal response. Call it from a browser signed into the panel, not from a server-side integration.
| Field | Type | |
|---|---|---|
| primaryColor | string | required |
| position | string | required |
| botName | string? | optional |
| subtitle | string? | optional |
| placeholder | string? | optional |
| fallbackMessage | string? | optional |
| outOfScopeMessage | string? | optional |
| connectionErrorMessage | string? | optional |
| headerColor | string? | optional |
| backgroundColor | string? | optional |
| textColor | string? | optional |
| leadCaptureEnabled | bool | required |
This is a full replace, not a patch. Omitted fields bind to their type's default — a missing leadCaptureEnabled silently becomes false, and a missing primaryColor/position becomes null. Always send current values back, not just the ones you're changing.
Response (from the signed-in panel): 200 OK with the full updated KnowledgeBaseDto.