Guía de Corpus

Corpus: de una clave a la primera consulta

Esta guía te lleva de un workspace vacío a una decisión resuelta, sin ayuda adicional. Recorre un mismo ejemplo de principio a fin: un experto llamado publication-review que decide cuántos revisores necesita un borrador antes de publicarse.

Necesitás una organización de Corpus con al menos un experto publicado y una versión de documento publicada. Si vas a montar eso vos mismo, hacelo primero en el workspace; esta guía arranca desde una clave.


1. Generá una clave

Todo lo que sigue se hace en el workspace, en corpus.trysenda.xyz, con sesión iniciada como administrador o propietario.

Generá una clave

Abrí Claves en la barra lateral de tu organización y generá una. Ponele un nombre que diga quién llama, no qué llama: "review-bot", no "query-expert". El registro de decisiones muestra ese nombre junto a cada llamada que hace la clave. Limitala al experto publication-review y a una etiqueta, para que la clave solo alcance lo que cae dentro de ambas.

La clave se muestra una sola vez, completa, justo después de generarla. Copiala antes de salir de la pantalla: después de eso el workspace solo muestra su prefijo (sk_corpus_...). Guardala como CORPUS_KEY en tu shell o en el gestor de secretos que ya usés en este entorno.


2. Una primera consulta, por curl

Toda llamada REST lleva Authorization: Bearer $CORPUS_KEY. La ruta base es https://corpus.trysenda.xyz/v1.

Listá los expertos que tu clave alcanza

curl "https://corpus.trysenda.xyz/v1/experts" \
  -H "Authorization: Bearer $CORPUS_KEY"

Esto también confirma que la clave funciona y devuelve la versión publicada actual del experto y su graphHash, los dos números a los que una traza fija su conclusión.

{
  "experts": [
    {
      "id": "5d9e2c1a-6f3b-4a7d-9e1c-2b3a4c5d6e7f",
      "name": "publication-review",
      "description": "How many reviewers a draft needs before it can be published.",
      "version": 4,
      "graphHash": "sha256:2f1a9c...",
      "tags": ["editorial"]
    }
  ]
}

Pedí reviewer_count sin todos los hechos

curl -X POST "https://corpus.trysenda.xyz/v1/experts/publication-review/query" \
  -H "Authorization: Bearer $CORPUS_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "goal": "reviewer_count",
    "facts": { "audience": "external" }
  }'

El experto necesita saber si el borrador contiene datos de un cliente antes de poder concluir. Solo suministraste audience, así que el resultado es needs_facts, y needs nombra exactamente lo que falta, tipado: un booleano llamado contains_customer_data.

No hay sesión que retomar. No repetís la misma llamada; llamás de nuevo con el hecho agregado.

{
  "decisionId": "3c6b1e2d-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
  "outcome": "needs_facts",
  "conclusion": null,
  "needs": [
    {
      "concept": "contains_customer_data",
      "type": { "kind": "boolean" },
      "description": "Whether the draft names or quotes a customer."
    }
  ],
  "reason": null,
  "certainty": null,
  "expert": {
    "id": "5d9e2c1a-6f3b-4a7d-9e1c-2b3a4c5d6e7f",
    "name": "publication-review",
    "version": 4,
    "graphHash": "sha256:2f1a9c..."
  },
  "trace": [
    {
      "id": "s1",
      "parentId": null,
      "kind": "goal",
      "concept": "reviewer_count",
      "provenance": { "kind": "caller", "field": "goal" },
      "expert": "publication-review",
      "expertVersion": 4
    },
    {
      "id": "s2",
      "parentId": "s1",
      "kind": "fact",
      "concept": "audience",
      "value": "external",
      "provenance": { "kind": "caller", "field": "audience" },
      "expert": "publication-review",
      "expertVersion": 4
    },
    {
      "id": "s3",
      "parentId": "s1",
      "kind": "unresolved",
      "concept": "contains_customer_data",
      "provenance": { "kind": "none" },
      "expert": "publication-review",
      "expertVersion": 4
    }
  ],
  "traceHash": "sha256:9b7e21...",
  "inputHash": "sha256:44ad10...",
  "cached": false
}

Preguntá de nuevo, con el hecho suministrado

curl -X POST "https://corpus.trysenda.xyz/v1/experts/publication-review/query" \
  -H "Authorization: Bearer $CORPUS_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "goal": "reviewer_count",
    "facts": {
      "audience": "external",
      "contains_customer_data": "true"
    }
  }'

Esta vez el resultado es resolved, conclusion es "3" (todo valor en el cable es una cadena; el tipo del concepto dice cómo leerla), y trace es la derivación completa: la meta, cada hecho, la regla que se disparó, y su último paso, un paso de documento que cita publication-policy.md en la versión 2. Esa es la cita que pide la especificación: la traza llega hasta el párrafo de la política en el que se apoyó la regla.

