VitrinaAPI

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.

Beta
Puede cambiar en cualquier momento, con una entrada en el changelog y aviso a quienes la llamaron recientemente — ver versionado.

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étodoRutaQué hace
GET/kb-filesList the workspace’s knowledge files
POST/kb-filesUpload a knowledge file
DELETE/kb-files/{id}Delete a knowledge file (SOFT; keeps the stored object)
GET/kb-files/{id}/contentDownload the raw file
PUT/kb-files/{id}/contentReplace a knowledge file with a new version
POST/kb-files/{id}/reingestRe-chunk and re-embed a file
GET/kb-files/crawl/{jobId}Crawl progress
POST/kb-files/crawl/{jobId}/generateTurn a finished crawl into a document
POST/kb-files/crawl/startStart an asynchronous site crawl
POST/kb-files/generate-from-urlDraft 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ámetroEnTipoObligatorioRestricciones
idpathuuid
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ámetroEnTipoObligatorioRestricciones
idpathuuid
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ámetroEnTipoObligatorioRestricciones
idpathuuid
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ámetroEnTipoObligatorioRestricciones
idpathuuid
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ámetroEnTipoObligatorioRestricciones
jobIdpathuuid
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ámetroEnTipoObligatorioRestricciones
jobIdpathuuid
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

CampoTipoObligatorioRestricciones
urlstringmín. 1
max_pagesinteger≥ 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

CampoTipoObligatorioRestricciones
urlstringmín. 1
crawlboolean
max_pagesinteger≥ 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

En esta página