API reference
Canonn serves one model id, canonn-r1, tuned for a single job: answering from the documents you give it and declining when the answer is not there. Behind that id, each request is answered by one of four experts on the same base: the default, a conflict expert when the data disagrees with itself, an injection expert when the user's message tries to override your documents, and a missing-answer expert when your documents do not contain the answer. A trained router picks among the first three; a check on whether the data contains the answer hands a request to the fourth. When the router does not run, a conflict check stands in. Both report what they saw in canonn.route and canonn.conflict. How Canonn answers.
The API is OpenAI-compatible. Point any OpenAI SDK at https://api.canonn.ai/v1, swap the key, and your existing code works. Every example on this page was run against production as written.
Authentication
Create a key in the dashboard and send it as a bearer token. A key is shown once at creation and stored only as a hash: it cannot be recovered later, only revoked and replaced.
Authorization: Bearer canonn-sk-...New accounts get 100 free requests with no card. Refer a developer and you both get +1,000 requests when they sign up. Past that, add credits in the dashboard.
How to prompt it
Put the documents in the system message and the question in the user message. That is the whole technique.
Do not write a rulebook. Faithfulness is trained into the weights, not instructed at runtime. Instructions like "only use the provided context, do not hallucinate, cite your sources" cost tokens on every request and dilute attention. Canonn behaves that way without them.
// system Answer from the data below. Data: Refunds are accepted within 45 days of purchase. // user What is the refund window?
Quickstart
A complete, runnable request. Replace the key and go.
curl --location 'https://api.canonn.ai/v1/chat/completions' \ --header 'Authorization: Bearer canonn-sk-...' \ --header 'Content-Type: application/json' \ --data '{ "model": "canonn-r1", "messages": [ { "role": "system", "content": "Answer from the data below.\n\nData: Refund window: 45 days from purchase." }, { "role": "user", "content": "How long do I have to request a refund?" } ] }'
from openai import OpenAI client = OpenAI( base_url="https://api.canonn.ai/v1", api_key="canonn-sk-...", ) answer = client.chat.completions.create( model="canonn-r1", messages=[ {"role": "system", "content": "Answer from the data below.\n\nData: Refund window: 45 days from purchase."}, {"role": "user", "content": "How long do I have to request a refund?"}, ], ) print(answer.choices[0].message.content)
import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.canonn.ai/v1", apiKey: "canonn-sk-...", }); const answer = await client.chat.completions.create({ model: "canonn-r1", messages: [ { role: "system", content: "Answer from the data below.\n\nData: Refund window: 45 days from purchase." }, { role: "user", content: "How long do I have to request a refund?" }, ], }); console.log(answer.choices[0].message.content);
{
"id": "chatcmpl-84af4cb938563908",
"object": "chat.completion",
"created": 1790572843,
"model": "canonn-r1",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "45 days from purchase." },
"finish_reason": "stop",
"logprobs": null
}],
"usage": { "prompt_tokens": 47, "completion_tokens": 7, "total_tokens": 54 },
"canonn": {
"ai_generated": true,
"generator": "canonn-r1",
"provider": "Canonn",
"generated_at": "2026-09-28T05:20:43.000Z",
"standard": "EU AI Act Art. 50(2)"
}
}Everything outside canonn is the OpenAI shape; OpenAI SDKs ignore the extra field. What canonn carries.
Chat completions
Creates a grounded answer. Same request and response shape as the OpenAI endpoint of the same name.
Body
| Field | Type | Description |
|---|---|---|
| messages required | array | Documents in the system message, question in the user message. Prior turns may be included. |
| model | string | canonn-r1 (the alias canonn/canonn-r1 also works). Omitted means canonn-r1; any other name is a 404 with code model_not_found. |
| stream | boolean | Server-sent events instead of one JSON body. Default false. Streaming. |
| stream_options | object | {"include_usage": true} adds OpenAI's separate usage chunk to a stream. |
| temperature | number | Default 0.2. Set 0 for extraction work; higher values add variance, not insight. |
| max_tokens | integer | Cap on the answer length. Default 1,024; values above 4,096 are lowered to 4,096. |
| top_p | number | Nucleus sampling. |
| stop | string or array | Sequences that end generation. |
| seed | integer | Sampling seed. |
| response_format | object | json_object or json_schema, as in the OpenAI API. |
| files | boolean or array | Ground on uploaded files. Grounded answers. |
| retrieval | object | Retrieval tuning when files is set. |
| canonn | boolean | Streams only: true adds the canonn event before [DONE]. Non-streamed responses always carry canonn. |
Parameters Canonn does not support
| Field | What happens |
|---|---|
| tools, functions | 400, code unsupported_parameter. An empty tools list is ignored. |
| logprobs, top_logprobs | 400, code unsupported_parameter. |
| n, best_of | Ignored: every request returns one choice. |
When the answer is not in the data
This is the behaviour Canonn exists for. Ask something the documents do not cover and it says so, instead of inventing a plausible answer.
{
"model": "canonn-r1",
"messages": [
{ "role": "system", "content": "Answer from the data below.\n\nData: Plan A costs 10 dollars per month." },
{ "role": "user", "content": "How much does Plan B cost?" }
]
}"The data does not say anything about Plan B, so I can't answer that.
What it does say is that Plan A costs 10 dollars per month."Finish reasons
| Value | Meaning |
|---|---|
| stop | The model finished its answer. |
| length | Cut off by max_tokens. Raise it and retry rather than shipping a truncated answer. |
The canonn object
Every non-streamed completion carries a canonn object next to the OpenAI fields. A stream carries the same object as its last event when the request sets "canonn": true.
| Field | When | What it says |
|---|---|---|
| route | The router ran | {"route", "p"}: the expert that answered (default, conflict, injection or absent, when the documents do not contain the answer) and the router's probability for each. p is null when the router could not be reached and the default expert answered; p_absent is the probability that the data does not contain the answer. |
| conflict | The router or the conflict check ran | {"p", "flagged"}: the probability that the data contradicts itself on this question, and whether the answer was sent down the conflict path. |
| files, citations | The request set files | The files and passages the answer was grounded on. Grounded answers. |
| ai_generated, generator, provider, generated_at, standard | Always | The machine-readable mark that the text is AI-generated (EU AI Act Art. 50(2)). Every response also carries the header X-Canonn-AI-Generated: true. |
The router and the conflict check read requests whose data (the system messages, plus any user message of 400 characters or more) is between 300 and 60,000 characters. Shorter or longer requests go to the default expert, and route and conflict are absent.
"canonn": { "conflict": { "p": 0.048, "flagged": false }, "route": { "route": "default", "p": { "default": 0.952, "conflict": 0.048, "injection": 0 } }, "ai_generated": true, "generator": "canonn-r1", "provider": "Canonn", "generated_at": "2026-09-28T05:21:38.000Z", "standard": "EU AI Act Art. 50(2)" }
Streaming
Set stream: true to receive tokens as they are generated. Chunks arrive as data: lines and the stream ends with data: [DONE]. The chunk that carries finish_reason also carries usage. With stream_options.include_usage the usage comes instead in OpenAI's separate final chunk, with an empty choices array.
Use streaming for long answers. A non-streamed generation that runs past about 100 seconds can hit the edge's first-byte timeout; a streamed one is unaffected once the first chunk flows.
curl -N --location 'https://api.canonn.ai/v1/chat/completions' \ --header 'Authorization: Bearer canonn-sk-...' \ --header 'Content-Type: application/json' \ --data '{"model":"canonn-r1","stream":true,"messages":[ {"role":"system","content":"Answer from the data below.\n\nData: Support hours are 9am to 5pm on weekdays."}, {"role":"user","content":"Is support available on Saturday?"}]}' data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"The"},"finish_reason":null}], ...} data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":""},"finish_reason":"stop"}], "usage":{"prompt_tokens":45,"completion_tokens":30,"total_tokens":75}, ...} data: [DONE]
stream = client.chat.completions.create(
model="canonn-r1",
stream=True,
messages=[
{"role": "system", "content": "Answer from the data below.\n\nData: Support hours are 9am to 5pm on weekdays."},
{"role": "user", "content": "Is support available on Saturday?"},
],
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
With "canonn": true in the body, one more event arrives just before [DONE]: data: {"canonn": {...}}, the same object a non-streamed response carries. It is not an OpenAI chunk, so it is off unless asked for.
Knowledge files
Upload documents once and ground any chat completion on them, instead of packing your knowledge base into every request.
Upload
OpenAI-compatible multipart, or JSON with filename and content. Supported: plain text, Markdown, JSON (one passage per record), HTML. Max 1 MB per uploaded file, 100 files per account, 30 uploads an hour.
curl https://api.canonn.ai/v1/files \ -H "Authorization: Bearer canonn-sk-..." \ -F purpose=assistants \ -F file=@policy.md
{
"id": "file-2da93b04d430cc5e52d1ef9b",
"object": "file",
"purpose": "assistants",
"filename": "policy.md",
"bytes": 365,
"created_at": 1790572907,
"status": "processed",
"chunk_count": 1,
"chunking": "pending"
}How a file is cut into passages
A file answers questions the moment the upload returns: it is stored at once, cut by a size-based chunker (imported sources and JSON keep one passage per record). The model then re-cuts it in the background, usually within seconds. Wherever a cut would separate text that depends on what came before, such as a rule from its exception, a heading from its body or a table from its header row, the model is asked whether the two halves belong together, and the cut is not made if they do. Every passage also gets one context line saying which document and section it comes from; that line is indexed and shown to the model with the passage. The file object's chunking field says where this stands:
| chunking | Meaning |
|---|---|
| pending, working | Stored and answering from the first cut; the model pass is queued or running. |
| model | Re-cut by the model, with a context line on every passage. |
| partial | The per-file time or request cap was hit; the rest of the file kept the first cut. |
| heuristic | Kept the first cut: the file is too large for the model pass, or the pass could not run for a day. |
Sources the server fetches for you
Send a JSON body instead of a file and the edge pulls the content itself: public pages only, one record per page, article or file, so a passage never begins on one page and ends on another and a citation always points at a whole document. The response is the same file object; web imports also carry found and records, so a source larger than one import can take says so rather than leaving a partial import looking complete.
| Body | What you get |
|---|---|
{"url": "https://…", "crawl": true} | One page, or with crawl a same-site crawl, one record per page. |
{"sitemap": "https://…/sitemap.xml"} | Every page the sitemap lists. Sitemap indexes are followed one level, up to 50 child sitemaps. |
{"feed": "https://…/rss"} | RSS 2.0 or Atom, up to 40 items; teaser-only items are fetched from their link. |
{"zendesk": "support.acme.com"} | Every article of a public Zendesk help center. |
{"helpcenter": {"provider": "intercom", "url": "https://help.acme.com/en/"}} | Public Intercom, Help Scout ("helpscout") or HubSpot ("hubspot") help centers. A path such as /en/ keeps one language. |
{"github": "owner/repo"} | README, docs/ and every Markdown file of a public repository, up to 80 files. |
{"postgres": {"connection_string": "postgres://…", "query": "select …"}} | One record per row of a SELECT, up to 5,000 rows, run read-only with a 20 s statement timeout. "mysql" works the same way. The database must be reachable from the internet; credentials are used for that request and never stored. {"table": {"schema": "public", "name": "articles"}} instead of "query" imports a whole table. POST /v1/files/preview with the same body returns the first 5 rows, and with "tables": true inside the postgres or mysql object lists the tables with row counts and columns; nothing is stored either way. |
{"rest": {"url": "https://…", "authorization": "Bearer …"}} | One JSON response, flattened into records. The header is used for that request only and never stored. |
{"filename": "faq.csv", "content": "…"} | Text you extracted yourself. This is how the dashboard sends PDFs, Office files, scanned pages and Google Workspace documents, which it reads in the browser. |
Manage
| Route | Description |
|---|---|
GET /v1/files | List your files, newest first. |
GET /v1/files/{id} | File metadata, including chunking. |
GET /v1/files/{id}/content | The extracted text, reconstructed, as text/plain. |
GET /v1/files/{id}/chunks | The passages as retrieval sees them: ordinal, method (judge, heuristic or record), context, text, chars, n_tokens. |
DELETE /v1/files/{id} | Removes the file and every passage extracted from it, immediately. Returns {"id", "object": "file", "deleted": true}. |
{
"object": "list",
"file_id": "file-2da93b04d430cc5e52d1ef9b",
"filename": "policy.md",
"chunking": "model",
"chunking_at": 1790572911,
"chunk_count": 1,
"data": [{
"ordinal": 0,
"method": "judge",
"context": "HarborPay pricing policy - pricing policy: HarborPay charges 2.9% plus 30 cents per card transaction, …",
"text": "# HarborPay pricing policy\n\nHarborPay charges 2.9% plus 30 cents per card transaction. …",
"chars": 492,
"n_tokens": 66
}]
}Grounded answers
Set "files" on a chat completion and the server retrieves the most relevant passages, builds the grounded prompt, and answers from them. An array of file ids limits the search to those files (up to 20); true searches every file on the account. A system message you send is kept as a style note under the retrieved data. Greetings and small talk are answered without retrieval.
{
"model": "canonn-r1",
"files": ["file-2da93b04d430cc5e52d1ef9b"],
"messages": [
{ "role": "user", "content": "What does HarborPay charge per card transaction?" }
]
}The response's canonn object names the passages that stood in front of the model, in rank order, as citations. A stream gets the same list in its canonn event when the request sets "canonn": true, and every grounded stream carries the file ids in the X-Canonn-Files header.
"canonn": { "files": ["file-2da93b04d430cc5e52d1ef9b"], "citations": [ { "file_id": "file-2da93b04d430cc5e52d1ef9b", "filename": "policy.md", "ordinal": 0, "score": 1, "excerpt": "HarborPay pricing policy - pricing policy: … HarborPay charges 2.9% plus 30 cents per card transaction. …" } ], "conflict": { … }, "route": { … }, "ai_generated": true, … }
When nothing on the account matches the question, the answer says so without calling the model and is not billed: "No relevant data was found in your knowledge base for this question. …" with empty files and citations.
Retrieval tuning
| Field | Type | Description |
|---|---|---|
| retrieval.top_k | integer | Passages the ranker may select. Default 8, max 40. |
| retrieval.budget | integer | Max characters of retrieved data placed in front of the model. Default 12000, max 60000. |
Audits
Checks one knowledge file for statements that contradict each other or have gone out of date. Sentences with a measurable value (an amount, a percentage, a duration, a date) are paired by topic, and the model judges each pair. The request returns at once with 202; poll the audit until status is done. 5 audits a day per account.
curl https://api.canonn.ai/v1/audits \ -H "Authorization: Bearer canonn-sk-..." \ -H "Content-Type: application/json" \ -d '{"file_id": "file-2da93b04d430cc5e52d1ef9b"}' → 202 {"id":"audit-f2aa302029e45b6d7026","file_id":"file-2da93b04d430cc5e52d1ef9b","status":"done","chunks":1,"claims":4,"candidates":0}
Instead of file_id, the body may name a source to import first: url (with optional crawl), sitemap or zendesk, as in POST /v1/files. The imported file stays on the account.
| Route | Description |
|---|---|
GET /v1/audits/{id} | status (judging or done), progress from 0 to 1, and once done a report: counts of conflicts and stale statements, and items, each with both quotes, their values, the passage ordinals, a probability p and a confidence of likely or possible. |
GET /v1/audits | Your 50 most recent audits, without reports. |
Models
Returns the models your key can address. There is one. GET /v1/models/canonn-r1 returns the same object on its own (what the OpenAI SDKs' models.retrieve calls); any other id is a 404 with code model_not_found.
{
"object": "list",
"data": [{ "id": "canonn-r1", "object": "model", "created": 1785196800, "owned_by": "canonn" }]
}Model catalog
Public and unauthenticated: the machine-readable catalog used by aggregators, with context length, modalities, supported parameters and per-token prices in USD.
{
"data": [{
"id": "canonn-r1",
"name": "Canonn R1",
"created": 1785196800,
"description": "A 9B model fine-tuned for context faithfulness: … Benchmarks: canonn.ai/benchmark",
"input_modalities": ["text"],
"output_modalities": ["text"],
"context_length": 16384,
"max_output_length": 4096,
"pricing": { "prompt": "0.00000025", "completion": "0.000001", "image": "0", "request": "0" },
"quantization": "bf16",
"hugging_face_id": null,
"supported_sampling_parameters": ["temperature", "top_p", "max_tokens", "stop", "seed"],
"supported_features": ["streaming", "json_mode", "structured_outputs"],
"datacenters": [{ "country_code": "CA" }],
"is_ready": true,
"is_free": false
}]
}Errors
Errors from /v1/chat/completions, /v1/models, /v1/files and their sub-routes use the OpenAI error shape, so OpenAI SDKs raise their usual exception classes:
{
"error": {
"message": "The model 'gpt-4o' does not exist. Use 'canonn-r1'.",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}/v1/audits still answers errors as {"error": "message"}.
| Status | type / code | Meaning |
|---|---|---|
| 400 | invalid_request_error | Malformed body (invalid json), an empty file, or an unsupported parameter (code unsupported_parameter, with param naming it). |
| 401 | authentication_error / invalid_api_key | Missing, malformed, or revoked key. |
| 402 | insufficient_quota / free_tier_exhausted | Free requests spent and no credits on the account. The body adds a free_tier object with the limits. |
| 404 | invalid_request_error / not_found, model_not_found | Unknown file, or a model other than canonn-r1. |
| 413 | invalid_request_error | An uploaded file over 1 MB. |
| 429 | rate_limit_error / rate_limit_exceeded, capacity | More than 120 requests a minute on one key, or every serving node busy (code capacity, with Retry-After: 2). |
| 502, 504 | server_error | The model could not be reached or timed out. Retry; these are not billed. |
Pricing and limits
| Item | Value |
|---|---|
| Input | $0.25 per 1M tokens |
| Output | $1 per 1M tokens |
| Free tier | 100 requests on signup, no card; +1,000 requests per successful referral |
| Rate limit | 120 chat completions per minute per key |
| Context window | 16,384 tokens |
| Max output | 4,096 tokens |
| Files | 1 MB per upload, 100 files per account, 30 uploads an hour |
| Audits | 5 a day per account |
| Inference region | Canada |
Usage draws down prepaid credits. Optional auto top-up recharges your balance when it falls below a threshold you choose. Volume pricing and dedicated capacity: hello@canonn.ai.
We do not train on your prompts or completions. Prompt logging is opt-in, off by default, and logged bodies are deleted after 30 days. Full policy.
SDKs and tools
Any OpenAI-compatible client works: official SDKs, LangChain, LlamaIndex, Vercel AI SDK. Set the base URL to https://api.canonn.ai/v1 and the model to canonn-r1. Coding agents can reach your files through the Canonn MCP server.
The full machine-readable specification is published as OpenAPI 3.1 (JSON). Import it into Postman, Insomnia, or Apidog to get a working collection in one step.