Docs · Native API
Collections: send your own documents
A collection is a set of documents that you send to the gateway once, and that a chat call can draw on afterwards. It is how an app gives a model knowledge the model does not have, such as the manual of your own product, so that people can ask about it and get answers that come from it.
The gateway splits each document into pieces, indexes them, and on every call that names the collection it finds the pieces that bear on the question and puts them in the prompt. This is retrieval-augmented generation, usually called RAG.
How it works#
- Create a collection:
POST /api/v1/collections. - Send the documents: a
PUTfor each one, to…/documents/{document id}. Sending a document again replaces it. - Name the collection in a chat call: add
"collections": [12]to the body ofPOST /v1/chat/completions,POST /v1/messagesorPOST /v1/responses.
The documents are stored by the gateway. Your app sends them once, and again only when they change.
Everything on this page needs your API key and that your account group has API access. See authentication. The routes that manage collections refuse a narrow token, such as the one Code mode carries, with 403 and codigo: ability_insuficiente. A chat call does not need more than the key it already uses.
Limits#
| What | Default |
|---|---|
| collections per account | 20 |
| documents per collection | 300 |
| text of one document | 1,000,000 bytes |
| pieces in all your collections | 3000 |
| collections named in one chat call | 5 |
| pieces that go into one prompt | 5 |
The operator of an installation can change these numbers. The pieces are about 1200 characters each, so 3000 of them hold a few megabytes of text. A request that would pass a limit is refused, and nothing of it is stored.
The embedding calls that every write makes are limited too: 30 writes per minute, 6 reindex calls per minute and 60 searches per minute, each on its own counter and on top of the general limit of 300 requests per minute. See rate limits and quotas.
Collections#
POST/api/v1/collections#
curl -s "$SHVIA_URL/api/v1/collections" \
-H "Authorization: Bearer $SHVIA_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Product manual", "description": "How the app works, screen by screen" }'{
"id": 12,
"name": "Product manual",
"description": "How the app works, screen by screen",
"documents": 0,
"chunks": 0,
"created_at": "2026-10-15T12:00:00+00:00",
"updated_at": "2026-10-15T12:00:00+00:00"
}It answers 201. name is required, up to 120 characters, and must be unique among your collections. description is optional, up to 2000 characters. chunks is the number of pieces the documents were split into.
The other routes#
| Route | What it does |
|---|---|
GET /api/v1/collections | {"collections": […]}, yours only |
GET /api/v1/collections/{id} | one collection |
PUT /api/v1/collections/{id} | changes name and description. Send only what changes |
DELETE /api/v1/collections/{id} | deletes the collection and every document in it. Answers {"status": "ok", "deleted": true} |
A collection that is not yours answers 404 with code: collection_not_found, exactly like one that does not exist, so a 404 never tells whether the id is real.
Documents#
A document has an id that you choose, from 1 to 191 letters, digits, ., _, : and -. It is how you send the same document again: the path of PUT is the document's identity.
PUT /api/v1/collections/{id}/documents/{document id}#
curl -s -X PUT "$SHVIA_URL/api/v1/collections/12/documents/screens-deadlines.md" \
-H "Authorization: Bearer $SHVIA_API_KEY" -H "Content-Type: application/json" \
-d '{
"title": "Deadlines screen",
"text": "# Deadlines\n\nA notice received from the court gazette opens a deadline. The system counts it in business days.\n"
}'{
"id": "screens-deadlines.md",
"title": "Deadlines screen",
"size_bytes": 112,
"chunks": 1,
"embed_model": "nomic-embed-text",
"hash": "9b1d…",
"created_at": "2026-10-15T12:01:00+00:00",
"updated_at": "2026-10-15T12:01:00+00:00",
"result": "created"
}| Field | Notes |
|---|---|
text | required. The document, as plain text or Markdown. Whitespace at the start and at the end is removed |
title | optional, up to 255 characters. It labels the document in the excerpts. It defaults to the document id |
result says what happened:
result | Status | Meaning |
|---|---|---|
created | 201 | a new document |
updated | 200 | the text changed, so the old pieces were replaced by new ones |
unchanged | 200 | the same text as before. Nothing was split or embedded, so sending everything again is cheap. A new title alone is applied without embedding |
Write Markdown headings. The gateway splits a document at its headings first and then at paragraph breaks, and it puts the chain of headings (Deadlines > Business days) in front of every piece, so a piece keeps the context of the section it came from. A document with no headings is split at paragraphs.
The text is split and embedded before anything is stored. If the embedding service cannot be reached, the call answers 503 and the document stays as it was.
The other routes#
| Route | What it does |
|---|---|
GET /api/v1/collections/{id}/documents | {"documents": […]}, without the text |
GET /api/v1/collections/{id}/documents/{document id} | one document, with its text |
DELETE /api/v1/collections/{id}/documents/{document id} | deletes it and its pieces |
Search#
POST/api/v1/collections/{id}/search#
Runs the same retrieval that a chat call runs, and shows what came back. Use it to check a collection before you point a chat at it, and to see why an answer missed something.
curl -s "$SHVIA_URL/api/v1/collections/12/search" \
-H "Authorization: Bearer $SHVIA_API_KEY" -H "Content-Type: application/json" \
-d '{ "query": "how is a deadline counted?", "top_k": 3 }'{
"results": [
{
"document_id": "screens-deadlines.md",
"title": "Deadlines screen",
"heading_path": "Deadlines",
"text": "[Deadlines]\n\nA notice received from the court gazette opens a deadline. …",
"score": 0.03279,
"similarity": 0.61
}
],
"semantic": true
}query is required, up to 2000 characters. top_k is optional, from 1 to 12. score orders the results and has no scale of its own: compare results with each other, not with a threshold. similarity is the cosine between the question and the piece, or null for a piece that was found by its words alone. semantic is false when the embedding service could not be reached and the order comes from the words of the question alone; the results are still real.
How the pieces are chosen#
Two rankings are merged. One compares meaning, through the embeddings. The other compares the words themselves, which catches names that an embedding blurs, such as a screen's title or an acronym. Accents and case do not matter in the second.
POST/api/v1/collections/{id}/reindex#
Splits and embeds every document of the collection again, from the text that the gateway kept, and answers {"status": "ok", "reindexed": 3}. Call it after the operator changes the embedding model: pieces made by another model are found by their words only until then.
Using a collection in a chat call#
Add collections to the body of one of these endpoints:
POST /v1/chat/completions, see chat completionsPOST /v1/messages, see messagesPOST /v1/responses, see responses
curl -si "$SHVIA_URL/v1/chat/completions" \
-H "Authorization: Bearer $SHVIA_API_KEY" -H "Content-Type: application/json" \
-d '{
"model": "<an id from GET /v1/models>",
"collections": [12],
"messages": [
{ "role": "system", "content": "You are the help assistant of the Acme app. Answer briefly." },
{ "role": "user", "content": "How is a deadline counted?" }
]
}'collections is a list of 1 to 5 collection ids. With an OpenAI SDK, send it as an extra body field (extra_body={"collections": [12]} in Python). A call that does not name a collection gets nothing from any of them, and the field is never forwarded to the model server.
What the gateway does with it:
- It takes the text to search by from your messages: the last
usermessage. A short follow-up of fewer than 40 characters, such as "and on a Sunday?", is searched together with theusermessage before it. - It retrieves up to 5 pieces from the named collections.
- It adds them to the system message of the call: to the one you sent, or as a new one when you sent none. The pieces are marked as data, and an instruction inside a document is not obeyed, so a document cannot give orders to the model. Your own system message is where the instructions go.
- The model answers. The prompt tells it to answer from the pieces, to say so when they do not contain the answer, and to answer in the language of the question.
When no piece matches, the prompt says that none matched, so the model does not answer a question about your documents from its general knowledge as if it knew.
The response is the normal response of the endpoint. These headers say what was used:
| Header | Value |
|---|---|
X-Inference-Collections | the ids that were used, separated by commas |
X-Inference-Excerpts | how many pieces went into the prompt. 0 means none matched |
The pieces count as input tokens of the call, and so against your quota. Five pieces are roughly 1500 to 2500 tokens, depending on the language. Retrieval costs one embedding call per question and is not metered.
What a call can be refused for#
| Status | code | When |
|---|---|---|
| 422 | invalid_collections | collections is not a list of 1 to 5 positive integers |
| 404 | collection_not_found | an id is not yours or does not exist |
| 403 | collections_blocked_by_lgpd | the account has LGPD protection on and the model is not on the operator's own servers. See below |
The code is in error.code on /v1/chat/completions and /v1/responses, and in error.code of the Anthropic error object on /v1/messages. A refused call reaches no model.
A call is refused rather than answered without the documents on purpose: an app that asked for its manual and got a reply from the model's general knowledge would be wrong with confidence, and nothing in the reply would say so.
Your data#
The pieces are your own text, sent on purpose and named on each call. They follow the rule of a project's instructions and files, and not the rule of the operator's knowledge base:
- They can go to a cloud model, like the rest of your prompt, unless you turned on LGPD protection in your account settings. With it on, a call that names a collection and ends up on a cloud model is refused with
403, because leaving the pieces out silently would give a wrong answer. A model on the operator's own servers is only backed up by another one there, so a call that starts there stays there. - On a cloud model, personal data in the pieces is masked like the rest of the prompt. See your data.
- The documents are stored by the gateway, in its database, and only your account reads them. Deleting a collection or a document deletes its text and its pieces.
- Do not put in a collection what must never leave your own servers. Check where a model runs with
data_localityin models.
Recipe: an assistant that knows your app#
Your app wants to answer "where do I issue an invoice?" from its own documentation. Keep the documentation as Markdown files with headings, one per screen or topic, and sync them with a script:
#!/bin/sh
# Sends every .md file of ./help to collection $COLLECTION. Run it on every release.
for f in help/*.md; do
id=$(basename "$f")
jq -n --rawfile text "$f" --arg title "$id" '{title: $title, text: $text}' |
curl -s -X PUT "$SHVIA_URL/api/v1/collections/$COLLECTION/documents/$id" \
-H "Authorization: Bearer $SHVIA_API_KEY" -H "Content-Type: application/json" \
--data-binary @- | jq -r '"\(.id): \(.result)"'
doneFiles that did not change answer unchanged and cost nothing. A file you deleted from help/ stays in the collection until you DELETE its document.
Then each question goes to the model with the collection named. Put the behaviour of the assistant in the system message, and the facts in the collection:
from openai import OpenAI
client = OpenAI(base_url="https://ai.shvia.org/v1", api_key=SHVIA_API_KEY)
reply = client.chat.completions.create(
model=MODEL_ID, # copy an id from GET /v1/models
messages=[
{"role": "system", "content": "You are the help assistant of Acme. Answer in two or three sentences and say which screen to open."},
{"role": "user", "content": question},
],
extra_body={"collections": [12]},
)
print(reply.choices[0].message.content)Some practical points:
- One account serves all your users. Your app calls with its own key, and every call counts against the account's limits: 20 requests per minute across all the
/v1endpoints by default, see rate limits and quotas. Ask the operator for a higher limit before you put a busy app behind one key. - Keep the question about the product. Retrieval works from the user's last message. A question about something your documents do not cover returns weak pieces, and the prompt tells the model to say it does not know.
- Let people choose a model, or choose for them. Read the list once from
GET /api/v1/profiles, keep the ids, and send the id the person picked asmodel. - Check before you trust. Use the search route with the questions people ask, and look at what comes back. A missing answer is usually a missing heading or a document that never said it.
Errors#
The routes under /api/v1/collections answer errors as:
{ "status": "error", "code": "collection_not_found", "message": "No such collection." }Switch on the status and on code; the text is for people. A validation failure is the usual Laravel 422 with message and errors.
| Status | code | When |
|---|---|---|
| 401 | the key is not valid, or its account has no API access | |
| 403 | api_not_allowed | a session token whose account group has no API access. A key in that situation gets 401 |
| 403 | ability_insuficiente | a narrow token. The field is codigo here |
| 404 | collection_not_found, document_not_found | the id is not yours or does not exist |
| 413 | document_too_large | the text passes the size of one document |
| 422 | collection_limit | you already have the most collections an account may hold |
| 422 | collection_full | the collection already has the most documents it may hold |
| 422 | account_full | the document would take your collections past the most pieces an account may hold |
| 422 | empty_document | the text has nothing in it |
| 429 | a request limit was reached. See rate limits and quotas | |
| 503 | embedding_unavailable | the embedding service could not be reached, so nothing was stored. Try again |