{
  "decisionId": "8f0f3b7a-9c2e-4d61-8a3f-1a2b3c4d5e6f",
  "outcome": "resolved",
  "conclusion": "3",
  "needs": [],
  "reason": null,
  "certainty": 1,
  "expert": {
    "id": "5d9e2c1a-6f3b-4a7d-9e1c-2b3a4c5d6e7f",
    "name": "publication-review",
    "version": 4,
    "graphHash": "sha256:2f1a9c..."
  },
  "trace": [
    {
      "id": "s1",
      "parentId": null,
      "kind": "goal",
      "concept": "reviewer_count",
      "provenance": { "kind": "caller", "field": "goal" },
      "expert": "publication-review",
      "expertVersion": 4
    },
    {
      "id": "s2",
      "parentId": "s1",
      "kind": "fact",
      "concept": "audience",
      "value": "external",
      "provenance": { "kind": "caller", "field": "audience" },
      "expert": "publication-review",
      "expertVersion": 4
    },
    {
      "id": "s3",
      "parentId": "s1",
      "kind": "fact",
      "concept": "contains_customer_data",
      "value": "true",
      "provenance": { "kind": "caller", "field": "contains_customer_data" },
      "expert": "publication-review",
      "expertVersion": 4
    },
    {
      "id": "s4",
      "parentId": "s1",
      "kind": "rule",
      "concept": "reviewer_count",
      "value": "3",
      "detail": "audience = external and contains_customer_data",
      "provenance": {
        "kind": "rule",
        "ruleId": "external-customer-data",
        "expert": "publication-review",
        "version": 4
      },
      "expert": "publication-review",
      "expertVersion": 4
    },
    {
      "id": "s5",
      "parentId": "s4",
      "kind": "document",
      "concept": "reviewer_count",
      "detail": "Section 3: drafts naming or quoting a customer need three reviewers.",
      "provenance": {
        "kind": "document",
        "documentId": "7a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
        "version": 2,
        "title": "publication-policy.md"
      },
      "expert": "publication-review",
      "expertVersion": 4
    }
  ],
  "traceHash": "sha256:6e12ab...",
  "inputHash": "sha256:c02fd7...",
  "cached": false
}

Hacé exactamente la misma pregunta otra vez

Misma versión del experto, mismos hechos: cached ahora es true, y traceHash no cambió. El motor no volvió a evaluar; devolvió la decisión ya registrada. traceHash es cómo lo notarías si lo hubiera hecho: una política cambiada o una regla cambiada cambia el hash aunque la conclusión no cambie.

{
  "decisionId": "8f0f3b7a-9c2e-4d61-8a3f-1a2b3c4d5e6f",
  "outcome": "resolved",
  "conclusion": "3",
  "needs": [],
  "reason": null,
  "certainty": 1,
  "expert": {
    "id": "5d9e2c1a-6f3b-4a7d-9e1c-2b3a4c5d6e7f",
    "name": "publication-review",
    "version": 4,
    "graphHash": "sha256:2f1a9c..."
  },
  "trace": [ /* identical to the call above */ ],
  "traceHash": "sha256:6e12ab...",
  "inputHash": "sha256:c02fd7...",
  "cached": true
}

Reproducí la decisión

curl -X POST "https://corpus.trysenda.xyz/v1/decisions/8f0f3b7a-9c2e-4d61-8a3f-1a2b3c4d5e6f/replay" \
  -H "Authorization: Bearer $CORPUS_KEY"

Replay vuelve a ejecutar decisionId contra la misma versión del experto y las mismas entradas, y reporta si la traza todavía coincide. matches: true aquí significa que nada se movió bajo esta decisión desde que quedó registrada. Un match en false nombraría el primer paso que divergió, y qué esperaba frente a qué encontró.

{
  "matches": true,
  "divergedAt": null,
  "expected": null,
  "actual": null,
  "trace": [ /* identical to the resolved call above */ ]
}

Listá los documentos y encontrá un id

curl "https://corpus.trysenda.xyz/v1/documents?title=policy&limit=2" \
  -H "Authorization: Bearer $CORPUS_KEY"

Cada documento tiene un id, y get_document, GET /v1/documents/{id}, sus versiones y su publicación lo toman. Listá los documentos publicados en el alcance de la clave con GET /v1/documents, ordenados por título. title conserva los que tienen ese texto en el título actual, sin distinguir mayúsculas, y limit fija el tamaño de la página: 50 por defecto, 200 como máximo.

