> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zelto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Crear casos con la API

> Agrega casos a un benchmark desde tu código con la API REST, o desde Claude, Cursor y otros clientes con MCP.

> [!NOTE]
>
> [Benchmarks](https://docs.zelto.ai/es/docs/benchmarks.md) está en vista previa y se habilita para las
> organizaciones de forma gradual. Los endpoints y herramientas de esta página
> están disponibles cuando Benchmarks está habilitado para tu organización; hasta
> entonces responden `403 not_entitled`.

Todo lo que hace el menú **Agregar casos** del panel lo puedes hacer con la API
REST o con [MCP](https://docs.zelto.ai/es/docs/mcp.md): escribir casos a mano, convertir
[conversaciones](https://docs.zelto.ai/es/docs/conversations.md) o [trazas](https://docs.zelto.ai/es/docs/traces.md) de
producción en casos de forma masiva, y adjuntar expectativas de llamadas a
herramientas y ajustes de jueces. Ambas superficies usan el mismo servicio que el
panel, así que un caso creado por API es idéntico a uno creado en la interfaz.

## Qué es un caso

Un caso es una entrada más su resultado esperado. Cada caso tiene un `caseType`:

| `caseType` | Benchmark | Entrada | Resultado esperado |
| - | - | - | - |
| `turn` | LLM | La conversación hasta ese punto (mensajes, llamadas a herramientas anteriores y sus resultados), el prompt contra el que se ejecuta y las variables de la llamada. El candidato produce **una generación**: una respuesta, llamadas a herramientas o ambas. | Una respuesta de referencia, indicaciones de lo que debió decir en su lugar y/o una [expectativa de llamadas a herramientas](#expectativas-de-llamadas-a-herramientas). |
| `conversation` | LLM | Un llamante simulado (persona, objetivo, frase inicial), un límite de turnos y simulaciones de herramientas. El candidato habla con el llamante hasta que la conversación termina. Las herramientas se simulan, nunca se ejecutan. | Notas de rúbrica, si se alcanzó el objetivo, recorrido de nodos y herramientas que deben o no deben llamarse. |
| `audio` | STT | Un clip de audio. | La transcripción de referencia, más términos clave opcionales que deben transcribirse exactamente. |

Un caso `turn` no es dueño de un prompt. Apunta a una
[versión del agente](https://docs.zelto.ai/es/docs/agent-versions.md) (`prompt.agentVersionId`), así que
el mismo caso puede reproducirse contra varias revisiones de prompt y modelos.

## Autenticación

Usa una [clave de API](https://docs.zelto.ai/es/docs/settings.md#claves-de-api) de la organización como
token bearer. La misma clave sirve para REST y para
[MCP](https://docs.zelto.ai/es/docs/mcp.md#autenticación). Crear y editar casos es una escritura: la
clave debe pertenecer a un **propietario o administrador** de la organización;
si no, la solicitud se rechaza con `403`.

```bash theme={null}
export ZELTO_API_KEY="zk_..."
export BENCHMARK_ID="..."   # de GET /v1/benchmarks o de la URL del benchmark
```

Los casos solo pueden referirse a agentes asignados al benchmark. Para agregar un
caso de otro agente, asigna primero ese agente al benchmark.

## Crear casos

`POST /v1/benchmarks/{id}/cases` crea hasta **100 casos por solicitud** y los
devuelve en `data`.

```bash theme={null}
curl -X POST "https://api.zelto.ai/v1/benchmarks/$BENCHMARK_ID/cases" \
  -H "Authorization: Bearer $ZELTO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: import-2026-10-31-batch-1" \
  -d @cases.json
```

```json title="cases.json" theme={null}
{
  "cases": [
    {
      "caseType": "turn",
      "input": {
        "prompt": { "agentVersionId": "6f1c2a9e-..." },
        "variables": { "customer_name": "Jordan Reyes", "balance": "$240.00" },
        "messages": [
          { "role": "assistant", "content": "Hola, habla Ana de Acme. ¿Hablo con Jordan?" },
          { "role": "user", "content": "Sí. Este mes no puedo pagar el saldo completo." }
        ]
      },
      "expected": {
        "replyExpectation": {
          "check": "different",
          "text": "Ofrecer un plan de pagos antes de mencionar recargos por mora.",
          "idealReply": "Entiendo. Podemos dividir los $240 en tres pagos mensuales. ¿Le sirve?"
        },
        "mustNotContain": ["recargo"]
      },
      "tags": ["payment-plan", "hardship"]
    }
  ]
}
```

```json Respuesta (201) theme={null}
{
  "data": [
    {
      "id": "3f9a6c1e-...",
      "version": 1,
      "status": "ready",
      "source": "api",
      "sourceConversationId": null,
      "input": { "...": "..." },
      "expected": { "...": "..." },
      "groupId": null,
      "tags": ["payment-plan", "hardship"],
      "autoTags": ["source:api"]
    }
  ]
}
```

Envía un encabezado `Idempotency-Key` en la creación por lotes para que una
solicitud reintentada devuelva los casos que creó la primera vez en lugar de
crear duplicados.

### Campos del caso

| Campo | Aplica a | Descripción |
| - | - | - |
| `caseType` | todos los casos LLM | `turn` o `conversation`. Los casos STT lo omiten y se devuelven como `audio`. |
| `input.prompt` | `turn`, `conversation` | `{ "agentVersionId": "<uuid>" }`: el prompt contra el que se ejecuta el caso cuando un candidato usa el prompt propio del caso. |
| `input.messages` | `turn` | El historial antes del paso evaluado. Cada mensaje tiene un `role` (`user`, `assistant` o `tool`) y `content`; un mensaje de herramienta lleva `toolCall: { name, arguments, result }`. |
| `input.variables` | `turn`, `conversation` | Valores para los marcadores `{{variable}}` del prompt. Si el prompt necesita una variable que el caso no aporta, ese resultado falla de forma explícita en lugar de enviar el marcador sin reemplazar. |
| `input.activeNodeId` | `turn` | Para agentes basados en nodos: el nodo en el que está la conversación en este paso. |
| `input.persona` | `conversation` | `{ role, goal, traits?, knowledge?, openingLine?, firstSpeaker? }` del llamante simulado. `firstSpeaker` es `agent` o `caller`. |
| `input.maxTurns` | `conversation` | Límite de turnos, hasta 30 (12 por defecto). |
| `input.toolMocks` | `conversation` | Resultados predefinidos para llamadas a herramientas: `{ tool, match?, response, once? }`. `match` usa los [comparadores de argumentos](#comparadores-de-argumentos). |
| `input.onUnmatchedTool` | `conversation` | `error` (por defecto) hace fallar el resultado cuando el agente llama a una herramienta sin simulación que coincida; `generic_mock` devuelve `{"ok": true}`. |
| `input.startNodeId`, `input.prefix` | `conversation` | Nodo inicial opcional y mensajes opcionales desde los que empezar (por ejemplo, los primeros turnos de una llamada real). |
| `expected.referenceAnswer` | `turn` | La respuesta de referencia. |
| `expected.replyExpectation` | `turn` | Cómo se califica la respuesta: `check` es `reference` (comparar con la respuesta de referencia), `different` (se califica contra `text`, lo que el agente debió hacer, con un `idealReply` opcional) o `none` (los jueces de respuesta no aplican a este caso). |
| `expected.toolExpectation` | `turn` | Qué llamadas a herramientas debe o no debe hacer el paso. Consulta [Expectativas de llamadas a herramientas](#expectativas-de-llamadas-a-herramientas). |
| `expected.rubricNotes` | LLM | Indicaciones específicas del caso para los jueces LLM. |
| `expected.mustContain`, `expected.mustNotContain` | `turn` | Frases que la respuesta debe incluir o evitar. |
| `expected.expectedTransition` | `turn` | Para agentes basados en nodos: `{ "toNodeId": "..." }` o `{ "stay": true }`. |
| `expected.goalReached`, `pathIncludes`, `pathExcludes`, `toolCalled`, `toolNotCalled` | `conversation` | Verificaciones del resultado de la conversación. |
| `skippedJudgeIds` | todos | Jueces que se omiten en este caso. Consulta [Omitir o ajustar un juez](#omitir-o-ajustar-un-juez-para-un-caso). |
| `judgeParamOverrides` | todos | Valores por caso para los parámetros de un juez. |
| `variableOverrides` | casos de producción | Reemplaza una variable capturada en este caso: `{ "<name>": { "value": "...", "kind": "edited" \| "placeholder" } }`. Solo se aceptan nombres que el caso capturó. |
| `groupId` | todos | El grupo al que pertenece el caso (como máximo uno). |
| `tags` | todos | Etiquetas libres para segmentar resultados. Nunca afectan la puntuación. |

Un caso `turn` debe verificar al menos algo: una expectativa de respuesta distinta
de `none`, o una expectativa de herramientas.

### Expectativas de llamadas a herramientas

`toolExpectation` verifica si el paso llamó a la herramienta correcta y si la
llamada tiene la forma correcta. Elige una de tres formas:

| `expect` | Pasa cuando |
| - | - |
| `call` | El paso hace las `calls` indicadas (en orden si `ordered` es `true`) y ninguna de las `forbiddenTools`. |
| `no_call` | El paso responde directamente y no llama a ninguna herramienta. |
| `any` | El paso puede llamar herramientas o no, pero nunca una de las `forbiddenTools`. |

Este caso espera que se cree un plan de pagos de tres cuotas, en la moneda del
cliente, y nunca un reembolso:

```json theme={null}
{
  "expected": {
    "replyExpectation": { "check": "none" },
    "toolExpectation": {
      "expect": "call",
      "ordered": true,
      "calls": [
        {
          "tool": "create_payment_plan",
          "arguments": {
            "installments": { "op": "equals", "value": 3 },
            "currency": { "op": "one_of", "values": ["USD", "usd"] },
            "first_payment_date": { "op": "semantic", "description": "una fecha dentro de los próximos 7 días" },
            "notes": { "op": "ignore" }
          }
        }
      ],
      "forbiddenTools": ["issue_refund"],
      "noExtraArguments": false
    }
  }
}
```

Solo se verifican los argumentos que indicas; usa `noExtraArguments: true` para
que fallen las llamadas que envían cualquier otro argumento. Cuando un paso habla y
además llama herramientas, `replyAt` (con `ordered: true`) fija dónde va la
respuesta entre las llamadas: `0` significa antes de todas. Cada `tool` indicada
debe existir en el prompt del caso.

#### Comparadores de argumentos

| `op` | Pasa cuando |
| - | - |
| `equals` | El valor es igual en profundidad a `value`. |
| `one_of` | El valor es igual a uno de `values`. |
| `regex` | El valor coincide con `pattern` (sintaxis RE2). |
| `number_range` | El número está entre `min` y `max` (ambos opcionales). |
| `present` | Cualquier valor no nulo. |
| `semantic` | Un LLM juzga que el valor se ajusta a `description`, por ejemplo "significa el próximo viernes". Es el único comparador con costo de juez. |
| `ignore` | Nunca se verifica. |

Además de los comparadores, los argumentos de cada llamada deben validar contra el
JSON schema de la herramienta: `"amount": "240"` falla en un argumento
`{ "type": "number" }`.

### Omitir o ajustar un juez para un caso

Los jueces se definen en el benchmark. Obtén sus ids con
`GET /v1/benchmarks/{id}` (`validators[]`); la API llama a los jueces
"validators".

* **`skippedJudgeIds`** excluye este caso de jueces que no tienen sentido para
  él, como una rúbrica de tono en un paso que solo llama herramientas. Los jueces
  obligatorios no se pueden omitir, y al menos un juez debe seguir ejecutándose.
* **`judgeParamOverrides`** cambia los parámetros de un juez solo para este caso.
  Un juez con la etiqueta "Respuesta de menos de {maxWords} palabras" y un valor
  por defecto de 60 puede ajustarse a 10 para un caso:

```json theme={null}
{
  "skippedJudgeIds": ["<tone-judge-id>"],
  "judgeParamOverrides": {
    "<length-judge-id>": { "maxWords": 10 }
  }
}
```

Cada id de juez y clave de parámetro debe existir en el benchmark, y los valores
deben coincidir con el tipo del parámetro y respetar su `min` / `max`. Un ajuste
para un juez que también omites se descarta.

### Casos de conversación

```json theme={null}
{
  "caseType": "conversation",
  "input": {
    "prompt": { "agentVersionId": "6f1c2a9e-..." },
    "persona": {
      "role": "Cliente que se atrasó en dos pagos",
      "goal": "Conseguir dividir el saldo en pagos más pequeños",
      "traits": "Amable pero firme; cuelga si lo presionan",
      "firstSpeaker": "agent"
    },
    "maxTurns": 12,
    "variables": { "customer_name": "Jordan Reyes", "balance": "$240.00" },
    "toolMocks": [
      { "tool": "get_balance", "response": { "balance": 240, "currency": "USD" } },
      {
        "tool": "create_payment_plan",
        "match": { "installments": { "op": "number_range", "min": 2, "max": 6 } },
        "response": { "ok": true, "planId": "pp_123" }
      }
    ],
    "onUnmatchedTool": "error"
  },
  "expected": {
    "rubricNotes": "El agente ofrece un plan antes de mencionar recargos y confirma la fecha del primer pago.",
    "goalReached": true,
    "toolNotCalled": { "tool": "issue_refund", "arguments": {} }
  }
}
```

### Casos de audio (STT)

En un benchmark STT, cada caso es `{ audio, referenceTranscript, keyTerms?, tags? }`.
El audio nunca pasa por el cuerpo de la solicitud, salvo clips cortos:

1. Prepara el archivo con
   [`POST /v1/recording-uploads`](https://docs.zelto.ai/es/api-reference/conversations/create-a-direct-recording-upload)
   y luego haz `PUT` de los bytes a la `uploadUrl` devuelta.
2. Crea el caso con `"audio": { "uploadId": "<id>" }`.

También puedes enviar `"audio": { "url": "https://..." }` (Zelto lo descarga; el
caso queda `preparing` hasta que termina la descarga) o, para clips de hasta
3 MB, `"audio": { "base64": "...", "contentType": "audio/wav" }`.

```json theme={null}
{
  "cases": [
    {
      "audio": { "uploadId": "<upload-id>" },
      "referenceTranscript": "Mi número de cuenta es 4 4 7 1 9 y quiero cancelar.",
      "keyTerms": ["44719"],
      "tags": ["noisy", "lang:es"]
    }
  ]
}
```

## Agregar casos desde llamadas de producción

`POST /v1/benchmarks/{id}/cases/from-conversations` convierte hasta **100**
[conversaciones](https://docs.zelto.ai/es/docs/conversations.md) en casos en una sola solicitud, con el
mismo mapeo que **Desde llamadas de producción** en el panel. Cada caso registra
la versión del agente de la llamada, los valores de variables capturados de la
llamada, el nodo activo (en agentes basados en nodos) y la transcripción previa al
turno evaluado.

```bash theme={null}
curl -X POST "https://api.zelto.ai/v1/benchmarks/$BENCHMARK_ID/cases/from-conversations" \
  -H "Authorization: Bearer $ZELTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversationIds": ["0b6e...", "1c7f...", "2d80..."],
    "turn": "last_assistant"
  }'
```

| Campo | Descripción |
| - | - |
| `conversationIds` | Hasta 100 ids de conversación. Encuéntralos con [`GET /v1/conversations`](https://docs.zelto.ai/es/api-reference/conversations/list-conversations). Cada llamada debe pertenecer a un agente asignado al benchmark. |
| `turn` | LLM: `last_assistant` (por defecto) o un número de turno. |
| `as` | LLM: `turn` (por defecto) o `conversation`. `conversation` redacta una persona, un objetivo y simulaciones de herramientas a partir de cada llamada y los devuelve como casos `draft` para que los revises. |
| `trim` | STT: `{ "startSeconds": 12, "endSeconds": 31 }` para recortar la grabación a una sola intervención. La grabación se copia al benchmark al crear el caso, y la referencia se rellena con la transcripción de la llamada y se marca como no verificada. |

Las variables capturadas aparecen enmascaradas en la respuesta. Los casos cuyo
nodo no se pudo resolver vuelven como `draft`. Usa
`PATCH /v1/benchmarks/{id}/cases/{caseId}` para afinar cualquier caso después:
cambiar su resultado esperado, agregar una expectativa de herramientas, reemplazar
una variable capturada con `variableOverrides`, o enviar `"status": "ready"` para
aceptar un borrador.

## Agregar casos desde trazas

`POST /v1/benchmarks/{id}/cases/from-traces` crea hasta **100** casos a partir de
[trazas](https://docs.zelto.ai/es/docs/traces.md), igual que **Agregar al benchmark** en una traza o un
span.

```bash theme={null}
curl -X POST "https://api.zelto.ai/v1/benchmarks/$BENCHMARK_ID/cases/from-traces" \
  -H "Authorization: Bearer $ZELTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "00f067aa0ba902b7" },
      { "traceId": "a1b2c3d4e5f60718293a4b5c6d7e8f90" }
    ]
  }'
```

* **Con un `spanId`** (un span de LLM o de herramienta), el caso prueba esa única
  llamada al LLM: los mensajes de entrada del span se convierten en el historial,
  y su salida en el paso esperado.
* **Sin `spanId`**, se usa la
  [conversación vinculada](https://docs.zelto.ai/es/docs/traces.md#vincular-trazas-a-conversaciones) de la
  traza, como `from-conversations` con `turn: "last_assistant"`.

Cada caso creado informa `turnSource`: `span_messages` cuando el historial vino del
span, o `transcript` cuando vino de la llamada vinculada. Una traza sin llamada
vinculada necesita un span que haya registrado sus mensajes; si no, ese elemento
falla con `422 no_turn_source`.

## Con MCP

El [servidor MCP de Zelto](https://docs.zelto.ai/es/docs/mcp.md) expone las mismas operaciones como
herramientas, así que puedes pedirle a Claude, Cursor o cualquier cliente MCP que
construya un benchmark por ti:

| Herramienta | Qué hace | Límite por llamada |
| - | - | - |
| `add_benchmark_cases` | Crea casos `turn` o `conversation` (misma forma que `POST …/cases`, más `groupId` y `tags`). | 50 |
| `add_benchmark_cases_from_conversations` | Convierte llamadas de producción en casos. | 50 |
| `add_benchmark_cases_from_traces` | Convierte trazas o spans en casos (elementos `{ traceId, spanId? }`). | 50 |
| `create_benchmark_audio_upload` | Obtiene una URL de carga prefirmada (y una línea `curl -T` lista) para un caso de audio. | — |
| `update_benchmark_case`, `retire_benchmark_case` | Edita un caso (una nueva revisión en el borrador) o lo retira. | — |
| `list_benchmarks`, `get_benchmark`, `list_benchmark_cases` | Encuentra benchmarks, sus jueces y los casos existentes. | — |
| `publish_benchmark_version` | Publica el borrador. | — |

Un prompt de ejemplo:

```text theme={null}
En Zelto, busca el benchmark "Collections next-turn" y agrega un caso por
cada una de las últimas 30 llamadas fallidas del agente Collections, usando el
último turno del asistente. En las llamadas donde el agente debió ofrecer un plan
de pagos, espera una llamada a create_payment_plan con entre 2 y 6 cuotas, y
nunca issue_refund. Etiquétalos "payment-plan". Muéstrame el borrador antes de
publicar.
```

El cliente llama a `list_benchmarks`, `list_conversations`,
`add_benchmark_cases_from_conversations` y luego a `update_benchmark_case` para
las expectativas de herramientas.

## Dónde quedan los casos nuevos

Los casos nuevos y editados van al **borrador** del benchmark, la copia de
trabajo que el panel muestra como "Cambios en borrador". Las ejecuciones usan por
defecto la última versión **publicada**, así que los casos nuevos entran en una
ejecución solo después de publicar:

```bash theme={null}
curl -X POST "https://api.zelto.ai/v1/benchmarks/$BENCHMARK_ID/versions" \
  -H "Authorization: Bearer $ZELTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "note": "Agregar 30 casos de plan de pagos de llamadas de octubre" }'
```

Publicar excluye los casos que siguen en estado `draft` y te avisa de ellos. Los
propietarios y administradores también pueden ejecutar el borrador directamente;
esa ejecución se marca como "ejecución de borrador". Los casos nunca se eliminan:
editar uno crea una nueva revisión, y retirarlo lo quita de la siguiente versión
mientras las ejecuciones anteriores conservan sus resultados.

## Errores

Los errores son JSON: `{ "error": "<code>", "details": ... }`.

| Estado | Cuándo |
| - | - |
| `400` | El cuerpo no es válido. `details` enumera los problemas, por ejemplo: una herramienta de una expectativa que no está en el prompt del caso, `expect: "call"` sin llamadas, una regex inválida, un caso sin nada que verificar, un juez desconocido u obligatorio en `skippedJudgeIds` (u omitir todos los jueces), un parámetro desconocido o un valor fuera de rango en `judgeParamOverrides`, un nombre en `variableOverrides` que el caso no capturó, más casos que el límite por solicitud, o un caso o conversación cuyo agente no está asignado al benchmark (`agent_not_assigned`; asigna primero el agente). |
| `403` | El dueño de la clave no es propietario ni administrador de la organización, o Benchmarks no está habilitado para tu organización (`not_entitled`). |
| `404` | El benchmark, la conversación, la traza o la versión del agente no está en tu organización. |
| `409` | Un conflicto con el estado actual del benchmark, por ejemplo publicar sin cambios en el borrador (`no_draft_changes`). |
| `422 no_turn_source` | Una traza sin llamada vinculada y sin mensajes de span con los que construir un caso. |

Por MCP, los mismos rechazos llegan como resultados de herramienta con
`isError: true`; consulta [Resultados de error](https://docs.zelto.ai/es/docs/mcp.md#resultados-de-error).

## Contenido relacionado

* [Benchmarks](https://docs.zelto.ai/es/docs/benchmarks.md): casos, jueces y versiones.
* [Versiones de agentes](https://docs.zelto.ai/es/docs/agent-versions.md): a qué apunta `prompt.agentVersionId`.
* [MCP](https://docs.zelto.ai/es/docs/mcp.md): conectar un cliente y autenticarse.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.