R RoxanAIDEVELOPER DOCS
v1

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.

Base URL: https://roxanai.ir/api/v1 Format: JSON Unversioned routes still work
Versioning

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.

Required header
HTTP Header
X-API-KEY: {your_api_key}
Missing or invalid key
CaseStatusBody
Header not sent401{"{"} "error": "API key is required", "message": "..." {"}"}
Invalid key401{"{"} "error": "Unauthorized", "message": "..." {"}"}
Quotas are independent

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.

Generic error
JSON
{"{"}
  "error": "Not Found",
  "message": "...",
  "errorId": "AB12CD34"
{"}"}
Validation error (422)
JSON
{"{"}
  "error": "Validation Error",
  "message": "One or more validation errors occurred.",
  "errors": { "{ }" }
{"}"}
Status mapping
StatusCause
400ArgumentException
401UnauthorizedAccessException / invalid API key
403ForbiddenException (includes quota exhausted)
404NotFoundException / KeyNotFoundException
409InvalidOperationException
422ValidationException
500Any other unhandled error

Moderation API

Checks a piece of text for policy violations.

🔑 Requires X-API-KEY
POST/api/v1/moderation/check

Submits a text for policy review and returns the result immediately (synchronous).

Request Body
FieldTypeDescription
textstringrequiredThe text to check
Response 200
FieldTypeDescription
approvedboolWhether the text passed (no violations)
violationsstring[]List of violated policies; empty when approved
confidencedoubleThe model's confidence in the decision
rejectionReasonstring?Why it was rejected, if it was; null otherwise
Status Codes
200Check completed (approved or not)
401API key missing or invalid
403Quota exceeded — request quota used up
cURL
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"}'
Example response
200 OK
{"{"}
  "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-KEY
POST/api/v1/webhook/check-async

Queues a text for processing and immediately returns a tracking ID.

Request Body
FieldType
textstringrequired
Response 200
FieldType
status"ok"
request_idstring
messagestring
400 — prerequisite

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
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"}'
Example response
200 OK
{"{"}
  "status": "ok",
  "request_id": "a1b2c3...",
  "message": "Request queued..."
{"}"}
GET/api/v1/webhook/status/{"{requestId}"}

Tracks the status of an async request using the ID returned by check-async.

Response 200
FieldTypeDescription
requestIdstring—
statusenumQueued · Processing · Sent · Failed · WaitingForRetry
createdAtdatetime—
completedAtdatetime?null until completion
resultobject?Only populated after completion — same shape as the Moderation API
errorMessagestring?—

Unknown / invalid request ID → 404

cURL
curl https://roxanai.ir/api/v1/webhook/status/a1b2c3 \
  -H "X-API-KEY: your_api_key"
Example response (completed)
200 OK
{"{"}
  "requestId": "a1b2c3",
  "status": "Sent",
  "createdAt": "2026-08-30T09:12:00Z",
  "completedAt": "2026-08-30T09:12:04Z",
  "result": {"{"} "approved": true, ... {"}"},
  "errorMessage": null
{"}"}
GET/api/v1/webhook/requests

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.

Query Params
ParameterTypeDefault
pageint1
pageSizeint20

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).

POST to your webhook URL
JSON
{"{"}
  "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.

Retry schedule on delivery failure
AttemptDelay
130 seconds
22 minutes
310 minutes
430 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}"}.

Webhook URL requirements

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/Referer
Don't lump this in with the Moderation/Webhook API

These 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:

HTML
<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):

HTML
<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.

The chatbot won't appear on the wrong domain

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.

POST/api/v1/knowledgebase/chat/{"{slug}"}

Sends a message to the chatbot and returns the full (non-streamed) answer.

Request Body
FieldType
questionstringrequired
sessionTokenstringrequired
visitorNamestring?optional
visitorPhonestring?optional
Response 200
FieldTypeDescription
answerstring—
sessionTokenstring—
remainingCreditsintChatbot's remaining credit balance
noContextFoundboolRetrieval found no relevant content
botMessageIdint?Used with the rate endpoint

403 {"{ code: \"credit_exhausted\" }"} if the chatbot is out of chat credit.

cURL
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": "..."}'
Example response
200 OK
{"{"}
  "answer": "The basic plan is billed monthly...",
  "sessionToken": "sess_...",
  "remainingCredits": 482,
  "noContextFound": false,
  "botMessageId": 9931
{"}"}
POST/api/v1/knowledgebase/chat/{"{slug}"}/stream

