SORAEN API reference
One HTTPS API for chat, answers from your own documents with citations, images and voice. JSON in, JSON out, from any language.
Quickstart
Subscribe on the pricing page to get an API key, then send your first request. Every example uses the base URL https://api.soraen.example.
curl https://api.soraen.example/v1/chat/completions \ -H "Authorization: Bearer $SORAEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "soraen-lite", "messages": [{"role": "user", "content": "Hello!"}]}'
Python, with nothing but requests:
import os, requests r = requests.post("https://api.soraen.example/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ['SORAEN_API_KEY']}"}, json={"model": "soraen-lite", "messages": [{"role": "user", "content": "Hello!"}]}) print(r.json()["choices"][0]["message"]["content"])
The request and response formats follow the widely used chat-completions convention, so most existing client libraries work by changing the base URL.
Authentication
Send your key in the Authorization header: Bearer sk-soraen-.... Keys are shown once when you subscribe; you can create a new one on the dashboard, which switches the old one off immediately.
Keep keys on your server. Never put them in a web page or mobile app, where anyone can read them.
Chat completions
POST /v1/chat/completions
| Field | Type | Description |
|---|---|---|
messages | array, required | The conversation: objects with role (system, user, assistant, tool) and content. Up to 100 messages. |
max_tokens | integer | Longest answer to generate. Default 1,024, maximum 4,096. Reserved against your quota up front. |
temperature, top_p | number | Sampling. Lower temperature (e.g. 0.2) for precise answers, higher for creative writing. |
stream | boolean | Send the answer as it's written. See Streaming. |
stop, seed, presence_penalty, frequency_penalty, response_format | Standard generation options. | |
tools, tool_choice | array, string | Functions the model may call. See Tool calling. |
rag | object | Answer from one of your document collections: {"collection": 1, "top_k": 6}. See Answers from documents. |
Other fields are ignored. The response:
{
"model": "soraen-lite",
"choices": [{"index": 0, "finish_reason": "stop",
"message": {"role": "assistant", "content": "Hello! How can I help?"}}],
"usage": {"prompt_tokens": 9, "completion_tokens": 7, "total_tokens": 16}
}
Streaming
With "stream": true the response is a stream of server-sent events: lines starting data: , each a JSON chunk whose choices[0].delta.content holds the next piece of text, ending with data: [DONE]. With rag, the first event carries sources.
with requests.post(URL, headers=H, json={**body, "stream": True}, stream=True) as r: for line in r.iter_lines(): if line.startswith(b"data: ") and line != b"data: [DONE]": chunk = json.loads(line[6:]) for c in chunk.get("choices", []): print(c["delta"].get("content", ""), end="", flush=True)
Documents
Collections hold your documents privately: only your key can list, search or answer from them. Upload plain text (Markdown, CSV and JSON work too); a document uploaded again under the same name replaces the old one.
| Endpoint | What it does |
|---|---|
POST /v1/collections | Create a collection: {"name": "Help center"} → {"id": 1, "name": ...} |
GET /v1/collections | Your collections, with document and passage counts |
DELETE /v1/collections/{id} | Delete a collection and everything in it |
POST /v1/collections/{id}/documents | Upload: {"name": "refunds.md", "text": "..."} → {"document": ..., "chunks": 3} |
GET /v1/collections/{id}/documents | Documents in a collection |
DELETE /v1/collections/{id}/documents/{name} | Delete one document |
POST /v1/collections/{id}/search | Search without generating: {"query": "refund window", "top_k": 6} → matching passages |
Answers from documents
Add rag to a chat request. SORAEN finds the most relevant passages with keyword and semantic search, answers only from them, and cites each fact as [1], [2]. The response lists what each number refers to. If the documents don't contain the answer, SORAEN says so instead of guessing.
r = requests.post(URL, headers=H, json={ "model": "soraen-lite", "rag": {"collection": 1, "top_k": 6}, "messages": [{"role": "user", "content": "How long do refunds take?"}]}).json() r["choices"][0]["message"]["content"] # "Refunds are issued within 14 days of purchase [1]." r["sources"] # [{"index": 1, "document": "refunds.md", "position": 0, "text": "..."}]
Your own system prompt is kept and the documents are added after it. Instructions written inside documents are treated as information, never as commands.
Images
Send images inside a user message as content parts. Photos, screenshots, charts and scanned pages all work. Images must be inline (data:image/png;base64,..., or JPEG, WebP, GIF); links to images on other sites are not fetched.
import base64 img = "data:image/jpeg;base64," + base64.b64encode(open("receipt.jpg", "rb").read()).decode() body = {"model": "soraen-lite", "messages": [{"role": "user", "content": [ {"type": "text", "text": "What's the total on this receipt?"}, {"type": "image_url", "image_url": {"url": img}}]}]}
Up to 4 images per request, about 5 MB each. Each image reserves 1,000 tokens of quota; you're billed what the model actually used.
Voice
POST /v1/audio/transcriptions turns speech into text (about 100 languages). Send the recording as a multipart file (WAV, MP3, M4A, OGG or WebM, up to 10 MB and 10 minutes); optionally add language, e.g. en. Then send the text to chat as usual.
r = requests.post("https://api.soraen.example/v1/audio/transcriptions", headers=H, files={"file": open("question.m4a", "rb")}) r.json() # {"text": "How long do refunds take?", "language": "en", "duration": 3.6}
Billed at 25 tokens per second of audio (about 1,500 per minute).
Tool calling
Describe functions in tools (JSON Schema parameters). When the model wants one, the reply has tool_calls instead of text; run the function, send the result back as a tool message, and ask again.
tools = [{"type": "function", "function": {
"name": "get_order", "description": "Look up an order by its number",
"parameters": {"type": "object", "properties": {"number": {"type": "string"}}, "required": ["number"]}}}]
# reply: {"tool_calls": [{"function": {"name": "get_order", "arguments": "{\"number\": \"A123\"}"}}]}
Usage & account
| Endpoint | What it does |
|---|---|
GET /v1/usage | {"tokens_this_month": 12345, "monthly_token_limit": 3000000} |
GET /v1/account | Email, subscription status, usage and limits |
POST /v1/account/rotate-key | New key for the same account; the old key stops working at once |
GET /v1/models | Available models (soraen-lite) |
Usage resets on the 1st of each month (UTC). Questions, answers, document passages added for rag, images and audio all count.
Errors
Errors return a JSON body with a readable message in detail (or error.message).
| Status | Meaning | What to do |
|---|---|---|
400 | Malformed request | Check the JSON and field types |
401 | Missing or invalid key, or subscription ended | Check the Authorization header; see your dashboard |
402 | Monthly tokens used up, or this request would exceed them | Lower max_tokens, or wait for the 1st |
403 | A collection, document or storage limit reached | Delete something, or contact us for more |
404 | Collection or document not found | Check the id; collections are private to your key |
413 | Request too large (messages, images or audio) | See Limits |
429 | More than 60 requests in a minute | Wait a few seconds and retry with backoff |
502, 503 | The model or a helper service is briefly unavailable | Retry after a few seconds |
Limits (Pro plan)
| Tokens per month | 3,000,000 |
| Requests per minute | 60 |
| Answer length | 4,096 tokens (default 1,024) |
| Conversation | 100 messages, 200,000 characters |
| Images | 4 per request, ~5 MB each |
| Audio | 10 MB, 10 minutes per file |
| Collections | 10 per account, 500 documents each, 20 million characters in total |
| Document size | 2 million characters |
Safety
Every request and answer is screened by a safety model. Harmful requests get a polite refusal with finish_reason: "content_filter" instead of an error, so your app can show it like any other reply. Use of the API is subject to the terms and acceptable use policy.