Base de conocimiento — archivos
La biblioteca de documentos subidos del workspace: uno se sube o se genera desde una URL, y se adjunta a uno o más agentes.
Descarga la proyección completa de la API pública: openapi.json.
Alimentar la base de conocimiento explica este recurso en prosa, con ejemplos ejecutables.
| Método | Ruta | Qué hace |
|---|---|---|
GET | /kb-files | List the workspace’s knowledge files |
POST | /kb-files | Upload a knowledge file |
DELETE | /kb-files/{id} | Delete a knowledge file (SOFT; keeps the stored object) |
GET | /kb-files/{id}/content | Download the raw file |
PUT | /kb-files/{id}/content | Replace a knowledge file with a new version |
POST | /kb-files/{id}/reingest | Re-chunk and re-embed a file |
GET | /kb-files/crawl/{jobId} | Crawl progress |
POST | /kb-files/crawl/{jobId}/generate | Turn a finished crawl into a document |
POST | /kb-files/crawl/start | Start an asynchronous site crawl |
POST | /kb-files/generate-from-url | Draft a document from a URL (synchronous) |
GET /kb-files
List the workspace’s knowledge files
Every file with its ingestion status, size and content type, plus the agents each one is attached to — so this single call answers both "what do we have" and "who can see it" without a per-file lookup. Unpaginated, with a total.
status is the INGESTION state, not an upload state: a file is ready the moment its bytes land, before it has been chunked or embedded. A ready file is not necessarily retrievable by the agent yet.
curl https://api.vitrinadev.com/api/v1/kb-files \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "a6a6a6a6-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "Garantías 2026.pdf",
"content_type": "application/pdf",
"size_bytes": 182304,
"status": "ingested",
"agents": [
{
"id": "a3a3a3a3-0000-4000-8000-000000000001",
"name": "Asistente de ventas"
}
],
"created_at": "2026-09-01T13:00:00.000Z",
"updated_at": "2026-09-01T13:05:00.000Z",
"deleted_at": null
}
],
"meta": {
"total": 1
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /kb-files
Upload a knowledge file
A multipart/form-data request with the document in a field named file — not a JSON body. The filename and content type come from the part itself. Capped at 25 MB; an empty file and a missing part are both 400s.
Storage and the database row are written together — if the row fails the stored object is removed, so there are no orphans. Ingestion is then enqueued best-effort: a 201 means the file is stored, not that it is searchable. Poll status on the list, and use /\{id\}/reingest if it never leaves ready.
Uploading here puts the file in the library and attaches it to NO agent. To upload straight onto an agent, post to /ai-agents/\{id\}/knowledge instead. Answers 201.
curl -X POST https://api.vitrinadev.com/api/v1/kb-files \
-H "Authorization: Bearer $VITRINA_KEY" \
-F "file=@/path/to/file.png"Ejemplo de respuesta (201)
{
"data": {
"id": "a6a6a6a6-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "Garantías 2026.pdf",
"content_type": "application/pdf",
"size_bytes": 182304,
"status": "ready",
"agents": [
{
"id": "a3a3a3a3-0000-4000-8000-000000000001",
"name": "Asistente de ventas"
}
],
"created_at": "2026-09-01T13:00:00.000Z",
"updated_at": "2026-09-01T13:05:00.000Z",
"deleted_at": null
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /kb-files/{id}
Delete a knowledge file (SOFT; keeps the stored object)
Removes the document from the workspace: it leaves the library, every agent attachment goes, and the embedded chunks are purged so retrieval can no longer return it. This is the "wrong file" action — to stop ONE agent using a file that others still need, detach it with DELETE /ai-agents/\{id\}/knowledge/\{fileId\}.
ADR 0093 — it is a soft delete: the kb_file row survives with status: "deleted" + deleted_at, and the stored object is kept. Every ai_agent_version references its files by id, so hard-deleting used to leave the version viewer unable to name what the agent knew and made the next publish of a draft restored from that version fail on the foreign key. Restoring such a version now un-deletes the file and re-ingests it from the kept object. GET /kb-files/\{id\}/content keeps serving a deleted file for exactly that viewer.
Always 204, including for an id that does not exist or belongs to another workspace, so this endpoint cannot be used to probe for ids. It is not a confirmation that something was deleted.
Chunk removal is best-effort and logged on failure; the row is tombstoned either way.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl -X DELETE https://api.vitrinadev.com/api/v1/kb-files/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429
GET /kb-files/{id}/content
Download the raw file
The original bytes, with the stored Content-Type and an inline Content-Disposition so a browser previews text, markdown and PDFs rather than downloading them. This is not the \{ data \} envelope.
A file whose row exists but whose stored object has gone answers a clean 404 naming that case, rather than a 500 — worth distinguishing in a UI, since it means the row should be deleted and the file re-uploaded.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/kb-files/<id>/content \
-H "Authorization: Bearer $VITRINA_KEY"Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /kb-files/{id}/content
Replace a knowledge file with a new version
A multipart/form-data request with the new document in a field named file, like the upload. The file keeps its id: every agent it is attached to keeps seeing it, and every published version that references it still resolves — this is the path for "the prices changed, here is the new PDF", where delete + upload would have meant re-attaching the document to each agent by hand.
The name, content type and size become those of the new part. Its previously indexed chunks are purged in the same transaction as the swap, so the agent never quotes the superseded text while the new ingestion runs; the row answers with status: ingesting and settles to ingested (or failed) like an upload. Same 25 MB cap. A deleted or foreign file is a 404.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl -X PUT https://api.vitrinadev.com/api/v1/kb-files/<id>/content \
-H "Authorization: Bearer $VITRINA_KEY" \
-F "file=@/path/to/file.png"Ejemplo de respuesta (200)
{
"data": {
"id": "a6a6a6a6-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "Garantías 2026.pdf",
"content_type": "application/pdf",
"size_bytes": 190112,
"status": "ingesting",
"agents": [
{
"id": "a3a3a3a3-0000-4000-8000-000000000001",
"name": "Asistente de ventas"
}
],
"created_at": "2026-09-01T13:00:00.000Z",
"updated_at": "2026-09-21T09:00:00.000Z",
"deleted_at": null
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /kb-files/{id}/reingest
Re-chunk and re-embed a file
Re-runs ingestion over the stored bytes — for a file whose ingestion failed, or to pick up a parser improvement. Returns the file row with its status reset; the actual work happens on the worker, so a 200 is an ACCEPTANCE and not a completed re-ingest.
Safe to repeat: the worker deletes this file’s chunks before inserting the new ones, so re-ingesting never doubles a document up in retrieval. Nothing is re-uploaded and the id does not change.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl -X POST https://api.vitrinadev.com/api/v1/kb-files/<id>/reingest \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"id": "a6a6a6a6-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "Garantías 2026.pdf",
"content_type": "application/pdf",
"size_bytes": 182304,
"status": "ingesting",
"agents": [
{
"id": "a3a3a3a3-0000-4000-8000-000000000001",
"name": "Asistente de ventas"
}
],
"created_at": "2026-09-01T13:00:00.000Z",
"updated_at": "2026-09-01T13:05:00.000Z",
"deleted_at": null
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /kb-files/crawl/{jobId}
Crawl progress
\{ status, done, pages_found, pages_succeeded, pages_failed, error, pages \}, where pages lists each URL with its title and HTTP status as it is discovered — enough to show live progress.
Poll done, not status. done is true for every terminal state, failures included, so a crawl that errored still reports done: true with error set. Treating a non-completed status as "keep waiting" polls forever.
The jobId is the only credential on this route. Crawl jobs live in the crawler service, which has no tenant column, so this endpoint checks that the caller holds a valid job id and not that the job belongs to their workspace. The id is a random uuid handed only to whoever started the crawl — treat it like a bearer token and keep it out of logs and shared URLs.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
jobId | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/kb-files/crawl/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"status": "completed",
"done": true,
"pages_found": 6,
"pages_succeeded": 6,
"pages_failed": 0,
"error": null,
"pages": [
{
"url": "https://www.midealer.cl",
"title": "Inicio",
"http_status": 200
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /kb-files/crawl/{jobId}/generate
Turn a finished crawl into a document
Consolidates the crawled pages into one markdown document, in the same shape /generate-from-url returns.
The document is returned, not stored — no kb_file row is created and nothing becomes searchable. This step exists so a human can read and edit what the crawler made of the site before it becomes something the agent will quote to a customer. Save it with the multipart POST /kb-files.
Usually instant: when the crawler’s completion webhook is configured, the document is generated as the crawl finishes and cached, and this call just serves it. Without that it generates live and takes as long as the LLM does. Either way the result is the same, so callers should not depend on the timing.
Call it once the crawl reports done. Calling it early consolidates only the pages fetched so far, which succeeds and quietly produces a partial document rather than failing.
The jobId is the only credential on this route. Crawl jobs live in the crawler service, which has no tenant column, so this endpoint checks that the caller holds a valid job id and not that the job belongs to their workspace. The id is a random uuid handed only to whoever started the crawl — treat it like a bearer token and keep it out of logs and shared URLs.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
jobId | path | uuid | sí |
curl -X POST https://api.vitrinadev.com/api/v1/kb-files/crawl/<id>/generate \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"title": "Garantía — Mi Dealer",
"markdown": "# Garantía\n\nTodos los vehículos nuevos incluyen 3 años o 100.000 km de garantía de fábrica.",
"source_url": "https://www.midealer.cl/garantia",
"page_count": 1
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /kb-files/crawl/start
Start an asynchronous site crawl
Kicks off a crawl of up to max_pages (max 25) and returns \{ job_id \} immediately. Poll GET /kb-files/crawl/\{jobId\} for progress, then call POST /kb-files/crawl/\{jobId\}/generate once it is done.
Keep the job_id: it is the ONLY handle on the crawl and there is no endpoint that lists your jobs. Losing it means starting again.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
url | string | sí | mín. 1 |
max_pages | integer | — | ≥ 1, ≤ 25 |
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.midealer.cl",
"max_pages": 10
}'Ejemplo de respuesta (200)
{
"data": {
"job_id": "b3b3b3b3-0000-4000-8000-000000000001"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /kb-files/generate-from-url
Draft a document from a URL (synchronous)
Fetches the page — or, with crawl: true, up to max_pages (max 25, default 1) — and has an LLM consolidate them into one markdown document. Answers \{ title, markdown, source_url, page_count \}.
The document is returned, not stored — no kb_file row is created and nothing becomes searchable. This step exists so a human can read and edit what the crawler made of the site before it becomes something the agent will quote to a customer. Save it with the multipart POST /kb-files.
This blocks for as long as the crawl and the LLM take, which for a multi-page site is tens of seconds. Use /crawl/start instead when you want progress to show, and keep this one for the single-page case.
A bare domain is accepted and normalised. A URL with no readable content is a 400 — a page that is entirely JavaScript often lands here.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
url | string | sí | mín. 1 |
crawl | boolean | — | |
max_pages | integer | — | ≥ 1, ≤ 25 |
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.midealer.cl/garantia",
"crawl": false
}'Ejemplo de respuesta (200)
{
"data": {
"title": "Garantía — Mi Dealer",
"markdown": "# Garantía\n\nTodos los vehículos nuevos incluyen 3 años o 100.000 km de garantía de fábrica.",
"source_url": "https://www.midealer.cl/garantia",
"page_count": 1
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
Base de conocimiento — fuentes y texto indexado
Las fuentes manuales del agente — un problema, su causa y los pasos de solución, redactados a mano en vez de subidos — y el texto indexado de un archivo de la biblioteca: literalmente lo que el agente busca.
Leads
Las oportunidades que el workspace está trabajando: abrirlas, moverlas por el tablero, cerrarlas ganadas, perdidas o no calificadas, y leer el embudo.