Same request, but the answer streams back token by token as Server-Sent Events.

SSE Events
eventpayload
token{"{ text }"}
done{"{ botMessageId, sessionToken, remainingCredits, noContextFound }"}
error{"{ message, partial }"}
POST/api/v1/knowledgebase/chat/{"{slug}"}/messages/{"{messageId}"}/rate

Records a user's thumbs up/down on a bot answer.

Request Body
FieldType
ratingbool

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)
POST/api/v1/knowledgebase/setup

Creates the tenant's chatbot / knowledge base for the first time.

Request Body
FieldTypeValidation
companyNamestringrequiredmax 200 chars
welcomeMessagestring?optionalmax 500 chars
toneInstructionsstring?optional—
websiteUrlstring?optionalvalid http(s) URL, max 500 chars

Response: 200 OK with the full KnowledgeBaseDto (see GET /KnowledgeBase), including the newly assigned slug.

cURL
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?"}'
GET/api/v1/knowledgebase

Returns the current tenant's knowledge base, including every document and all widget settings.

Response 200 — KnowledgeBaseDto
FieldType
idint
slugstring
companyNamestring
welcomeMessagestring?
toneInstructionsstring?
websiteUrlstring?
isActivebool
documentsDocument[]
widgetPrimaryColorstring
widgetPositionstring
widgetBotNamestring?
widgetSubtitlestring?
widgetPlaceholderstring?
fallbackMessagestring?
outOfScopeMessagestring?
connectionErrorMessagestring?
widgetHeaderColorstring
widgetBackgroundColorstring
widgetTextColorstring
widgetLogoUrlstring?
leadCaptureEnabledbool

There's no separate document-count field — use documents.length.

Each item in documents[]
Document
{"{"}
  "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.

POST/api/v1/knowledgebase/{"{knowledgeBaseId}"}/regenerate-slug

Issues a new public slug and retires the old one. No body. Response: 200 OK with the updated KnowledgeBaseDto.

Breaking change for anything already deployed

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.

POST/api/v1/knowledgebase/{"{knowledgeBaseId}"}/upload

Uploads a document to be chunked and embedded into the knowledge base.

Request — multipart/form-data
FieldType
fileIFormFilerequired
Limits
File type.docx only
Hard cap (HTTP layer)5 MB
Business-rule cap (default)100 KB
Max files per knowledge base5 (default)
Max words per document2,000 (default)

These business-rule limits are admin-configurable and can differ from the defaults shown.

cURL
curl -X POST https://roxanai.ir/api/v1/knowledgebase/12/upload \
  -H "X-API-KEY: your_api_key" \
  -F "file=@pricing.docx"
Example response — the full document object, same shape as items in GET /knowledgebase's documents[]
202 Accepted
{"{"}
  "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.

POST/api/v1/knowledgebase/{"{knowledgeBaseId}"}/url

Fetches a web page, extracts its text, and embeds it — synchronously, unlike upload.

Request Body
FieldType
urlstringrequired

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.

Example response — the full document object, same shape as upload's
200 OK
{"{"}
  "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).

POST/api/v1/knowledgebase/{"{slug}"}/texts  ·  PUT .../texts/{"{documentId}"}  ·  DELETE .../texts/{"{documentId}"}

Create, edit, or remove a manual text document — content typed directly into the panel rather than uploaded or crawled.

Request Body (POST & PUT)
FieldType
titlestringrequired, max 200
descriptionstringrequired — this becomes the document's content, max 500 words
Response 202 Accepted (POST & PUT)
FieldType
idint
titlestring
descriptionstring
statusint (enum)
chunkCountint
wordCountint
createdAtdatetime
processedAtdatetime?

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.

POST/api/v1/knowledgebase/widget/{"{knowledgeBaseId}"}

Updates the widget's appearance and copy — colors, position, bot name, fallback messages, lead capture.

X-API-KEY does not work on this endpoint

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.

Request Body
FieldType
primaryColorstringrequired
positionstringrequired
botNamestring?optional
subtitlestring?optional
placeholderstring?optional
fallbackMessagestring?optional
outOfScopeMessagestring?optional
connectionErrorMessagestring?optional
headerColorstring?optional
backgroundColorstring?optional
textColorstring?optional
leadCaptureEnabledboolrequired
Send the whole object every time

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.