next es el cursor de la página siguiente. Pasalo de vuelta como cursor y repetí hasta que next sea null; el cursor es opaco, así que no lo armes vos. Pasá q en lugar de paginar para buscar en los contenidos: una búsqueda está ordenada por relevancia, acotada por limit (20 por defecto), nunca se pagina, y cada resultado trae un snippet del contenido alrededor de la coincidencia.

{
  "documents": [
    {
      "id": "7a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
      "type": "policy",
      "language": "en",
      "tags": ["editorial"],
      "currentVersion": {
        "version": 2,
        "title": "publication-policy.md",
        "publishedAt": "2026-09-14T10:00:00.000Z"
      }
    },
    {
      "id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "type": "policy",
      "language": "en",
      "tags": ["editorial", "legal"],
      "currentVersion": {
        "version": 1,
        "title": "Retention policy",
        "publishedAt": "2026-09-02T08:30:00.000Z"
      }
    }
  ],
  "next": "WyJSZXRlbnRpb24gcG9saWN5IiwiOWM4YjdhNmQtNWU0Zi00YTNiLThjMmQtMWUwZjlhOGI3YzZkIl0"
}

create_document y ask_corpus toman nombres de etiquetas. GET /v1/tags lista las que están en el alcance de la clave, así no tenés que adivinarlas.

curl "https://corpus.trysenda.xyz/v1/tags" \
  -H "Authorization: Bearer $CORPUS_KEY"
{ "tags": [{ "name": "editorial" }, { "name": "legal" }] }

3. La misma consulta, desde Claude

Corpus expone un único endpoint MCP, https://corpus.trysenda.xyz/mcp, sobre Streamable HTTP, sin estado (sin sesiones). Toda llamada lleva la clave como cabecera bearer, la misma clave que generaste arriba.

Claude Code

Agregá el servidor una sola vez, desde un shell donde CORPUS_KEY esté definida:

claude mcp add --transport http corpus https://corpus.trysenda.xyz/mcp \
  --header "Authorization: Bearer $CORPUS_KEY"

El conector MCP de la Claude API

Pasá el servidor como una entrada mcp_servers en la solicitud, con la clave como su token de autorización. Revisá la referencia actual de la API para confirmar los nombres exactos de los campos antes de lanzar esto a producción; el conector todavía está evolucionando.

{
  "mcp_servers": [
    {
      "type": "url",
      "url": "https://corpus.trysenda.xyz/mcp",
      "name": "corpus",
      "authorization_token": "$CORPUS_KEY"
    }
  ]
}

Claude Desktop

La configuración de conectores remotos de Desktop (Settings, Connectors, agregar un conector personalizado) espera que la mayoría de los servidores entreguen acceso mediante OAuth. Corpus se autentica solo por clave de API en este hito; la cadena OAuth está planeada para uno posterior. Si el flujo de conectores de Desktop en tu versión no acepta una clave bearer directamente, usá Claude Code o el conector de la API de arriba, que sí lo hacen.

Las herramientas

Los nombres de las herramientas y sus esquemas de entrada se generan del mismo documento OpenAPI que la API REST, así que nunca se desalinean de ella.

tool
list_expertsLos expertos que la clave alcanza, con su versión publicada y su graphHash.
query_expertLa que más vas a usar: expert, goal y, opcionalmente, facts y question. El mismo sobre que POST /v1/experts/{name}/query.
replay_decisionVuelve a ejecutar una decisión registrada por decision_id y reporta si su traza todavía coincide.
search_documentsBusca o lista los documentos en el alcance de la clave. Con query, búsqueda de texto completo sobre las versiones actuales, filtrada por type o tag. Sin ella, los documentos publicados ordenados por título: title acota por un fragmento del título, limit fija el tamaño de la página (50 por defecto, 200 como máximo) y next en el resultado es el cursor que pasás como cursor para la página siguiente, null en la última. Cada resultado trae el id que toma get_document.
list_tagsLas etiquetas en el alcance de la clave: los nombres que create_document y ask_corpus toman como tags.
get_documentUn documento, en su versión actual o en una versión que indiques.
create_documentEscribe un documento nuevo. Cada etiqueta que lleve tiene que estar dentro del alcance de la clave.
{
  "name": "query_expert",
  "arguments": {
    "expert": "publication-review",
    "goal": "reviewer_count",
    "facts": { "audience": "external", "contains_customer_data": "true" }
  }
}

Qué significa needs_facts para el modelo

