Give the agent something to quote
Upload files or write sources so the agent can quote them.
POST /kb-files uploads a document into the workspace library, and POST /kb/sources saves a hand-written entry. Both land in the same indexed text the agent searches and quotes from. Picking one over the other is a workflow call.
- Files (
/kb-files): a document you upload, or one generated from a web page. - Manual sources (
/kb/sources): a problem, its root cause and the solution steps.
Reading needs kb:read; writing, kb:write.
Uploading a file
curl -X POST https://api.vitrinadev.com/api/v1/kb-files \
-H "Authorization: Bearer $VITRINA_KEY" \
-F "[email protected]"{
"data": {
"id": "a6a6a6a6-0000-4000-8000-000000000001",
"name": "Warranty 2026.pdf",
"content_type": "application/pdf",
"size_bytes": 182304,
"status": "ready",
"agents": []
}
}The file lands in the workspace library, attached to no agent. Attaching is a separate step, with POST /ai-agents/{id}/knowledge.
status is the ingestion state, not an upload state. ready means the bytes landed, not yet that the file can be searched. Ingestion runs separately. If a file stays on ready and doesn't show up in searches, POST /kb-files/{id}/reingest retries it.
Cap: 25 MB per file.
Generating from a web page
To avoid re-typing what's already published on the workspace's own site:
curl -X POST https://api.vitrinadev.com/api/v1/kb-files/generate-from-url \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://www.mybusiness.com/warranty" }'{
"data": {
"title": "Warranty — My Business",
"markdown": "# Warranty\n\nEvery new vehicle includes a 3-year / 100,000 km factory warranty.",
"source_url": "https://www.mybusiness.com/warranty",
"page_count": 1
}
}The result comes back; it isn't stored. Nothing is searchable yet. It's the draft somebody has to review before the agent quotes it to a customer. Saving it is an ordinary POST /kb-files, with the markdown as its content.
With crawl: true and max_pages it crawls several pages in one call, but the call takes as long as the crawl and the LLM pass do. For a large site, use the asynchronous flow:
curl -X POST https://api.vitrinadev.com/api/v1/kb-files/crawl/start \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://www.mybusiness.com", "max_pages": 10 }'It answers with a job_id right away. Keep it: it's the only credential on that crawl. GET /kb-files/crawl/{jobId} reports progress. Poll done there rather than status: done is true for every terminal state, including a failed one. POST /kb-files/crawl/{jobId}/generate consolidates what was crawled into one document, same shape as the synchronous version.
Replacing instead of delete-and-reupload
When a document that's already attached to agents changes content, replacing it keeps its id. With the id it keeps every attachment and every agent version that references it:
curl -X PUT https://api.vitrinadev.com/api/v1/kb-files/a6a6a6a6-0000-4000-8000-000000000001/content \
-H "Authorization: Bearer $VITRINA_KEY" \
-F "[email protected]"The previous chunks are purged in the same transaction as the swap. The agent never quotes the superseded text while the new ingestion runs.
Deleting
curl -X DELETE https://api.vitrinadev.com/api/v1/kb-files/a6a6a6a6-0000-4000-8000-000000000001 \
-H "Authorization: Bearer $VITRINA_KEY"The delete is soft. The file leaves the library, detaches from every agent and has its chunks purged. Restoring an agent version that used it brings it back.
To remove a file from one agent without touching the others, use DELETE /ai-agents/{id}/knowledge/{fileId}.
Manual sources
For an answer that lives in no document, something the team knows and wants the agent to know too:
curl -X POST https://api.vitrinadev.com/api/v1/kb/sources \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"summary": {
"title": "Opening hours",
"problem": "The customer asks about opening hours",
"solution_steps": [
"Confirm the hours: Monday to Friday, 9am to 7pm",
"Offer to book an appointment if it applies"
]
},
"tags": ["hours", "front-desk"]
}'It's born status: "pending" and with no chunks, so creating it doesn't make it searchable. PUT /kb/sources/{id} is the review step, for example moving it to status: "approved". Neither call embeds it on its own:
curl -X POST https://api.vitrinadev.com/api/v1/kb/sources/a5a5a5a5-0000-4000-8000-000000000001/embed \
-H "Authorization: Bearer $VITRINA_KEY"That's the call that makes it searchable. Calling it again is safe: it deletes the source's previous chunks before inserting the new ones, so a source never doubles up in retrieval.
POST /kb/sources/{id}/revoke is the reversible alternative to deleting. The row and its chunks survive, but the source drops out of the default list (?status=active). It comes back once it's re-approved and re-embedded. DELETE /kb/sources/{id} is final: the row and its chunks are gone, with no way back.
Events
| Event | When |
|---|---|
kb_file.uploaded | A document was added to the library |
kb_file.deleted | A document was deleted (soft delete) |
kb.source.create | A manual source was created |
kb.source.update | A source's summary, tags or status changed |
kb.source.delete | A source was deleted (hard delete) |
kb.source.embed | A source was (re-)embedded and its vectors are ready |
There's no update event for a file. Replacing its bytes keeps its id and changes no state a subscriber would act on differently.