VitrinaAPI

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

EventWhen
kb_file.uploadedA document was added to the library
kb_file.deletedA document was deleted (soft delete)
kb.source.createA manual source was created
kb.source.updateA source's summary, tags or status changed
kb.source.deleteA source was deleted (hard delete)
kb.source.embedA 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.

On this page