Cuando query_expert devuelve needs_facts, el modelo tiene un único movimiento correcto: leer needs, pedirle a la persona exactamente esos conceptos (nada más, nada adivinado), y llamar a query_expert de nuevo con ellos agregados. No hay una respuesta parcial que dar mientras tanto ni una conclusión que inferir a partir de los hechos ya conocidos; ese es todo el sentido de tener un motor determinista delante del modelo. Un modelo que completa por su cuenta un hecho faltante, o que reporta una conclusión a la que el motor nunca llegó, rompió la única garantía que Corpus existe para dar.


4. El contrato de rechazo

Toda llamada a query_expert, por REST o por MCP, devuelve uno de cuatro resultados. Bifurcá sobre outcome, nunca sobre el texto de reason o detail.

outcomesignificahacé esto
resolvedEl motor llegó a una conclusión. conclusion la contiene; trace es la derivación completa.Usá conclusion. Leé trace como evidencia; no vuelvas a preguntar.
needs_factsUna regla necesita un hecho que el motor no puede derivar y que no se suministró. needs los nombra a todos, tipados.Conseguí los hechos que faltan y llamá de nuevo con ellos agregados.
out_of_scopeLa meta no es un concepto de este experto, o la clave no tiene alcance sobre él.Revisá la meta y el alcance de la clave. No es un caso para reintentar.
errorLa llamada misma fue rechazada antes de empezar a evaluar: autenticación inválida, un cuerpo mal formado, un límite de tasa.Leé error.code y error.hint en el cuerpo de la respuesta (sección 5), no este sobre.

5. Errores

Una respuesta de error no lleva el sobre de la sección 4; es un cuerpo plano con un único objeto error: code, message, hint y traceId, el id que citar si pedís ayuda.

{
  "error": {
    "code": "rate_limited",
    "message": "This key has exceeded its rate limit.",
    "hint": "Wait for the window in Retry-After, then retry the same request.",
    "traceId": "b3c1e9f2-1a2b-4c3d-8e9f-0a1b2c3d4e5f"
  }
}

Idempotency-Key

Toda escritura (crear un documento, una versión nueva, una consulta, un replay) lleva una cabecera Idempotency-Key. Enviá la misma clave con el mismo cuerpo y recibís el resultado original, no una segunda escritura. Enviá la misma clave con un cuerpo distinto y la llamada falla con idempotency_key_reused: usá una clave nueva por solicitud distinta, y solo repite la misma clave en un reintento cuando el reintento es realmente idéntico.

codeestado HTTPsignificado
unauthenticated401Sin clave, o una clave que no se puede parsear. Revisá la cabecera Authorization.
revoked_key401La clave existió pero fue revocada. Generá una nueva.
rate_limited429Se superó el límite de tasa de la clave. Retry-After indica la espera.
not_found404El recurso no existe, o el alcance de la clave no llega a él. Los dos casos se ven iguales a propósito.
validation_failed422El cuerpo de la solicitud no coincide con el esquema. hint dice qué campo.
idempotency_key_reused422El Idempotency-Key ya se envió antes con un cuerpo distinto.
conflict409La escritura chocó con el estado actual, por ejemplo publicar sobre una versión que ya se movió.
quota_exceeded429La organización usó las decisiones, respuestas o páginas indexadas de su plan en el período (sección 6). Retry-After dice cuándo se reinicia la cuenta.
internal500Algo falló del lado de Corpus. Cita traceId si lo reportas.

6. Planes y límites

Todos los planes tienen todas las funciones; Basic, Pro y Enterprise cambian en cuánto incluyen de tres cosas por mes. No hay plan gratis. El mes corre desde el inicio de tu período de facturación.

Decisiones
Una llamada a query_expert, por REST o MCP, con cualquier resultado. Leer, buscar y reproducir no cuentan.
Respuestas
Una llamada a ask_corpus (o POST /v1/answers) por cada dos partes de su pregunta: una pregunta que el Corpus divide en tres subpreguntas cuenta dos. Se cuenta cuando la pregunta se entiende y antes de cualquier búsqueda. Una llamada que termina en needs_facts cuenta una decisión en su lugar.
Páginas indexadas
3.000 caracteres del texto de una versión publicada, archivos adjuntos incluidos, redondeado hacia arriba. Cada versión que publicás se indexa de nuevo, así que vuelve a contar. Un documento largo cuesta lo mismo por página que uno corto.
En el límite
Pasadas las decisiones o las respuestas, la llamada devuelve quota_exceeded (429) con Retry-After, y no se registra nada. Sin páginas, publicar devuelve quota_exceeded y la versión queda como borrador; una versión más larga que las páginas que quedan no se indexa hasta que la publiques de nuevo. El espacio de trabajo sigue funcionando y avisa al 80% y al 100%.
Enterprise
Cantidades y precio acordados con cada cliente, con contrato anual. Escribinos desde la página de precios.

Las cantidades y los precios de cada plan