ShvIA

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#

  1. Create a collection: POST /api/v1/collections.
  2. Send the documents: a PUT for each one, to …/documents/{document id}. Sending a document again replaces it.
  3. Name the collection in a chat call: add "collections": [12] to the body of POST /v1/chat/completions, POST /v1/messages or POST /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#

WhatDefault
collections per account20
documents per collection300
text of one document1,000,000 bytes
pieces in all your collections3000
collections named in one chat call5
pieces that go into one prompt5

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#

bash
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" }'
json
{
  "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#

RouteWhat 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}#

bash
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"
  }'
json
{
  "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"
}
FieldNotes
textrequired. The document, as plain text or Markdown. Whitespace at the start and at the end is removed
titleoptional, up to 255 characters. It labels the document in the excerpts. It defaults to the document id

result says what happened:

resultStatusMeaning
created201a new document
updated200the text changed, so the old pieces were replaced by new ones
unchanged200the 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#

RouteWhat 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

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.

bash
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 }'
json
{
  "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:

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

  1. It takes the text to search by from your messages: the last user message. A short follow-up of fewer than 40 characters, such as "and on a Sunday?", is searched together with the user message before it.
  2. It retrieves up to 5 pieces from the named collections.
  3. 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.
  4. 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:

HeaderValue
X-Inference-Collectionsthe ids that were used, separated by commas
X-Inference-Excerptshow 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#

StatuscodeWhen
422invalid_collectionscollections is not a list of 1 to 5 positive integers
404collection_not_foundan id is not yours or does not exist
403collections_blocked_by_lgpdthe 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_locality in 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:

bash
#!/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)"'
done

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

python
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 /v1 endpoints 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 as model.
  • 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:

json
{ "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.

StatuscodeWhen
401the key is not valid, or its account has no API access
403api_not_alloweda session token whose account group has no API access. A key in that situation gets 401
403ability_insuficientea narrow token. The field is codigo here
404collection_not_found, document_not_foundthe id is not yours or does not exist
413document_too_largethe text passes the size of one document
422collection_limityou already have the most collections an account may hold
422collection_fullthe collection already has the most documents it may hold
422account_fullthe document would take your collections past the most pieces an account may hold
422empty_documentthe text has nothing in it
429a request limit was reached. See rate limits and quotas
503embedding_unavailablethe embedding service could not be reached, so nothing was stored. Try again