VitrinaAPI

Agentes de IA

El agente que conversa por el workspace: su configuración en borrador y en vivo, versiones, herramientas y skills, el canal de voz, y las Solicitudes con las que un cliente pide un cambio.

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.

Configurar el agente que conversa explica este recurso en prosa, con ejemplos ejecutables.

MétodoRutaQué hace
GET/ai-agent-graphsList the graphs in the workspace
POST/ai-agent-graphsCreate a graph
DELETE/ai-agent-graphs/{id}Delete a graph
GET/ai-agent-graphs/{id}Fetch one graph with its nodes
PATCH/ai-agent-graphs/{id}Update a graph
DELETE/ai-agent-graphs/{id}/draftDiscard the draft
GET/ai-agent-graphs/{id}/draftFetch the pending draft
PUT/ai-agent-graphs/{id}/draftSave the draft
POST/ai-agent-graphs/{id}/nodesAdd a node to a graph
DELETE/ai-agent-graphs/{id}/nodes/{nodeId}Delete one node
PATCH/ai-agent-graphs/{id}/nodes/{nodeId}Update one node
POST/ai-agent-graphs/{id}/publishPublish the graph
GET/ai-agent-graphs/{id}/versionsList the published versions
POST/ai-agent-graphs/{id}/versions/{version}/restoreRestore a past version
GET/ai-agentsList AI agents
POST/ai-agentsCreate AI agent
DELETE/ai-agents/{id}Delete AI agent
GET/ai-agents/{id}Get AI agent
PUT/ai-agents/{id}Update AI agent
GET/ai-agents/{id}/change-request-statsOne agent's change-request ledger
GET/ai-agents/{id}/change-requestsList an agent's change requests
POST/ai-agents/{id}/change-requestsFile a change request (text or audio)
GET/ai-agents/{id}/change-requests/{crId}Get one change request
POST/ai-agents/{id}/change-requests/{crId}/ground«Buscar dónde pasó» — find the conversations a complaint is about
POST/ai-agents/{id}/change-requests/{crId}/ground/confirmConfirm (or reject) what the grounding run proposed
POST/ai-agents/{id}/change-requests/{crId}/propose«Proponer arreglo» — the investigator writes the fix from the red run
POST/ai-agents/{id}/change-requests/{crId}/scenario«Crear escenario» — author the eval from the request and run it
POST/ai-agents/{id}/change-requests/{crId}/scenario/evaluateRe-read the evidence run and re-apply the red-gate rule
POST/ai-agents/{id}/change-requests/{crId}/scenario/refine«Refinar» — rewrite the scenario that did not reproduce and re-run it
POST/ai-agents/{id}/change-requests/{crId}/transitionMove a change request along its lifecycle
POST/ai-agents/{id}/cloneClone an AI agent (copies tool links, drops is_default)
DELETE/ai-agents/{id}/draftDiscard the pending draft
GET/ai-agents/{id}/draftRead the pending draft (system_prompt + knowledge_tags)
PUT/ai-agents/{id}/draftEdit the pending draft (does not affect live runtime)
GET/ai-agents/{id}/exportExport the agent’s full configuration
GET/ai-agents/{id}/knowledgeList the files attached to an agent
POST/ai-agents/{id}/knowledgeUpload a new file, or attach an existing one
DELETE/ai-agents/{id}/knowledge/{fileId}Detach a file from an agent
GET/ai-agents/{id}/metricsRolling performance metrics for one agent
POST/ai-agents/{id}/publishAtomically promote the draft onto the live runtime + snapshot a version
GET/ai-agents/{id}/publish-gateWould publishing this agent be refused by its evals?
GET/ai-agents/{id}/skillsList the skills attached to one agent
POST/ai-agents/{id}/skillsAttach an existing skill to an agent
DELETE/ai-agents/{id}/skills/{skillId}Detach a skill from an agent (does not delete the skill)
GET/ai-agents/{id}/system-promptGet system prompt
PUT/ai-agents/{id}/system-promptUpdate system prompt
GET/ai-agents/{id}/toolsList tools registered to this AI agent
PUT/ai-agents/{id}/toolsSet the agent’s tools
GET/ai-agents/{id}/versionsList version history for an AI agent
GET/ai-agents/{id}/versions/{version}Get a specific stored version
POST/ai-agents/{id}/versions/{version}/restoreRestore an AI agent to a stored version
POST/ai-agents/{id}/versions/{version}/rollbackMake a past version live again
GET/ai-agents/{id}/voice-channelThe agent's voice channel config: line status, greeting (locked disclosure + brand), curated voices, handoff, propagation state
PUT/ai-agents/{id}/voice-channelUpdate greeting brand segment / curated voice / handoff number (persists Vitrina-side, then best-effort speech-layer propagation)
POST/ai-agents/{id}/voice-channel/propagateRetry pushing the persisted voice config onto the speech layer (when propagation is pending)
GET/ai-agents/{id}/voice-linesThe agent's voice lines (ADR 0053): the PSTN number callers dial and the WhatsApp in-chat call button, each with its own state (none/activating/active/in_call/desync)
POST/ai-agents/{id}/voice-lines/{kind}/activateStart the activation saga for one line. Async and idempotent; a second call while one is running returns refused_reason=already_running rather than starting a second saga
POST/ai-agents/{id}/voice-lines/{kind}/deactivateTake one line off the air. REFUSES with 200 + refused_reason=call_in_progress while a customer is on the line — the call is never dropped
GET/ai-agents/tools-catalogTools this workspace may wire to an agent
GET/change-requests/statsThe workspace's change-request ledger
GET/skillsList the tenant skill library (with per-agent attachment counts)
POST/skillsCreate a skill (optional channel_overrides, v1 key: voice)
DELETE/skills/{id}Delete a skill (SOFT; detaches it from every agent)
GET/skills/{id}Read one skill (base content + per-channel adjustments map)
PUT/skills/{id}Update a skill; channel_overrides replaces the whole map when present, omit to leave adjustments untouched

GET /ai-agent-graphs

List the graphs in the workspace

Graph rows only — no nodes. Fetch one graph to get its nodes. meta.total is the length of the list, not a page count: this endpoint is unpaginated.

curl https://api.vitrinadev.com/api/v1/ai-agent-graphs \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "b4b4b4b4-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "Ventas + postventa",
      "description": "Deriva entre el agente de ventas y el de postventa según la intención del cliente.",
      "topology": "supervisor",
      "status": "active",
      "is_default": false,
      "config": {
        "default_node_key": "triage"
      },
      "published_version_id": "b4b4b4b4-2000-4000-8000-000000000000",
      "draft": null,
      "created_at": "2026-09-05T12:00:00.000Z",
      "updated_at": "2026-09-18T09:00:00.000Z"
    }
  ],
  "meta": {
    "total": 1
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agent-graphs

Create a graph

Creates the graph and, if nodes is supplied, its nodes in one call. Each node references exactly one of ai_agent_id / subgraph_id — supplying both, or neither, is a 400. The response is \{ graph, nodes \} — not a bare graph — because the nodes come back with the ids the caller needs to address them. is_default: true makes this the graph conversations run against and clears the flag on whichever graph held it. Created graphs are unpublished: they have no version and nothing runs them until POST /ai-agent-graphs/{id}/publish. The parallel and evaluator_loop topologies are gated to internal tenants: publishing one without settings.topology_flags.<topology> answers 409. Creating and drafting them is not gated — only publish is, so a graph can be built long before it is allowed to go live.

Cuerpo

CampoTipoObligatorioRestricciones
namestringmín. 1, máx. 200
descriptionstring | nullmáx. 2000
topologysingle \sequential \routing \
configobject
nodesobject[]
is_defaultboolean
curl -X POST https://api.vitrinadev.com/api/v1/ai-agent-graphs \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ventas + postventa",
    "description": "Deriva entre el agente de ventas y el de postventa según la intención del cliente.",
    "topology": "supervisor",
    "nodes": [
      {
        "kind": "supervisor",
        "node_key": "triage",
        "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001"
      },
      {
        "kind": "leaf",
        "node_key": "postventa",
        "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000002"
      }
    ]
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "graph": {
      "id": "b4b4b4b4-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "Ventas + postventa",
      "description": "Deriva entre el agente de ventas y el de postventa según la intención del cliente.",
      "topology": "supervisor",
      "status": "active",
      "is_default": false,
      "config": {
        "default_node_key": "triage"
      },
      "published_version_id": "b4b4b4b4-2000-4000-8000-000000000000",
      "draft": null,
      "created_at": "2026-09-05T12:00:00.000Z",
      "updated_at": "2026-09-18T09:00:00.000Z"
    },
    "nodes": [
      {
        "id": "b4b4b4b4-1000-4000-8000-000000000001",
        "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
        "ai_agent_id": null,
        "subgraph_id": null,
        "kind": "supervisor",
        "node_key": "triage",
        "position": 0,
        "config": {
          "branch_label": "Triage"
        }
      },
      {
        "id": "b4b4b4b4-1000-4000-8000-000000000002",
        "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
        "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
        "subgraph_id": null,
        "kind": "leaf",
        "node_key": "ventas",
        "position": 1,
        "config": {}
      }
    ]
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /ai-agent-graphs/{id}

Delete a graph

Deletes the graph and its nodes. The ai_agent rows the nodes pointed at are untouched — a graph owns its wiring, never its agents.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl -X DELETE https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agent-graphs/{id}

Fetch one graph with its nodes

The graph row with a nodes array spliced in. These are the LIVE nodes; if the graph carries a draft, what will run after the next publish is in draft, which this returns untouched.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "id": "b4b4b4b4-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Ventas + postventa",
    "description": "Deriva entre el agente de ventas y el de postventa según la intención del cliente.",
    "topology": "supervisor",
    "status": "active",
    "is_default": false,
    "config": {
      "default_node_key": "triage"
    },
    "published_version_id": "b4b4b4b4-2000-4000-8000-000000000000",
    "draft": null,
    "created_at": "2026-09-05T12:00:00.000Z",
    "updated_at": "2026-09-18T09:00:00.000Z",
    "nodes": [
      {
        "id": "b4b4b4b4-1000-4000-8000-000000000001",
        "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
        "ai_agent_id": null,
        "subgraph_id": null,
        "kind": "supervisor",
        "node_key": "triage",
        "position": 0,
        "config": {
          "branch_label": "Triage"
        }
      },
      {
        "id": "b4b4b4b4-1000-4000-8000-000000000002",
        "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
        "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
        "subgraph_id": null,
        "kind": "leaf",
        "node_key": "ventas",
        "position": 1,
        "config": {}
      }
    ]
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

PATCH /ai-agent-graphs/{id}

Update a graph

Metadata and shape only — nodes have their own endpoints. At least one field is required. Changing topology re-validates the graph against its existing nodes and fails 400 if they no longer satisfy it, so reshaping usually means editing the nodes in the same session. is_default: true clears the flag on the previous default.

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
namestringmín. 1, máx. 200
descriptionstring | nullmáx. 2000
topologysingle \sequential \routing \
configobject
statusactive \testing \inactive
is_defaultboolean
curl -X PATCH https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ventas + postventa (CL)"
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "b4b4b4b4-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Ventas + postventa (CL)",
    "description": "Deriva entre el agente de ventas y el de postventa según la intención del cliente.",
    "topology": "supervisor",
    "status": "active",
    "is_default": false,
    "config": {
      "default_node_key": "triage"
    },
    "published_version_id": "b4b4b4b4-2000-4000-8000-000000000000",
    "draft": null,
    "created_at": "2026-09-05T12:00:00.000Z",
    "updated_at": "2026-09-18T09:00:00.000Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /ai-agent-graphs/{id}/draft

Discard the draft

Drops the pending edit; the live graph is untouched. Discarding when there is no draft is a no-op, not a 404.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl -X DELETE https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id>/draft \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agent-graphs/{id}/draft

Fetch the pending draft

Answers 200 with data: null when there is no draft — absence of a draft is not a 404.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id>/draft \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "topology": "supervisor",
    "config": {
      "default_node_key": "triage"
    },
    "nodes": [
      {
        "id": "b4b4b4b4-1000-4000-8000-000000000001",
        "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
        "ai_agent_id": null,
        "subgraph_id": null,
        "kind": "supervisor",
        "node_key": "triage",
        "position": 0,
        "config": {
          "branch_label": "Triage"
        }
      },
      {
        "id": "b4b4b4b4-1000-4000-8000-000000000002",
        "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
        "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
        "subgraph_id": null,
        "kind": "leaf",
        "node_key": "ventas",
        "position": 1,
        "config": {}
      }
    ]
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /ai-agent-graphs/{id}/draft

Save the draft

Replaces the whole draft — this is a PUT, so omitting a key drops it rather than leaving the previous value. The draft is intentionally NOT validated: a half-edited graph is a legal thing to save, and validation is deferred to publish. Returns the graph, with the draft on it.

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
topologysingle \sequential \routing \
configobject
nodesobject[]
curl -X PUT https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id>/draft \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "topology": "supervisor",
    "nodes": [
      {
        "kind": "supervisor",
        "node_key": "triage",
        "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001"
      },
      {
        "kind": "leaf",
        "node_key": "ventas",
        "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001"
      },
      {
        "kind": "leaf",
        "node_key": "postventa",
        "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000002"
      }
    ]
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "b4b4b4b4-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Ventas + postventa",
    "description": "Deriva entre el agente de ventas y el de postventa según la intención del cliente.",
    "topology": "supervisor",
    "status": "active",
    "is_default": false,
    "config": {
      "default_node_key": "triage"
    },
    "published_version_id": "b4b4b4b4-2000-4000-8000-000000000000",
    "draft": {
      "topology": "supervisor",
      "nodes": [
        {
          "kind": "supervisor",
          "node_key": "triage",
          "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001"
        },
        {
          "kind": "leaf",
          "node_key": "ventas",
          "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001"
        },
        {
          "kind": "leaf",
          "node_key": "postventa",
          "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000002"
        }
      ]
    },
    "created_at": "2026-09-05T12:00:00.000Z",
    "updated_at": "2026-09-18T09:00:00.000Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agent-graphs/{id}/nodes

Add a node to a graph

Each node references exactly one of ai_agent_id / subgraph_id — supplying both, or neither, is a 400. The whole graph is re-validated with the new node included, so a node that breaks the topology is refused with 400 and nothing is written. Node writes hit the LIVE graph directly — they do not go through the draft.

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
ai_agent_idstring | null
subgraph_idstring | null
kindroot \leaf \supervisor \
node_keystringmín. 1, máx. 80
positioninteger≥ 0, ≤ 99
configobject
curl -X POST https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id>/nodes \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "leaf",
    "node_key": "finanzas",
    "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000002"
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "id": "b4b4b4b4-1000-4000-8000-000000000002",
    "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
    "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000002",
    "subgraph_id": null,
    "kind": "leaf",
    "node_key": "finanzas",
    "position": 1,
    "config": {}
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /ai-agent-graphs/{id}/nodes/{nodeId}

Delete one node

Unlike create and update, deletion is NOT re-validated: the graph is allowed to be left in a shape that could not be published. That is deliberate — removing a node is usually the first step of a rewire — but it means the next publish is where the breakage surfaces.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
nodeIdpathuuid
curl -X DELETE https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id>/nodes/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429

PATCH /ai-agent-graphs/{id}/nodes/{nodeId}

Update one node

At least one field is required. As with create, the graph is re-validated with the patch applied before anything is written.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
nodeIdpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
ai_agent_idstring | null
subgraph_idstring | null
kindroot \leaf \supervisor \
node_keystringmín. 1, máx. 80
positioninteger≥ 0, ≤ 99
configobject
curl -X PATCH https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id>/nodes/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "position": 2
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "b4b4b4b4-1000-4000-8000-000000000002",
    "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
    "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
    "subgraph_id": null,
    "kind": "leaf",
    "node_key": "ventas",
    "position": 2,
    "config": {}
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agent-graphs/{id}/publish

Publish the graph

The only validating write on this resource, and the one with real consequences. In order: enforce the tenant editable_window (409 outside it); promote the draft onto the live shape, replacing the node set wholesale if the draft carries nodes; hard-validate (400 on a bad graph — and the draft has already been promoted by then); snapshot into a new ai_agent_graph_version; move published_version_id; dispatch the ai_agent_graph.publish webhook; and, unless skip_eval is set, start the graph eval suite. The parallel and evaluator_loop topologies are gated to internal tenants: publishing one without settings.topology_flags.&lt;topology&gt; answers 409. Creating and drafting them is not gated — only publish is, so a graph can be built long before it is allowed to go live. The eval run is fire-and-forget: it is NOT awaited and its failure is logged, never surfaced, so a 200 here says the graph published, not that its tests passed. Poll GET /ai-agent-graphs/{id}/tests/runs for that. Returns \{ graph, version \}.

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
labelstring | nullmáx. 200
skip_evalboolean
curl -X POST https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id>/publish \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Agrega derivación a postventa"
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "graph": {
      "id": "b4b4b4b4-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "Ventas + postventa",
      "description": "Deriva entre el agente de ventas y el de postventa según la intención del cliente.",
      "topology": "supervisor",
      "status": "active",
      "is_default": false,
      "config": {
        "default_node_key": "triage"
      },
      "published_version_id": "b4b4b4b4-2000-4000-8000-000000000001",
      "draft": null,
      "created_at": "2026-09-05T12:00:00.000Z",
      "updated_at": "2026-09-18T09:00:00.000Z"
    },
    "version": {
      "id": "b4b4b4b4-2000-4000-8000-000000000001",
      "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
      "version_number": 3,
      "label": "Agrega derivación a postventa",
      "topology": "supervisor",
      "created_at": "2026-09-22T09:05:00.000Z"
    }
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agent-graphs/{id}/versions

List the published versions

The snapshot timeline, newest first, capped at the 50 most recent — the limit is fixed and not a query parameter. Each row carries the full node_snapshot that was live at publish time, so a version is enough to reconstruct the graph without any other read.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id>/versions \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "b4b4b4b4-2000-4000-8000-000000000001",
      "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
      "version_number": 3,
      "label": "Agrega derivación a postventa",
      "topology": "supervisor",
      "created_at": "2026-09-22T09:05:00.000Z"
    },
    {
      "id": "b4b4b4b4-2000-4000-8000-000000000000",
      "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
      "version_number": 2,
      "label": null,
      "topology": "supervisor",
      "created_at": "2026-09-18T09:00:00.000Z"
    }
  ],
  "meta": {
    "total": 2
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agent-graphs/{id}/versions/{version}/restore

Restore a past version

Writes the snapshot back as the live shape and then re-publishes, so a restore is itself a NEW version rather than a pointer move — the timeline only ever grows. Two consequences worth planning for: the new version's label is always Restored from v\{n\} (any label you send is ignored), and because the tail of this is a real publish it inherits every publish gate — the editable window, topology flags, and validation of the snapshot against TODAY's rules. A version published under a since-revoked topology flag can therefore refuse to restore. \{version\} is the version_number from the versions list, not the version row's uuid.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
versionpathinteger≥ 1
curl -X POST https://api.vitrinadev.com/api/v1/ai-agent-graphs/<id>/versions/<id>/restore \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "graph": {
      "id": "b4b4b4b4-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "Ventas + postventa",
      "description": "Deriva entre el agente de ventas y el de postventa según la intención del cliente.",
      "topology": "supervisor",
      "status": "active",
      "is_default": false,
      "config": {
        "default_node_key": "triage"
      },
      "published_version_id": "b4b4b4b4-2000-4000-8000-000000000002",
      "draft": null,
      "created_at": "2026-09-05T12:00:00.000Z",
      "updated_at": "2026-09-18T09:00:00.000Z"
    },
    "version": {
      "id": "b4b4b4b4-2000-4000-8000-000000000002",
      "graph_id": "b4b4b4b4-0000-4000-8000-000000000001",
      "version_number": 4,
      "label": "Restored from v2",
      "topology": "supervisor",
      "created_at": "2026-09-22T09:10:00.000Z"
    }
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents

List AI agents

Every agent in the workspace with the stages wired to it, plus the summary the list cards render: live_version (the highest published ai_agent_version, null when never published), has_draft (any staged change waiting to be published), and counts { tools, kb_files, skills } — enabled tool wirings and NON-DELETED attachments. live_version: null with has_draft: true is an agent that has been configured but never went live.

curl https://api.vitrinadev.com/api/v1/ai-agents \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "a3a3a3a3-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "Asistente de ventas",
      "description": "Responde preguntas de stock y agenda visitas a sucursal.",
      "status": "active",
      "is_default": true,
      "model": "xiaomi/mimo-v2.6-flash",
      "temperature": null,
      "default_max_steps": 5,
      "reasoning_effort": "high",
      "autonomy_level": "supervised",
      "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana.",
      "knowledge_tags": [
        "horarios",
        "garantia"
      ],
      "tags": [],
      "followups_enabled": true,
      "allowed_url_prefixes": [],
      "published_version_id": "a3a3a3a3-1000-4000-8000-000000000007",
      "draft_system_prompt": null,
      "draft_model": null,
      "draft_temperature": null,
      "draft_max_steps": null,
      "draft_tool_wiring": null,
      "draft_skill_ids": null,
      "draft_kb_file_ids": null,
      "draft_knowledge_tags": null,
      "draft_updated_at": null,
      "created_at": "2026-08-04T19:07:05.226Z",
      "updated_at": "2026-09-17T14:49:02.893Z",
      "live_version": 7,
      "has_draft": false,
      "counts": {
        "tools": 15,
        "kb_files": 2,
        "skills": 1
      }
    },
    {
      "id": "a3a3a3a3-0000-4000-8000-000000000002",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "Asistente de postventa",
      "description": "Responde preguntas de stock y agenda visitas a sucursal.",
      "status": "active",
      "is_default": false,
      "model": "xiaomi/mimo-v2.6-flash",
      "temperature": null,
      "default_max_steps": 5,
      "reasoning_effort": "high",
      "autonomy_level": "supervised",
      "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana.",
      "knowledge_tags": [
        "horarios",
        "garantia"
      ],
      "tags": [],
      "followups_enabled": true,
      "allowed_url_prefixes": [],
      "published_version_id": "a3a3a3a3-1000-4000-8000-000000000007",
      "draft_system_prompt": null,
      "draft_model": null,
      "draft_temperature": null,
      "draft_max_steps": null,
      "draft_tool_wiring": null,
      "draft_skill_ids": null,
      "draft_kb_file_ids": null,
      "draft_knowledge_tags": null,
      "draft_updated_at": null,
      "created_at": "2026-08-04T19:07:05.226Z",
      "updated_at": "2026-09-17T14:49:02.893Z",
      "live_version": null,
      "has_draft": true,
      "counts": {
        "tools": 3,
        "kb_files": 0,
        "skills": 0
      }
    }
  ]
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents

Create AI agent

Only name is required. model is an escape hatch and almost always omitted — Vitrina picks and manages the runtime model (ai-agent.service DEFAULT_AGENT_MODEL, currently a flash-tier model; the product never runs a "pro"/reasoning-max tier here) and the UI never asks for one. The agent is born with an empty system_prompt and no published_version_id — it does nothing until a draft is saved and published. Answers 201.

Cuerpo

CampoTipoObligatorioRestricciones
namestringmín. 1, máx. 120
descriptionstringmáx. 4000, por defecto ""
modelstringmín. 1, máx. 120
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Asistente de postventa",
    "description": "Coordina servicio técnico y repuestos."
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "id": "a3a3a3a3-0000-4000-8000-000000000002",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Asistente de postventa",
    "description": "Coordina servicio técnico y repuestos.",
    "status": "active",
    "is_default": false,
    "model": "xiaomi/mimo-v2.6-flash",
    "temperature": null,
    "default_max_steps": 5,
    "reasoning_effort": "high",
    "autonomy_level": "supervised",
    "system_prompt": "",
    "knowledge_tags": [],
    "tags": [],
    "followups_enabled": true,
    "allowed_url_prefixes": [],
    "published_version_id": null,
    "draft_system_prompt": null,
    "draft_model": null,
    "draft_temperature": null,
    "draft_max_steps": null,
    "draft_tool_wiring": null,
    "draft_skill_ids": null,
    "draft_kb_file_ids": null,
    "draft_knowledge_tags": null,
    "draft_updated_at": null,
    "created_at": "2026-08-04T19:07:05.226Z",
    "updated_at": "2026-09-17T14:49:02.893Z"
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /ai-agents/{id}

Delete AI agent

Removes the agent. Its published versions, skills and KB attachments do not follow it — skills and KB files are tenant-shared resources that survive independently. There is no undelete; recreate and reconfigure from scratch. 204.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl -X DELETE https://api.vitrinadev.com/api/v1/ai-agents/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}

Get AI agent

The full row: live config plus every staged draft_* field.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl https://api.vitrinadev.com/api/v1/ai-agents/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a3a3a3a3-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Asistente de ventas",
    "description": "Responde preguntas de stock y agenda visitas a sucursal.",
    "status": "active",
    "is_default": true,
    "model": "xiaomi/mimo-v2.6-flash",
    "temperature": null,
    "default_max_steps": 5,
    "reasoning_effort": "high",
    "autonomy_level": "supervised",
    "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana.",
    "knowledge_tags": [
      "horarios",
      "garantia"
    ],
    "tags": [],
    "followups_enabled": true,
    "allowed_url_prefixes": [],
    "published_version_id": "a3a3a3a3-1000-4000-8000-000000000007",
    "draft_system_prompt": null,
    "draft_model": null,
    "draft_temperature": null,
    "draft_max_steps": null,
    "draft_tool_wiring": null,
    "draft_skill_ids": null,
    "draft_kb_file_ids": null,
    "draft_knowledge_tags": null,
    "draft_updated_at": null,
    "created_at": "2026-08-04T19:07:05.226Z",
    "updated_at": "2026-09-17T14:49:02.893Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /ai-agents/{id}

Update AI agent

Every field optional. Unlike the system prompt and the tool/skill/KB wiring, name/description/model/followups_enabled/allowed_url_prefixes are LIVE IMMEDIATELY — they do not stage on the draft and do not need a publish. A model sent here must stay inside the platform’s flash-tier catalogue; it is not a free-form passthrough to whatever OpenRouter exposes.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
namestringmín. 1, máx. 120
descriptionstringmáx. 4000
modelstringmín. 1, máx. 120
followups_enabledboolean
allowed_url_prefixesstring[]
curl -X PUT https://api.vitrinadev.com/api/v1/ai-agents/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Asistente de postventa (CL)"
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a3a3a3a3-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Asistente de postventa (CL)",
    "description": "Responde preguntas de stock y agenda visitas a sucursal.",
    "status": "active",
    "is_default": true,
    "model": "xiaomi/mimo-v2.6-flash",
    "temperature": null,
    "default_max_steps": 5,
    "reasoning_effort": "high",
    "autonomy_level": "supervised",
    "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana.",
    "knowledge_tags": [
      "horarios",
      "garantia"
    ],
    "tags": [],
    "followups_enabled": true,
    "allowed_url_prefixes": [],
    "published_version_id": "a3a3a3a3-1000-4000-8000-000000000007",
    "draft_system_prompt": null,
    "draft_model": null,
    "draft_temperature": null,
    "draft_max_steps": null,
    "draft_tool_wiring": null,
    "draft_skill_ids": null,
    "draft_kb_file_ids": null,
    "draft_knowledge_tags": null,
    "draft_updated_at": null,
    "created_at": "2026-08-04T19:07:05.226Z",
    "updated_at": "2026-09-17T14:49:02.893Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/change-request-stats

One agent's change-request ledger

ADR 0103 §4.5's two questions, over one agent. Neither existed before this endpoint, and the ADR says why each is worth counting:

  • harness.ratio — requests that terminated at harness (the turn loop, tool execution, retries, handoff mechanics and channel behaviour Vitrina owns, §1.6) over all requests filed in the window. It is the measure of where the product is weak: a workspace whose complaints keep landing there is reporting a defect in the harness, not asking for prompt edits.
  • appliedBySurface / appliedBySurfaceOverTime — the fixes that were APPLIED in the window, by surface (prompt, skill, knowledge, tool, model, handoff, routing, integration, template, harness, feature), as a total and bucketed by ISO week. A month of fixes that are all prompt is a knowledge base nobody is filling.

TWO CLOCKS, deliberately: a request is counted by when it was FILED, an applied fix by when it was APPLIED — a fix landed this week on a complaint from last quarter belongs to this week. byStatus always carries all eight states, zero-filled, so «harness: 0» is visible rather than absent. days defaults to 90 (max 365): the quantity is a trend, and a workspace files a handful of requests a month.

Requires ai_agents:read AND corrections:read.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
daysqueryintegerno≥ 1, ≤ 365
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-request-stats \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "days": 90,
    "total": 14,
    "byStatus": {
      "received": 2,
      "grounded": 1,
      "reproduced": 0,
      "proposed": 1,
      "applied": 3,
      "verified": 5,
      "ya_cumple": 1,
      "harness": 1
    },
    "harness": {
      "terminated": 1,
      "ratio": 0.0714
    },
    "appliedBySurface": [
      {
        "surface": "prompt",
        "count": 5
      },
      {
        "surface": "skill",
        "count": 2
      },
      {
        "surface": "knowledge",
        "count": 1
      }
    ],
    "appliedBySurfaceOverTime": [
      {
        "bucket": "2026-W37",
        "surface": "prompt",
        "count": 2
      },
      {
        "bucket": "2026-W37",
        "surface": "skill",
        "count": 1
      },
      {
        "bucket": "2026-W38",
        "surface": "prompt",
        "count": 3
      },
      {
        "bucket": "2026-W38",
        "surface": "skill",
        "count": 1
      },
      {
        "bucket": "2026-W38",
        "surface": "knowledge",
        "count": 1
      }
    ]
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/change-requests

List an agent's change requests

Newest first — the «Solicitudes» list on the agent page, which is what answers "¿arreglaron lo de los guiones?" without asking Vitrina. Each row carries its display_id (SR-n, per workspace — a label; every path here addresses a request by uuid), the verbatim the client said, the reporter (reporter_kind plus reporter_name when the filer is still a member with a display name), the linked scenario and evidence run once they exist, and the status.

Lifecycle: receivedgroundedreproducedproposedappliedverified, plus two terminals that are outcomes rather than failures — ya_cumple (the scenario went green on its first run and the complaint was about an older version, so the request closes with that green run attached) and harness (the fix belongs to Vitrina's harness — turn loop, retries, handoff mechanics, channel behaviour — so the tenant reads "esto lo arregla Vitrina" with a reference instead of being sent to edit a skill that cannot fix it). harness is reachable from every non-terminal state; ya_cumple from grounded and reproduced. Everything else is a 400 naming both ends of the refused edge.

Requires ai_agents:read AND corrections:write.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
statusqueryreceived \grounded \reproduced \
limitqueryintegerno≥ 1, ≤ 200, por defecto 100
conversation_idqueryuuidno
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-requests \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "b5b5b5b5-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
      "display_id": "SR-14",
      "status": "grounded",
      "verbatim": "No uses guiones largos, suena robótico.",
      "reporter_member_id": "11111111-0000-4000-8000-000000000001",
      "reporter_kind": "client_via_member",
      "reporter_name": "Camila (vendedora)",
      "conversation_refs": [
        {
          "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
          "messageId": "bbbbbbbb-1000-4000-8000-000000000001"
        }
      ],
      "scenario_id": null,
      "evidence_run_id": null,
      "verification_run_id": null,
      "proposal_ids": [],
      "grounding_attempts": [
        {
          "outcome": "confirmed",
          "path": "reviews",
          "candidates": [
            {
              "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
              "messageId": "bbbbbbbb-1000-4000-8000-000000000001",
              "quote": "listo, sin problema - te confirmo por aquí",
              "why": "Coincide con el reclamo."
            }
          ]
        }
      ],
      "no_repro_note": null,
      "received_against_version": 6,
      "override": null,
      "harness_reference": null,
      "tenant_message": null,
      "created_at": "2026-09-20T14:00:00.000Z",
      "updated_at": "2026-09-20T14:05:00.000Z",
      "scenario": null,
      "evidence_run": null,
      "verification_run": null,
      "override_by_name": null
    }
  ],
  "meta": {
    "total": 1
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/change-requests

File a change request (text or audio)

The «Nueva solicitud» form and the inbox turn action. Two content types, one behaviour:

  • application/json with \{ verbatim \} — what the client said, typed.
  • multipart/form-data with the recording in a part named file — webm / ogg / mp4 / m4a / aac / mp3 / wav, up to 20 MB (the video/* spelling of those containers is accepted too: a browser types a .webm picked from disk video/webm and its own MediaRecorder blob audio/webm, and only the audio track is read either way). The audio is normalised to OGG/Opus and transcribed, and THE TRANSCRIPT IS THE verbatim: nothing is stored that could not be turned into text. Send one or the other, never both.

reporter_kind is member (the member's own observation) or client_via_member (the member relaying what a customer said — there is no feedback inbox and no channel, so a client's words always arrive through a member). conversation_refs is [\{ conversationId, messageId? \}]; in the multipart form it is a JSON-encoded string, because a form cannot carry an array.

The new request is born at received and stamps received_against_version — the ai_agent_version.version_number that was live at that moment, i.e. the version the client complained about, keyed exactly like agent_scenario_run.agent_version_number so the red gate can compare the two. Null when the agent has never been published.

Requires ai_agents:read AND corrections:write — filing what a customer told you is a front-line act, and it does not grant anybody the agent's configuration.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
verbatimstringmín. 1, máx. 5000
reporter_kindmember \client_via_member
conversation_refsobject[]
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-requests \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "verbatim": "No uses guiones largos, suena robótico.",
    "reporter_kind": "client_via_member",
    "conversation_refs": [
      {
        "conversationId": "bbbbbbbb-0000-4000-8000-000000000001"
      }
    ]
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "id": "b5b5b5b5-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
    "display_id": "SR-14",
    "status": "received",
    "verbatim": "No uses guiones largos, suena robótico.",
    "reporter_member_id": "11111111-0000-4000-8000-000000000001",
    "reporter_kind": "client_via_member",
    "reporter_name": "Camila (vendedora)",
    "conversation_refs": [],
    "scenario_id": null,
    "evidence_run_id": null,
    "verification_run_id": null,
    "proposal_ids": [],
    "grounding_attempts": [],
    "no_repro_note": null,
    "received_against_version": 6,
    "override": null,
    "harness_reference": null,
    "tenant_message": null,
    "created_at": "2026-09-20T14:00:00.000Z",
    "updated_at": "2026-09-20T14:00:00.000Z",
    "scenario": null,
    "evidence_run": null,
    "verification_run": null,
    "override_by_name": null
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/change-requests/{crId}

Get one change request

The whole row: verbatim, reporter, confirmed conversation refs, the linked scenario and the failed run that is its evidence, the proposals it produced, the grounding attempts, any override recorded when somebody published past a red gate, and — for a harness terminal — the tenant-facing message and its reference.

Requires ai_agents:read AND corrections:write.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
crIdpathuuid
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-requests/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "id": "b5b5b5b5-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
    "display_id": "SR-14",
    "status": "grounded",
    "verbatim": "No uses guiones largos, suena robótico.",
    "reporter_member_id": "11111111-0000-4000-8000-000000000001",
    "reporter_kind": "client_via_member",
    "reporter_name": "Camila (vendedora)",
    "conversation_refs": [
      {
        "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
        "messageId": "bbbbbbbb-1000-4000-8000-000000000001"
      }
    ],
    "scenario_id": null,
    "evidence_run_id": null,
    "verification_run_id": null,
    "proposal_ids": [],
    "grounding_attempts": [
      {
        "outcome": "confirmed",
        "path": "reviews",
        "candidates": [
          {
            "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
            "messageId": "bbbbbbbb-1000-4000-8000-000000000001",
            "quote": "listo, sin problema - te confirmo por aquí",
            "why": "Coincide con el reclamo."
          }
        ]
      }
    ],
    "no_repro_note": null,
    "received_against_version": 6,
    "override": null,
    "harness_reference": null,
    "tenant_message": null,
    "created_at": "2026-09-20T14:00:00.000Z",
    "updated_at": "2026-09-20T14:05:00.000Z",
    "scenario": null,
    "evidence_run": null,
    "verification_run": null,
    "override_by_name": null
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/change-requests/{crId}/ground

«Buscar dónde pasó» — find the conversations a complaint is about

Runs the change-request-grounder read-only tool loop over this tenant's own conversation_review summaries and typed events for this agent, ranked against the verbatim, opens the likely threads and answers with up to five candidates — each a conversation, the agent turn that matches (by message id) and the quote, plus one sentence saying why (ADR 0103 §4.3).

WHEN THE TENANT HAS NO REVIEWS in the window, the loop falls back to reading that agent's most recent conversations directly. Which corpus answered is recorded on the attempt as path (reviews | recent_conversations | none).

FULL-TEXT SEARCH OVER MESSAGE BODIES WAS REJECTED (§4.3): it finds words, and a complaint describes behaviour. The verbatim is treated as DATA throughout — it is the query, never an instruction — and every read is bounded before the first tool call: 30 days by default, at most 12 conversations opened, at most 60 messages of each. The run is metered as a platform agent like every other.

This call CHANGES NOTHING but the ledger: it appends one entry to grounding_attempts at outcome: "pending". No ref is written and the status does not move until a human answers at /ground/confirm.

A RUN THE MODEL GATEWAY REFUSED (exhausted credits, a rate limit, a dead provider) is NOT an empty answer: the attempt is recorded with outcome: "error" carrying the provider's own sentence, and the call answers 502. A 200 with no candidates would say "we looked and it is not there", which is a different and untrue statement.

REFUSED WITH 400 when the request already names its conversations (a pasted ref, or the inbox action, skips grounding — there is nothing to search for) or when it has left received. Requires ai_agents:read AND corrections:write — the same pair as filing, because this asks "which conversation did you mean", not "change the agent".

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
crIdpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
window_daysinteger≥ 1, ≤ 365
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-requests/<id>/ground \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Ejemplo de respuesta (200)

{
  "data": {
    "request": {
      "id": "b5b5b5b5-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
      "display_id": "SR-14",
      "status": "received",
      "verbatim": "No uses guiones largos, suena robótico.",
      "reporter_member_id": "11111111-0000-4000-8000-000000000001",
      "reporter_kind": "client_via_member",
      "reporter_name": "Camila (vendedora)",
      "conversation_refs": [
        {
          "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
          "messageId": "bbbbbbbb-1000-4000-8000-000000000001"
        }
      ],
      "scenario_id": null,
      "evidence_run_id": null,
      "verification_run_id": null,
      "proposal_ids": [],
      "grounding_attempts": [
        {
          "outcome": "confirmed",
          "path": "reviews",
          "candidates": [
            {
              "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
              "messageId": "bbbbbbbb-1000-4000-8000-000000000001",
              "quote": "listo, sin problema - te confirmo por aquí",
              "why": "Coincide con el reclamo."
            }
          ]
        }
      ],
      "no_repro_note": null,
      "received_against_version": 6,
      "override": null,
      "harness_reference": null,
      "tenant_message": null,
      "created_at": "2026-09-20T14:00:00.000Z",
      "updated_at": "2026-09-20T14:05:00.000Z",
      "scenario": null,
      "evidence_run": null,
      "verification_run": null,
      "override_by_name": null
    },
    "attempt": {
      "outcome": "pending",
      "path": "reviews",
      "candidates": [
        {
          "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
          "messageId": "bbbbbbbb-1000-4000-8000-000000000001",
          "quote": "listo, sin problema - te confirmo por aquí",
          "why": "El agente usó un guión largo en una respuesta de confirmación."
        }
      ]
    }
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/change-requests/{crId}/ground/confirm

Confirm (or reject) what the grounding run proposed

The human's answer to the last grounding run, and the only thing that turns it into a fact.

  • \{ refs: [\{ conversationId, messageId \}] \} — the candidates they recognised. They are written to conversation_refs, the attempt is marked confirmed, and the request walks received → grounded through the ordinary machine. Every ref must be one of the LAST run's candidates, message id included: confirming is answering a question that was asked, not a second way to attach an arbitrary conversation (that is the create path, where a pasted ref skips grounding).
  • \{ refs: [] \} — «Ninguna coincide». The attempt is marked rejected and the request STAYS at received with the run on record. Nothing is written to conversation_refs: a rejection is not the absence of refs, it is the statement that the loop did not find them.

meta.outcome echoes which of the two happened. Requires ai_agents:read AND corrections:write.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
crIdpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
refsobject[]
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-requests/<id>/ground/confirm \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "refs": [
      {
        "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
        "messageId": "bbbbbbbb-1000-4000-8000-000000000001"
      }
    ]
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "b5b5b5b5-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
    "display_id": "SR-14",
    "status": "grounded",
    "verbatim": "No uses guiones largos, suena robótico.",
    "reporter_member_id": "11111111-0000-4000-8000-000000000001",
    "reporter_kind": "client_via_member",
    "reporter_name": "Camila (vendedora)",
    "conversation_refs": [
      {
        "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
        "messageId": "bbbbbbbb-1000-4000-8000-000000000001"
      }
    ],
    "scenario_id": null,
    "evidence_run_id": null,
    "verification_run_id": null,
    "proposal_ids": [],
    "grounding_attempts": [
      {
        "outcome": "confirmed",
        "path": "reviews",
        "candidates": [
          {
            "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
            "messageId": "bbbbbbbb-1000-4000-8000-000000000001",
            "quote": "listo, sin problema - te confirmo por aquí",
            "why": "Coincide con el reclamo."
          }
        ]
      }
    ],
    "no_repro_note": null,
    "received_against_version": 6,
    "override": null,
    "harness_reference": null,
    "tenant_message": null,
    "created_at": "2026-09-20T14:00:00.000Z",
    "updated_at": "2026-09-20T14:05:00.000Z",
    "scenario": null,
    "evidence_run": null,
    "verification_run": null,
    "override_by_name": null
  },
  "meta": {
    "outcome": "confirmed"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/change-requests/{crId}/propose

«Proponer arreglo» — the investigator writes the fix from the red run

Runs the finding-investigator's READ-ONLY tool loop over this request's evidence and stores what it proposes, then walks the request reproduced → proposed (ADR 0103 §4.5).

The evidence set is the REQUEST, not a finding: the verbatim (fenced and declared untrusted — it is a customer’s words reaching a tool-using model), the conversations a human confirmed, the RED RUN’s transcript and judge verdict — the controlled reproduction §4.2 spent a model call and a run to produce — and the agent’s draft-aware configuration with what is available but not attached.

The proposals go through the Coach’s own validateProposals / persistProposalsDeduped: an unknown action is dropped, applyable is forced from the canonical set (the model never decides it), and a repeat of an already-open proposal for this agent bumps it instead of inserting a twin. They land with change_request_id set and their ids appended to the request’s proposal_ids.

Every proposal carries a surface derived from its action — prompt, skill, knowledge, tool, model, handoff, routing, integration, template, harness, feature — which is the vocabulary §4.5 makes the ledger countable in: how often the answer is "this one is Vitrina’s", and whether the prompt is doing work the knowledge base should be doing.

REFUSED WITH 400 unless the request is reproduced AND its scenario run went red against the version complained about — the same assertRedRunEvidence the transition surface asks, called BEFORE the model runs so a request without evidence costs nothing. A run the model gateway REFUSED (exhausted credits, a rate limit, a dead provider) is a 502 carrying the provider’s own sentence and moves nothing — "we looked and there is no fix" is a different and untrue statement. Synchronous: somebody pressed a button on their own request. Requires ai_agents:read AND corrections:write AND ai_agents:write.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
crIdpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-requests/<id>/propose \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Ejemplo de respuesta (201)

{
  "data": {
    "request": {
      "id": "b5b5b5b5-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
      "display_id": "SR-14",
      "status": "proposed",
      "verbatim": "No uses guiones largos, suena robótico.",
      "reporter_member_id": "11111111-0000-4000-8000-000000000001",
      "reporter_kind": "client_via_member",
      "reporter_name": "Camila (vendedora)",
      "conversation_refs": [
        {
          "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
          "messageId": "bbbbbbbb-1000-4000-8000-000000000001"
        }
      ],
      "scenario_id": "b5b5b5b5-1000-4000-8000-000000000001",
      "evidence_run_id": "b5b5b5b5-2000-4000-8000-000000000001",
      "verification_run_id": null,
      "proposal_ids": [
        "b5b5b5b5-3000-4000-8000-000000000001"
      ],
      "grounding_attempts": [
        {
          "outcome": "confirmed",
          "path": "reviews",
          "candidates": [
            {
              "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
              "messageId": "bbbbbbbb-1000-4000-8000-000000000001",
              "quote": "listo, sin problema - te confirmo por aquí",
              "why": "Coincide con el reclamo."
            }
          ]
        }
      ],
      "no_repro_note": null,
      "received_against_version": 6,
      "override": null,
      "harness_reference": null,
      "tenant_message": null,
      "created_at": "2026-09-20T14:00:00.000Z",
      "updated_at": "2026-09-20T14:05:00.000Z",
      "scenario": null,
      "evidence_run": null,
      "verification_run": null,
      "override_by_name": null
    },
    "proposals": [
      {
        "id": "b5b5b5b5-3000-4000-8000-000000000001",
        "surface": "prompt",
        "action": "edit_system_prompt",
        "summary": "Agrega una instrucción explícita: nunca usar guiones largos en las respuestas.",
        "applyable": true
      }
    ],
    "investigation": {
      "evidence": "red_run",
      "model": "xiaomi/mimo-v2.6-flash"
    }
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/change-requests/{crId}/scenario

«Crear escenario» — author the eval from the request and run it

ONE call does four things, because all four are the same act and half of it is worse than none (ADR 0103 §4.2):

  1. The scenario-builder writes the scenario — from: "conversation" over the FIRST confirmed ref (the builder masks phones, e-mails and RUTs out of the transcript before the model sees it), or from: "description" with the verbatim when the request names no conversation. EITHER WAY the verbatim is the operator note, so the client's own words end up in scenario.source.note; when the behaviour was confirmed in more than one thread the note says so, because a complaint seen four times is a class and deserves a check that guards the class.
  2. The scenario is saved ACTIVE and TAGGED with the request SR-n, which is how the Evals tab and the «Solicitudes» list find each other.
  3. It joins the agent's golden suite — by construction when the suite has the usual empty filter (every ACTIVE scenario), by appending to scenario_ids when the suite names an explicit list.
  4. ONE run is enqueued — never the scenario's own repeats, because this is evidence and not a flake measurement — against the version the client complained about: live when received_against_version is a number, draft when it is null (the agent was never published, so the draft IS the thing under repair). scenario_id and evidence_run_id land on the request.

The run is ASYNC: the answer carries it queued, and the verdict is applied by the scenario-run worker when it finishes (or on demand at …/scenario/evaluate).

REFUSED WITH 400 when the request is not grounded (nobody has settled which conversations it is about) or already links a scenario (that is «Refinar»). A body the builder could not make valid twice is a 422 with code: "builder_invalid" and the zod issues, exactly as POST /ai-agents/\{id\}/scenarios/build answers it. Requires ai_agents:read AND corrections:write AND ai_agents:write — it authors and saves an eval, which is what the scenario routes take.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
crIdpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-requests/<id>/scenario \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Ejemplo de respuesta (201)

{
  "data": {
    "request": {
      "id": "b5b5b5b5-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
      "display_id": "SR-14",
      "status": "reproduced",
      "verbatim": "No uses guiones largos, suena robótico.",
      "reporter_member_id": "11111111-0000-4000-8000-000000000001",
      "reporter_kind": "client_via_member",
      "reporter_name": "Camila (vendedora)",
      "conversation_refs": [
        {
          "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
          "messageId": "bbbbbbbb-1000-4000-8000-000000000001"
        }
      ],
      "scenario_id": "b5b5b5b5-1000-4000-8000-000000000001",
      "evidence_run_id": "b5b5b5b5-2000-4000-8000-000000000001",
      "verification_run_id": null,
      "proposal_ids": [],
      "grounding_attempts": [
        {
          "outcome": "confirmed",
          "path": "reviews",
          "candidates": [
            {
              "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
              "messageId": "bbbbbbbb-1000-4000-8000-000000000001",
              "quote": "listo, sin problema - te confirmo por aquí",
              "why": "Coincide con el reclamo."
            }
          ]
        }
      ],
      "no_repro_note": null,
      "received_against_version": 6,
      "override": null,
      "harness_reference": null,
      "tenant_message": null,
      "created_at": "2026-09-20T14:00:00.000Z",
      "updated_at": "2026-09-20T14:05:00.000Z",
      "scenario": null,
      "evidence_run": null,
      "verification_run": null,
      "override_by_name": null
    },
    "scenario": {
      "id": "b5b5b5b5-1000-4000-8000-000000000001",
      "name": "SR-14 — sin guiones largos",
      "tags": [
        "SR-14"
      ]
    },
    "run": {
      "id": "b5b5b5b5-2000-4000-8000-000000000001",
      "status": "queued"
    },
    "ran_against": "live"
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/change-requests/{crId}/scenario/evaluate

Re-read the evidence run and re-apply the red-gate rule

§4.2's rule, applied on demand and IDEMPOTENTLY:

  • run failed — the complaint reproduces: grounded → reproduced, and «no reprodujo» is cleared.
  • run passed or partial and the complaint PREDATES the live version — the agent already complies: the request closes as ya_cumple with the green run still attached, because a verdict nobody can open is not a verdict.
  • run passed or partial otherwise — the request STAYS at grounded, no_repro_note records «no reprodujo» with a timestamp, and «Refinar» is what the surface offers next. A scenario authored from a complaint that passes on its first run has captured something else.
  • run error — nothing was learned. The request stays exactly where it was and the note says the gateway refused, because "we ran it and it did not reproduce" is a different and untrue statement.
  • run queued/runningmeta.outcome: "pending" and nothing changes.

The scenario-run worker applies the same rule through the same function the moment a run finishes; this exists for when that hook did not run (the evals pool was down, the process was killed between the finish and the hook). 400 when the request has no run to evaluate. Same scopes as authoring: the rule MOVES the request, and every target past grounded takes ai_agents:write.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
crIdpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-requests/<id>/scenario/evaluate \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Ejemplo de respuesta (200)

{
  "data": {
    "request": {
      "id": "b5b5b5b5-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
      "display_id": "SR-14",
      "status": "reproduced",
      "verbatim": "No uses guiones largos, suena robótico.",
      "reporter_member_id": "11111111-0000-4000-8000-000000000001",
      "reporter_kind": "client_via_member",
      "reporter_name": "Camila (vendedora)",
      "conversation_refs": [
        {
          "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
          "messageId": "bbbbbbbb-1000-4000-8000-000000000001"
        }
      ],
      "scenario_id": "b5b5b5b5-1000-4000-8000-000000000001",
      "evidence_run_id": "b5b5b5b5-2000-4000-8000-000000000001",
      "verification_run_id": null,
      "proposal_ids": [],
      "grounding_attempts": [
        {
          "outcome": "confirmed",
          "path": "reviews",
          "candidates": [
            {
              "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
              "messageId": "bbbbbbbb-1000-4000-8000-000000000001",
              "quote": "listo, sin problema - te confirmo por aquí",
              "why": "Coincide con el reclamo."
            }
          ]
        }
      ],
      "no_repro_note": null,
      "received_against_version": 6,
      "override": null,
      "harness_reference": null,
      "tenant_message": null,
      "created_at": "2026-09-20T14:00:00.000Z",
      "updated_at": "2026-09-20T14:05:00.000Z",
      "scenario": null,
      "evidence_run": null,
      "verification_run": null,
      "override_by_name": null
    },
    "run": {
      "id": "b5b5b5b5-2000-4000-8000-000000000001",
      "status": "failed"
    },
    "meta": {
      "outcome": "reproduced"
    }
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/change-requests/{crId}/scenario/refine

«Refinar» — rewrite the scenario that did not reproduce and re-run it

The builder's refine step over the request's current scenario, then a fresh run (ADR 0103 §4.2: a first-run pass "leaves the request at grounded with a note and offers the builder's refine step").

note is what the builder is told to change and DEFAULTS TO THE VERBATIM, which is the correct first refine: a scenario that went green on a real complaint usually missed the point of what the client said. The stored body is replaced with the refined one (the SR-n tag is re-applied rather than trusted), evidence_run_id points at the new run and no_repro_note is CLEARED — the old verdict was about a scenario that no longer exists in that shape.

Offered while the request is still grounded and it links a scenario; anything else is a 400. Past grounded there is a red run somebody is acting on, and replacing the scenario under it would move the evidence out from under a proposal. Same scopes as authoring; the same 422 contract when the builder cannot produce a valid body twice.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
crIdpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
notestringmín. 1, máx. 5000
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-requests/<id>/scenario/refine \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Ejemplo de respuesta (200)

{
  "data": {
    "request": {
      "id": "b5b5b5b5-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
      "display_id": "SR-14",
      "status": "grounded",
      "verbatim": "No uses guiones largos, suena robótico.",
      "reporter_member_id": "11111111-0000-4000-8000-000000000001",
      "reporter_kind": "client_via_member",
      "reporter_name": "Camila (vendedora)",
      "conversation_refs": [
        {
          "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
          "messageId": "bbbbbbbb-1000-4000-8000-000000000001"
        }
      ],
      "scenario_id": "b5b5b5b5-1000-4000-8000-000000000001",
      "evidence_run_id": "b5b5b5b5-2000-4000-8000-000000000002",
      "verification_run_id": null,
      "proposal_ids": [],
      "grounding_attempts": [
        {
          "outcome": "confirmed",
          "path": "reviews",
          "candidates": [
            {
              "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
              "messageId": "bbbbbbbb-1000-4000-8000-000000000001",
              "quote": "listo, sin problema - te confirmo por aquí",
              "why": "Coincide con el reclamo."
            }
          ]
        }
      ],
      "no_repro_note": null,
      "received_against_version": 6,
      "override": null,
      "harness_reference": null,
      "tenant_message": null,
      "created_at": "2026-09-20T14:00:00.000Z",
      "updated_at": "2026-09-20T14:05:00.000Z",
      "scenario": null,
      "evidence_run": null,
      "verification_run": null,
      "override_by_name": null
    },
    "scenario": {
      "id": "b5b5b5b5-1000-4000-8000-000000000001",
      "name": "SR-14 — sin guiones largos (refinado)",
      "tags": [
        "SR-14"
      ]
    },
    "run": {
      "id": "b5b5b5b5-2000-4000-8000-000000000002",
      "status": "queued"
    }
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/change-requests/{crId}/transition

Move a change request along its lifecycle

Lifecycle: receivedgroundedreproducedproposedappliedverified, plus two terminals that are outcomes rather than failures — ya_cumple (the scenario went green on its first run and the complaint was about an older version, so the request closes with that green run attached) and harness (the fix belongs to Vitrina's harness — turn loop, retries, handoff mechanics, channel behaviour — so the tenant reads "esto lo arregla Vitrina" with a reference instead of being sent to edit a skill that cannot fix it). harness is reachable from every non-terminal state; ya_cumple from grounded and reproduced. Everything else is a 400 naming both ends of the refused edge.

THE SCOPE SPLIT. received → grounded takes the same pair as filing (ai_agents:read AND corrections:write): it says "this is the conversation I meant", not "change the agent". EVERY other target additionally requires ai_agents:write, and a caller without it gets a 403 naming the scope.

The write is a compare-and-set against the status that was read, so two people pressing at once cannot both win — the loser gets a 400 saying the request is no longer where they thought it was.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
crIdpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
toreceived \grounded \reproduced \
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/change-requests/<id>/transition \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "reproduced"
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "b5b5b5b5-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "ai_agent_id": "a3a3a3a3-0000-4000-8000-000000000001",
    "display_id": "SR-14",
    "status": "reproduced",
    "verbatim": "No uses guiones largos, suena robótico.",
    "reporter_member_id": "11111111-0000-4000-8000-000000000001",
    "reporter_kind": "client_via_member",
    "reporter_name": "Camila (vendedora)",
    "conversation_refs": [
      {
        "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
        "messageId": "bbbbbbbb-1000-4000-8000-000000000001"
      }
    ],
    "scenario_id": null,
    "evidence_run_id": null,
    "verification_run_id": null,
    "proposal_ids": [],
    "grounding_attempts": [
      {
        "outcome": "confirmed",
        "path": "reviews",
        "candidates": [
          {
            "conversationId": "bbbbbbbb-0000-4000-8000-000000000001",
            "messageId": "bbbbbbbb-1000-4000-8000-000000000001",
            "quote": "listo, sin problema - te confirmo por aquí",
            "why": "Coincide con el reclamo."
          }
        ]
      }
    ],
    "no_repro_note": null,
    "received_against_version": 6,
    "override": null,
    "harness_reference": null,
    "tenant_message": null,
    "created_at": "2026-09-20T14:00:00.000Z",
    "updated_at": "2026-09-20T14:05:00.000Z",
    "scenario": null,
    "evidence_run": null,
    "verification_run": null,
    "override_by_name": null
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/clone

Clone an AI agent (copies tool links, drops is_default)

Copies the SOURCE agent’s live config plus its tool wirings into a brand-new agent — skills and KB attachments included, each by reference (the shared row, not a duplicate). The clone is never is_default and starts with no published_version_id: publish it explicitly once you are happy with it. Answers 201.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
namestringmín. 1, máx. 120
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/clone \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Asistente de ventas (copia)"
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "id": "a3a3a3a3-0000-4000-8000-000000000003",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Asistente de ventas (copia)",
    "description": "Responde preguntas de stock y agenda visitas a sucursal.",
    "status": "active",
    "is_default": false,
    "model": "xiaomi/mimo-v2.6-flash",
    "temperature": null,
    "default_max_steps": 5,
    "reasoning_effort": "high",
    "autonomy_level": "supervised",
    "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana.",
    "knowledge_tags": [
      "horarios",
      "garantia"
    ],
    "tags": [],
    "followups_enabled": true,
    "allowed_url_prefixes": [],
    "published_version_id": "a3a3a3a3-1000-4000-8000-000000000007",
    "draft_system_prompt": null,
    "draft_model": null,
    "draft_temperature": null,
    "draft_max_steps": null,
    "draft_tool_wiring": null,
    "draft_skill_ids": null,
    "draft_kb_file_ids": null,
    "draft_knowledge_tags": null,
    "draft_updated_at": null,
    "created_at": "2026-08-04T19:07:05.226Z",
    "updated_at": "2026-09-17T14:49:02.893Z"
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /ai-agents/{id}/draft

Discard the pending draft

Clears every staged draft_* field. The live runtime is untouched — this only throws away work in progress. A no-op (still 204) when there is no pending draft.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl -X DELETE https://api.vitrinadev.com/api/v1/ai-agents/<id>/draft \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/draft

Read the pending draft (system_prompt + knowledge_tags)

Only the staged draft_* fields, not the whole agent — GET /ai-agents/\{id\} already carries them alongside the live ones. draft_updated_at: null means there is no pending draft.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/draft \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "draft_system_prompt": "Eres un asistente de ventas. Ahora también ofreces financiamiento.",
    "draft_knowledge_tags": [
      "horarios",
      "garantia",
      "financiamiento"
    ],
    "draft_updated_at": "2026-09-21T10:00:00.000Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /ai-agents/{id}/draft

Edit the pending draft (does not affect live runtime)

The full-config draft (ADR 0010/0012): system_prompt, voice_instructions, model/temperature/max_steps, tool_wiring, skill_ids, kb_file_ids, knowledge_tags. Every field optional and merged — an omitted field leaves the previous draft value untouched (an empty string IS a value: it stages a clear). Nothing here reaches the runtime until POST /ai-agents/\{id\}/publish.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
system_promptstring
voice_instructionsstringmáx. 40000
comment_dm_criteriastringmáx. 4000
knowledge_tagsstring[]
modelstringmín. 1, máx. 120
temperaturenumber | null≥ 0, ≤ 2
reasoning_effortmax \xhigh \high \
max_stepsinteger≥ 1, ≤ 20
tool_wiringobject[]
skill_idsuuid[]
kb_file_idsuuid[]
curl -X PUT https://api.vitrinadev.com/api/v1/ai-agents/<id>/draft \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "system_prompt": "Eres un asistente de ventas. Ahora también ofreces financiamiento.",
    "knowledge_tags": [
      "horarios",
      "garantia",
      "financiamiento"
    ]
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "draft_system_prompt": "Eres un asistente de ventas. Ahora también ofreces financiamiento.",
    "draft_knowledge_tags": [
      "horarios",
      "garantia",
      "financiamiento"
    ],
    "draft_updated_at": "2026-09-21T10:00:00.000Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/export

Export the agent’s full configuration

Everything the runtime assembles for this agent, built through the same assembler the runtime uses: the rendered system prompt, the gated tool catalogue with resolved "when to use" text, skills, KB files, workspace/business-hours/branch/branding/pipeline context, every parameter, and the staged draft. The best answer available to "why did the agent do that".

Neither format is the \{ data \} envelope. The default is the bundle as a JSON document served as an attachment; ?format=zip returns a readable folder tree (instructions.md, skills/, knowledge/ with the real file bytes, tools.json). Available in all environments, tenant-scoped.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
formatqueryzipno
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/export \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "agent": {
    "id": "a3a3a3a3-0000-4000-8000-000000000001",
    "name": "Asistente de ventas"
  },
  "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana.",
  "tools": [
    {
      "name": "search_vehicle_stock",
      "when_to_use": "Cuando el cliente pregunte por disponibilidad."
    }
  ],
  "skills": [
    {
      "id": "a4a4a4a4-0000-4000-8000-000000000001",
      "name": "Agendar hora"
    }
  ],
  "knowledge": [
    {
      "id": "a6a6a6a6-0000-4000-8000-000000000001",
      "name": "Garantías 2026.pdf"
    }
  ]
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/knowledge

List the files attached to an agent

The agent’s slice of the tenant knowledge library. A file lives in /kb-files and is ATTACHED here — the same file can back several agents.

Each row carries snippet: the first 400 characters of the file’s first INGESTED chunk. null means the file has no chunks — still ingesting, or ingestion failed — which also means the agent cannot retrieve it, so it is a status signal and not just a missing preview.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/knowledge \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "kb_file_id": "a6a6a6a6-0000-4000-8000-000000000001",
      "name": "Garantías 2026.pdf",
      "status": "ingested",
      "snippet": "Todos los vehículos nuevos incluyen 3 años o 100.000 km de garantía de fábrica…"
    }
  ],
  "meta": {
    "total": 1
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/knowledge

Upload a new file, or attach an existing one

One URL, two operations, chosen by content-type. Send multipart with a file field to upload a new file and auto-attach it (25 MB cap); send JSON \{ kb_file_id \} to attach a file already in the library. Neither is a fallback for the other — a JSON body that is not a valid \{ kb_file_id \} is a 400 telling you to pick one.

The two also answer differently: the upload branch returns the created file row, the attach branch returns \{ attached: true, kb_file_id \}. Both are 201.

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
kb_file_iduuid
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/knowledge \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kb_file_id": "a6a6a6a6-0000-4000-8000-000000000001"
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "attached": true,
    "kb_file_id": "a6a6a6a6-0000-4000-8000-000000000001"
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /ai-agents/{id}/knowledge/{fileId}

Detach a file from an agent

Detaches, it does not delete. The file stays in the tenant library and keeps backing every other agent it is attached to. To delete the file itself — which cascades to its attachments and its embedded chunks — use DELETE /kb-files/\{id\}.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
fileIdpathuuid
curl -X DELETE https://api.vitrinadev.com/api/v1/ai-agents/<id>/knowledge/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/metrics

Rolling performance metrics for one agent

The detail screen’s header strip. A ROLLING window ending now: days back from the request, default 7, max 90.

conversations, resolved_without_human and handoffs are the same numbers the Insights tab shows, read from the insights_ai_agents RPC — a conversation counts for an agent when that agent posted a REAL turn in it inside the window (template sends, tool-call rows and reasoning traces are not turns). resolved_without_human = no human wrote AND the agent never called handoff; handoffs = the agent called handoff, with handoffs_answered the subset a human then replied to. pct is a FRACTION of conversations, and is null — not 0 — when there were none.

first_response_seconds is a MEDIAN, not a mean, over conversations STARTED in the window, pairing each one’s first inbound message with this agent’s first later reply; null when no conversation has both. appointments_booked counts completed booking-tool executions.

An agent with no traffic answers 200 with zeros and nulls, never 404.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
daysqueryintegerno≥ 1, ≤ 90, por defecto 7
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/metrics \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "days": 7,
    "from": "2026-09-15T16:38:21.402Z",
    "to": "2026-09-22T16:38:21.402Z",
    "conversations": 18,
    "resolved_without_human": {
      "count": 4,
      "pct": 0.22
    },
    "handoffs": {
      "count": 2,
      "pct": 0.11
    },
    "handoffs_answered": 2,
    "appointments_booked": 1,
    "first_response_seconds": 388.36
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/publish

Atomically promote the draft onto the live runtime + snapshot a version

Gated by Agent Evals (ADR 0098): when the agent has an ENABLED GOLDEN suite whose gate is blocked or stale, this answers 409 \{ error, code: 'evals_blocked' | 'evals_stale', gate \} — see GET /ai-agents/\{id\}/publish-gate for the semantics. force: true publishes anyway and is recorded in the audit log with the actor. skip_eval: true skips the post-publish suite/scenario run; it does NOT bypass the gate.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
skip_evalboolean
forceboolean
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/publish \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a3a3a3a3-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Asistente de ventas",
    "description": "Responde preguntas de stock y agenda visitas a sucursal.",
    "status": "active",
    "is_default": true,
    "model": "xiaomi/mimo-v2.6-flash",
    "temperature": null,
    "default_max_steps": 5,
    "reasoning_effort": "high",
    "autonomy_level": "supervised",
    "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana.",
    "knowledge_tags": [
      "horarios",
      "garantia"
    ],
    "tags": [],
    "followups_enabled": true,
    "allowed_url_prefixes": [],
    "published_version_id": "a3a3a3a3-1000-4000-8000-000000000008",
    "draft_system_prompt": null,
    "draft_model": null,
    "draft_temperature": null,
    "draft_max_steps": null,
    "draft_tool_wiring": null,
    "draft_skill_ids": null,
    "draft_kb_file_ids": null,
    "draft_knowledge_tags": null,
    "draft_updated_at": null,
    "created_at": "2026-08-04T19:07:05.226Z",
    "updated_at": "2026-09-17T14:49:02.893Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/publish-gate

Would publishing this agent be refused by its evals?

Reads the agent's ENABLED GOLDEN suite and nothing else.

  • none — no enabled golden suite. Never blocks: a workspace that has not opted into gating is not gated.
  • pending — a golden suite run is queued or running. Warn only; blocking here would mean "press publish again in four minutes".
  • stale — the newest COMPLETED golden run predates the agent's draft_updated_at (or there is none). Checked BEFORE the policy: a red verdict about a draft that no longer exists is not evidence about the one being published.
  • blocked — that run has hard fails (when policy.block_publish_on_hard_fail) or a pass_rate under policy.min_pass_rate.
  • ready — otherwise.

reasons are stable snake_case codes (no_golden_suite, suite_run_in_flight, no_completed_run, stale_draft, hard_fails, pass_rate_below_min) so the dialog can localize them. failing lists the red scenarios of the last completed run with their run_ids.

blocked and stale make POST /ai-agents/\{id\}/publish answer 409 \{ error, code: 'evals_blocked' | 'evals_stale', gate \} unless the body carries force: true.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/publish-gate \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "gate": "clear",
    "suite": {
      "id": "b6b6b6b6-0000-4000-8000-000000000001",
      "name": "Golden",
      "kind": "golden"
    },
    "last_run": {
      "pass_rate": 91.2,
      "hard_fails": 0
    }
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/skills

List the skills attached to one agent

The LIVE attachment set for this agent — a subset of the tenant skill library (GET /skills).

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/skills \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "a4a4a4a4-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "Agendar hora",
      "description": "Cómo ofrecer y confirmar horarios disponibles.",
      "content": "Cuando el cliente quiera agendar, ofrece los horarios disponibles y confirma antes de reservar.",
      "status": "active",
      "channel_overrides": {},
      "created_by": "11111111-0000-4000-8000-000000000001",
      "created_at": "2026-09-04T19:10:00.000Z",
      "updated_at": "2026-09-04T19:10:00.000Z",
      "deleted_at": null
    }
  ],
  "meta": {
    "total": 1
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/skills

Attach an existing skill to an agent

Links a skill already in the library to this agent — it does not create one. The skill content itself is shared: editing it (PUT /skills/\{id\}) changes what every attached agent sees. Answers 201; attaching an already-attached skill is idempotent.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
skill_iduuid
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/skills \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "skill_id": "a4a4a4a4-0000-4000-8000-000000000001"
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "attached": true,
    "skill_id": "a4a4a4a4-0000-4000-8000-000000000001"
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /ai-agents/{id}/skills/{skillId}

Detach a skill from an agent (does not delete the skill)

Removes the attachment only — the skill row survives in the library and stays attached to any other agent. To remove the skill entirely, use DELETE /skills/\{id\}.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
skillIdpathuuid
curl -X DELETE https://api.vitrinadev.com/api/v1/ai-agents/<id>/skills/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/system-prompt

Get system prompt

The LIVE system_prompt — what the runtime reads on the next turn. The staged edit, if any, is on draft_system_prompt (GET /ai-agents/\{id\}/draft).

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/system-prompt \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana."
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /ai-agents/{id}/system-prompt

Update system prompt

A convenience shortcut for editing ONLY the prompt: identical in effect to PUT /ai-agents/\{id\}/draft with just system_prompt set — it STAGES the change onto draft_system_prompt and does not touch the live runtime until POST /ai-agents/\{id\}/publish.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
system_promptstring
knowledge_tagsstring[]
curl -X PUT https://api.vitrinadev.com/api/v1/ai-agents/<id>/system-prompt \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "system_prompt": "Eres un asistente de ventas. Responde de forma breve, cercana y en español."
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana."
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/tools

List tools registered to this AI agent

The LIVE tool wiring — platform tools and custom tools currently enabled for this agent. PUT /ai-agents/\{id\}/tools replaces this set wholesale.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/tools \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "a3a3a3a3-2000-4000-8000-000000000001",
      "name": "search_vehicle_stock",
      "kind": "builtin",
      "external_name": "Search vehicle stock",
      "description": "Search the dealer's current vehicle stock."
    }
  ]
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /ai-agents/{id}/tools

Set the agent’s tools

Replaces the whole wiring with tool_names — this is a set, not a patch, so omitting a currently-wired tool unwires it.

Applies LIVE, not to the draft. That is deliberate and was a bug fix: staging tool selection meant Save wrote a draft the picker never read back, so the selection appeared to revert on refetch. The runtime and the picker now read the same live set, and a save here changes what the agent can do on its very next turn without a publish.

Names come from /ai-agents/tools-catalog. A name the workspace is not entitled to is DROPPED rather than rejected, so compare meta.total against what you sent — a 200 does not mean everything was wired. Removal is narrower than that on purpose: only a tool you left OUT of tool_names is unwired. One you still selected that happens to be gated right now (a calendar that momentarily looks disconnected) keeps its link, because the runtime already hides it and deleting the wiring would lose it permanently once the integration came back.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
tool_namesstring[]
curl -X PUT https://api.vitrinadev.com/api/v1/ai-agents/<id>/tools \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tool_names": [
      "search_vehicle_stock",
      "book_appointment"
    ]
  }'

Ejemplo de respuesta (200)

{
  "data": [
    {
      "name": "search_vehicle_stock",
      "wired": true
    },
    {
      "name": "book_appointment",
      "wired": true
    }
  ],
  "meta": {
    "total": 2
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/versions

List version history for an AI agent

Newest first. Each row is the FULL stored version row EXCEPT that skill_snapshot[].content is omitted — the timeline never renders a skill body and it is the only unbounded field on the row. Open a version to get the bodies. The per-version "+N skills / +N archivos / +N herramientas" chips are a client-side diff of the id arrays against the previous row (ADR 0093).

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/versions \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "a3a3a3a3-1000-4000-8000-000000000007",
      "version_number": 7,
      "label": "Published draft",
      "created_by": "11111111-0000-4000-8000-000000000001",
      "created_at": "2026-09-17T14:49:02.601Z"
    },
    {
      "id": "a3a3a3a3-1000-4000-8000-000000000006",
      "version_number": 6,
      "label": "Rollback to v4",
      "created_by": "11111111-0000-4000-8000-000000000001",
      "created_at": "2026-09-10T11:02:00.000Z"
    }
  ]
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/versions/{version}

Get a specific stored version

The full row (skill bodies included) plus resolved — ADR 0093’s answer to "what became of the things this version referenced". Skills and KB files are tenant-shared resources referenced BY ID, so each one reports a state: current, modified (the skill exists but its content or channel_overrides drifted since this version), deleted (soft-deleted — a restore brings it back) or missing (no row at all; a restore drops it). Names for deleted/missing items come from the version’s own snapshot.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
versionpathinteger≥ 1
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/versions/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a3a3a3a3-1000-4000-8000-000000000007",
    "version_number": 7,
    "label": "Published draft",
    "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana.",
    "skill_snapshot": [
      {
        "id": "a4a4a4a4-0000-4000-8000-000000000001",
        "name": "Agendar hora",
        "state": "current"
      }
    ],
    "kb_file_snapshot": [
      {
        "id": "a6a6a6a6-0000-4000-8000-000000000001",
        "name": "Garantías 2026.pdf",
        "state": "current"
      }
    ],
    "resolved": {
      "skills": [
        {
          "id": "a4a4a4a4-0000-4000-8000-000000000001",
          "state": "current"
        }
      ],
      "kb_files": [
        {
          "id": "a6a6a6a6-0000-4000-8000-000000000001",
          "state": "current"
        }
      ]
    },
    "created_by": "11111111-0000-4000-8000-000000000001",
    "created_at": "2026-09-17T14:49:02.601Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/versions/{version}/restore

Restore an AI agent to a stored version

Loads the version into the DRAFT (nothing goes live until Publish). ADR 0093 — restore also REPAIRS the references first: a soft-deleted skill is un-deleted, a soft-deleted KB file is un-deleted and re-ingested from S3, and a reference with no row left is dropped from the staged draft rather than staged into a publish that would FK-fail. restore.recovered lists what came back, restore.warnings what was dropped.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
versionpathinteger≥ 1
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/versions/<id>/restore \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "restored": {
      "version_number": 4
    },
    "restore": {
      "recovered": [
        {
          "kind": "skill",
          "id": "a4a4a4a4-0000-4000-8000-000000000001"
        }
      ],
      "warnings": []
    }
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/versions/{version}/rollback

Make a past version live again

Three steps in one call: load the version into the draft, PUBLISH it, and snapshot the result labelled Rollback to v\{n\} so history stays linear rather than branching. It also fires ai_agent.publish, so downstream consumers see a rollback as the publish it is.

Guarded like a publish: the tenant editable_window applies and a rollback outside it is refused. Contrast the sibling /versions/\{version\}/restore, which only loads the version into the draft and leaves publishing to you — use that one if you want to look before going live.

\{version\} is the version NUMBER, not the version row’s id. Anything in the draft at the time is overwritten.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
versionpathinteger≥ 1
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/versions/<id>/rollback \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a3a3a3a3-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Asistente de ventas",
    "description": "Responde preguntas de stock y agenda visitas a sucursal.",
    "status": "active",
    "is_default": true,
    "model": "xiaomi/mimo-v2.6-flash",
    "temperature": null,
    "default_max_steps": 5,
    "reasoning_effort": "high",
    "autonomy_level": "supervised",
    "system_prompt": "Eres un asistente de ventas. Responde de forma breve y cercana.",
    "knowledge_tags": [
      "horarios",
      "garantia"
    ],
    "tags": [],
    "followups_enabled": true,
    "allowed_url_prefixes": [],
    "published_version_id": "a3a3a3a3-1000-4000-8000-000000000008",
    "draft_system_prompt": null,
    "draft_model": null,
    "draft_temperature": null,
    "draft_max_steps": null,
    "draft_tool_wiring": null,
    "draft_skill_ids": null,
    "draft_kb_file_ids": null,
    "draft_knowledge_tags": null,
    "draft_updated_at": null,
    "created_at": "2026-08-04T19:07:05.226Z",
    "updated_at": "2026-09-17T14:49:02.893Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/voice-channel

The agent's voice channel config: line status, greeting (locked disclosure + brand), curated voices, handoff, propagation state

One combined read of everything the Voz tab needs: the PSTN line state, the composed greeting (greeting_brand is the only editable segment — the AI-disclosure + recording notice is fixed and composed server-side, ADR 0039), the curated voice catalogue voice_id picks from, handoff_destination (E.164, null when unset), and propagation (whether the persisted config has reached the speech layer yet).

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/voice-channel \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "configured": true,
    "account_id": "a3a3a3a3-3000-4000-8000-000000000001",
    "kind": "elevenlabs_voice",
    "phone_number": "+56229381400",
    "enabled": true,
    "test_calls": {
      "limit": 5,
      "used_today": 1,
      "remaining_today": 4
    },
    "greeting": {
      "disclosure": "Hola, le atiende un asistente virtual. Le comento que esta llamada puede ser grabada.",
      "brand": "Bienvenido a Mi Empresa",
      "composed": "Hola, le atiende un asistente virtual de Mi Empresa. Le comento que esta llamada puede ser grabada. ¿En qué le puedo ayudar hoy?"
    },
    "voice_id": "EXAVITQu4vr4xnSDxMaL",
    "voices": [
      {
        "voice_id": "EXAVITQu4vr4xnSDxMaL",
        "voice_settings": {
          "stability": 0.5,
          "similarity_boost": 0.8
        },
        "name": "Sarah",
        "region": "US",
        "language": "en",
        "description_es": "Suave y profesional. Voz femenina americana."
      },
      {
        "voice_id": "6Gr4AVmTax1pMJO0lHRK",
        "voice_settings": {
          "stability": 0.5,
          "similarity_boost": 0.8
        },
        "name": "Catalina",
        "region": "CL",
        "language": "es",
        "description_es": "Chilena, cálida y profesional. Suena como una persona real."
      }
    ],
    "handoff_destination": "+56912345678",
    "propagation": {
      "status": "synced",
      "error": null,
      "attempted_at": "2026-09-21T10:00:00.000Z"
    }
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /ai-agents/{id}/voice-channel

Update greeting brand segment / curated voice / handoff number (persists Vitrina-side, then best-effort speech-layer propagation)

Every field optional (at least one required). voice_id must be one of the ids GET /ai-agents/\{id\}/voice-channel’s curated_voices lists — a curated catalogue key, not a per-tenant resource, so it is validated server-side rather than typed as a uuid. Persists Vitrina-side FIRST, then best-effort pushes the change to the speech layer; a push failure leaves propagation.state non-synced rather than failing the request — retry with POST /ai-agents/\{id\}/voice-channel/propagate.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
greeting_brandstringmáx. 350
voice_idstringmín. 1, máx. 120
handoff_destinationstring | nullmáx. 40
curl -X PUT https://api.vitrinadev.com/api/v1/ai-agents/<id>/voice-channel \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "greeting_brand": "Bienvenido a Mi Empresa",
    "voice_id": "EXAVITQu4vr4xnSDxMaL",
    "handoff_destination": "+56912345678"
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "configured": true,
    "account_id": "a3a3a3a3-3000-4000-8000-000000000001",
    "kind": "elevenlabs_voice",
    "phone_number": "+56229381400",
    "enabled": true,
    "test_calls": {
      "limit": 5,
      "used_today": 1,
      "remaining_today": 4
    },
    "greeting": {
      "disclosure": "Hola, le atiende un asistente virtual. Le comento que esta llamada puede ser grabada.",
      "brand": "Bienvenido a Mi Empresa",
      "composed": "Hola, le atiende un asistente virtual de Mi Empresa. Le comento que esta llamada puede ser grabada. ¿En qué le puedo ayudar hoy?"
    },
    "voice_id": "EXAVITQu4vr4xnSDxMaL",
    "voices": [
      {
        "voice_id": "EXAVITQu4vr4xnSDxMaL",
        "voice_settings": {
          "stability": 0.5,
          "similarity_boost": 0.8
        },
        "name": "Sarah",
        "region": "US",
        "language": "en",
        "description_es": "Suave y profesional. Voz femenina americana."
      },
      {
        "voice_id": "6Gr4AVmTax1pMJO0lHRK",
        "voice_settings": {
          "stability": 0.5,
          "similarity_boost": 0.8
        },
        "name": "Catalina",
        "region": "CL",
        "language": "es",
        "description_es": "Chilena, cálida y profesional. Suena como una persona real."
      }
    ],
    "handoff_destination": "+56912345678",
    "propagation": {
      "status": "synced",
      "error": null,
      "attempted_at": "2026-09-21T10:00:00.000Z"
    }
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/voice-channel/propagate

Retry pushing the persisted voice config onto the speech layer (when propagation is pending)

Re-attempts the push a PUT already tried best-effort. Idempotent — calling it while already synced is a no-op that just returns the current state.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/voice-channel/propagate \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "configured": true,
    "account_id": "a3a3a3a3-3000-4000-8000-000000000001",
    "kind": "elevenlabs_voice",
    "phone_number": "+56229381400",
    "enabled": true,
    "test_calls": {
      "limit": 5,
      "used_today": 1,
      "remaining_today": 4
    },
    "greeting": {
      "disclosure": "Hola, le atiende un asistente virtual. Le comento que esta llamada puede ser grabada.",
      "brand": "Bienvenido a Mi Empresa",
      "composed": "Hola, le atiende un asistente virtual de Mi Empresa. Le comento que esta llamada puede ser grabada. ¿En qué le puedo ayudar hoy?"
    },
    "voice_id": "EXAVITQu4vr4xnSDxMaL",
    "voices": [
      {
        "voice_id": "EXAVITQu4vr4xnSDxMaL",
        "voice_settings": {
          "stability": 0.5,
          "similarity_boost": 0.8
        },
        "name": "Sarah",
        "region": "US",
        "language": "en",
        "description_es": "Suave y profesional. Voz femenina americana."
      },
      {
        "voice_id": "6Gr4AVmTax1pMJO0lHRK",
        "voice_settings": {
          "stability": 0.5,
          "similarity_boost": 0.8
        },
        "name": "Catalina",
        "region": "CL",
        "language": "es",
        "description_es": "Chilena, cálida y profesional. Suena como una persona real."
      }
    ],
    "handoff_destination": "+56912345678",
    "propagation": {
      "status": "synced",
      "error": null,
      "attempted_at": "2026-09-22T09:00:00.000Z"
    }
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/{id}/voice-lines

The agent's voice lines (ADR 0053): the PSTN number callers dial and the WhatsApp in-chat call button, each with its own state (none/activating/active/in_call/desync)

Two lines, always both present in the response even when never activated: pstn (a dedicated phone number) and whatsapp (the in-chat call button on the agent’s WhatsApp number). Each carries state (none | activating | active | in_call | desync) and, once provisioned, number.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
curl https://api.vitrinadev.com/api/v1/ai-agents/<id>/voice-lines \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "lines": [
      {
        "kind": "pstn",
        "state": "active",
        "e164": "+56229381400",
        "account_id": "a3a3a3a3-3000-4000-8000-000000000001",
        "activation": null,
        "desync": null,
        "call": null,
        "handoff_e164": "+56912345678",
        "extra_line_count": 0,
        "eligibility": {
          "can_activate": true,
          "blocked_reason": null,
          "remedies": []
        }
      },
      {
        "kind": "whatsapp",
        "state": "none",
        "e164": null,
        "account_id": null,
        "activation": null,
        "desync": null,
        "call": null,
        "handoff_e164": null,
        "extra_line_count": 0,
        "eligibility": {
          "can_activate": true,
          "blocked_reason": null,
          "remedies": []
        }
      }
    ]
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/voice-lines/{kind}/activate

Start the activation saga for one line. Async and idempotent; a second call while one is running returns refused_reason=already_running rather than starting a second saga

kind is pstn or whatsapp. Provisioning runs on the speech layer in the background, so this answers 200 with state: "activating" immediately, not once the line is live — poll GET /ai-agents/\{id\}/voice-lines for the eventual active. refused_reason is non-null and state unchanged when a saga is already running for this line.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
kindpathpstn \whatsapp
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/voice-lines/<id>/activate \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "kind": "pstn",
    "state": "activating"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ai-agents/{id}/voice-lines/{kind}/deactivate

Take one line off the air. REFUSES with 200 + refused_reason=call_in_progress while a customer is on the line — the call is never dropped

A REFUSAL here is a 200, not a 4xx — refused_reason: "call_in_progress" with state unchanged means exactly that: a live call is not interrupted by this call. Retry once the call ends.

ParámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
kindpathpstn \whatsapp
curl -X POST https://api.vitrinadev.com/api/v1/ai-agents/<id>/voice-lines/<id>/deactivate \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "kind": "pstn",
    "state": "in_call",
    "refused_reason": "call_in_progress"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ai-agents/tools-catalog

Tools this workspace may wire to an agent

The catalogue is TENANT-RESOLVED, not static: tools gated on a vertical or an unconnected integration are filtered out, so what comes back is what this workspace can actually use today. Connecting an integration or changing the vertical changes this list.

Names from here are what PUT /ai-agents/\{id\}/tools accepts.

curl https://api.vitrinadev.com/api/v1/ai-agents/tools-catalog \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "platform": [
      {
        "key": "search_vehicle_stock",
        "name": "search_vehicle_stock",
        "description": "Search the dealer's current vehicle stock to answer availability/price questions.",
        "kind": "platform",
        "available": true
      }
    ],
    "custom": [
      {
        "key": "consultar_clima",
        "name": "consultar_clima",
        "description": "Consulta el clima actual de una ciudad",
        "kind": "custom",
        "available": true
      }
    ]
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /change-requests/stats

The workspace's change-request ledger

The same two aggregates as GET /ai-agents/\{id\}/change-request-stats, across EVERY agent of the workspace. "Where is the product weak here" is a question about the workspace: one agent's harness ratio over a quarter is a handful of rows, and a tenant running three agents wants the number across them.

There is no platform-wide variant on purpose. ADR 0103 §1.6: memberships are owner | admin | supervisor | agent, there is no staff role and no superadmin, and everything in this product is tenant-facing by construction. Vitrina's cross-tenant read of the same two numbers is the service-role, read-only script scripts/ops/change-request-stats.ts.

Requires ai_agents:read AND corrections:read.

ParámetroEnTipoObligatorioRestricciones
daysqueryintegerno≥ 1, ≤ 365
curl https://api.vitrinadev.com/api/v1/change-requests/stats \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "days": 90,
    "total": 14,
    "byStatus": {
      "received": 2,
      "grounded": 1,
      "reproduced": 0,
      "proposed": 1,
      "applied": 3,
      "verified": 5,
      "ya_cumple": 1,
      "harness": 1
    },
    "harness": {
      "terminated": 1,
      "ratio": 0.0714
    },
    "appliedBySurface": [
      {
        "surface": "prompt",
        "count": 5
      },
      {
        "surface": "skill",
        "count": 2
      },
      {
        "surface": "knowledge",
        "count": 1
      }
    ],
    "appliedBySurfaceOverTime": [
      {
        "bucket": "2026-W37",
        "surface": "prompt",
        "count": 2
      },
      {
        "bucket": "2026-W37",
        "surface": "skill",
        "count": 1
      },
      {
        "bucket": "2026-W38",
        "surface": "prompt",
        "count": 3
      },
      {
        "bucket": "2026-W38",
        "surface": "skill",
        "count": 1
      },
      {
        "bucket": "2026-W38",
        "surface": "knowledge",
        "count": 1
      }
    ]
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /skills

List the tenant skill library (with per-agent attachment counts)

Every skill in the workspace — the library is shared, not owned by one agent — with agent_count (how many agents currently have it attached). channel_overrides carries per-channel adjustments to the base content (today: voice, ADR 0039).

curl https://api.vitrinadev.com/api/v1/skills \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "a4a4a4a4-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "Agendar hora",
      "description": "Cómo ofrecer y confirmar horarios disponibles.",
      "content": "Cuando el cliente quiera agendar, ofrece los horarios disponibles y confirma antes de reservar.",
      "status": "active",
      "channel_overrides": {},
      "created_by": "11111111-0000-4000-8000-000000000001",
      "created_at": "2026-09-04T19:10:00.000Z",
      "updated_at": "2026-09-04T19:10:00.000Z",
      "deleted_at": null,
      "agent_count": 2
    }
  ],
  "meta": {
    "total": 1
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /skills

Create a skill (optional channel_overrides, v1 key: voice)

Adds a new playbook to the library. It is not attached to any agent yet — attach it with POST /ai-agents/\{id\}/skills, or reach it through POST /ai-agents/\{id\}/onboarding/materialise-skills. Answers 201. Fires skill.created.

Cuerpo

CampoTipoObligatorioRestricciones
namestringmín. 1, máx. 120
descriptionstring | nullmáx. 2000
contentstringmáx. 20000, por defecto ""
channel_overridesobject
statusactive \inactive
curl -X POST https://api.vitrinadev.com/api/v1/skills \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Agendar hora",
    "content": "Cuando el cliente quiera agendar, ofrece los horarios disponibles y confirma antes de reservar."
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "id": "a4a4a4a4-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Agendar hora",
    "description": "Cómo ofrecer y confirmar horarios disponibles.",
    "content": "Cuando el cliente quiera agendar, ofrece los horarios disponibles y confirma antes de reservar.",
    "status": "active",
    "channel_overrides": {},
    "created_by": "11111111-0000-4000-8000-000000000001",
    "created_at": "2026-09-04T19:10:00.000Z",
    "updated_at": "2026-09-04T19:10:00.000Z",
    "deleted_at": null
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /skills/{id}

Delete a skill (SOFT; detaches it from every agent)

ADR 0093 — a soft delete. The skill leaves the library and every agent it was attached to (its skill_agent rows are removed, so the runtime stops injecting it immediately), but the row survives with status: "deleted" and a deleted_at stamp.

It survives because every ai_agent_version references its skills by id: 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 skill instead.

204 on success, 404 for an id that does not exist in this workspace. Deleting an already-deleted skill is a 204 no-op. Fires skill.deleted.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl -X DELETE https://api.vitrinadev.com/api/v1/skills/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429

GET /skills/{id}

Read one skill (base content + per-channel adjustments map)

The full row: base content and the channel_overrides map.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl https://api.vitrinadev.com/api/v1/skills/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a4a4a4a4-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Agendar hora",
    "description": "Cómo ofrecer y confirmar horarios disponibles.",
    "content": "Cuando el cliente quiera agendar, ofrece los horarios disponibles y confirma antes de reservar.",
    "status": "active",
    "channel_overrides": {},
    "created_by": "11111111-0000-4000-8000-000000000001",
    "created_at": "2026-09-04T19:10:00.000Z",
    "updated_at": "2026-09-04T19:10:00.000Z",
    "deleted_at": null
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /skills/{id}

Update a skill; channel_overrides replaces the whole map when present, omit to leave adjustments untouched

Content-only edit — applies instantly to every agent the skill is attached to; there is no draft/publish step for a skill the way there is for an agent. channel_overrides, when present, REPLACES the whole map rather than merging into it. Fires skill.updated.

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
namestringmín. 1, máx. 120
descriptionstring | nullmáx. 2000
contentstringmáx. 20000
channel_overridesobject
statusactive \inactive
curl -X PUT https://api.vitrinadev.com/api/v1/skills/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Cuando el cliente quiera agendar, ofrece SOLO horarios de la sucursal más cercana y confirma antes de reservar."
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a4a4a4a4-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Agendar hora",
    "description": "Cómo ofrecer y confirmar horarios disponibles.",
    "content": "Cuando el cliente quiera agendar, ofrece SOLO horarios de la sucursal más cercana y confirma antes de reservar.",
    "status": "active",
    "channel_overrides": {},
    "created_by": "11111111-0000-4000-8000-000000000001",
    "created_at": "2026-09-04T19:10:00.000Z",
    "updated_at": "2026-09-04T19:10:00.000Z",
    "deleted_at": null
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

En esta página

GET /ai-agent-graphsPOST /ai-agent-graphsDELETE /ai-agent-graphs/{id}GET /ai-agent-graphs/{id}PATCH /ai-agent-graphs/{id}DELETE /ai-agent-graphs/{id}/draftGET /ai-agent-graphs/{id}/draftPUT /ai-agent-graphs/{id}/draftPOST /ai-agent-graphs/{id}/nodesDELETE /ai-agent-graphs/{id}/nodes/{nodeId}PATCH /ai-agent-graphs/{id}/nodes/{nodeId}POST /ai-agent-graphs/{id}/publishGET /ai-agent-graphs/{id}/versionsPOST /ai-agent-graphs/{id}/versions/{version}/restoreGET /ai-agentsPOST /ai-agentsDELETE /ai-agents/{id}GET /ai-agents/{id}PUT /ai-agents/{id}GET /ai-agents/{id}/change-request-statsGET /ai-agents/{id}/change-requestsPOST /ai-agents/{id}/change-requestsGET /ai-agents/{id}/change-requests/{crId}POST /ai-agents/{id}/change-requests/{crId}/groundPOST /ai-agents/{id}/change-requests/{crId}/ground/confirmPOST /ai-agents/{id}/change-requests/{crId}/proposePOST /ai-agents/{id}/change-requests/{crId}/scenarioPOST /ai-agents/{id}/change-requests/{crId}/scenario/evaluatePOST /ai-agents/{id}/change-requests/{crId}/scenario/refinePOST /ai-agents/{id}/change-requests/{crId}/transitionPOST /ai-agents/{id}/cloneDELETE /ai-agents/{id}/draftGET /ai-agents/{id}/draftPUT /ai-agents/{id}/draftGET /ai-agents/{id}/exportGET /ai-agents/{id}/knowledgePOST /ai-agents/{id}/knowledgeDELETE /ai-agents/{id}/knowledge/{fileId}GET /ai-agents/{id}/metricsPOST /ai-agents/{id}/publishGET /ai-agents/{id}/publish-gateGET /ai-agents/{id}/skillsPOST /ai-agents/{id}/skillsDELETE /ai-agents/{id}/skills/{skillId}GET /ai-agents/{id}/system-promptPUT /ai-agents/{id}/system-promptGET /ai-agents/{id}/toolsPUT /ai-agents/{id}/toolsGET /ai-agents/{id}/versionsGET /ai-agents/{id}/versions/{version}POST /ai-agents/{id}/versions/{version}/restorePOST /ai-agents/{id}/versions/{version}/rollbackGET /ai-agents/{id}/voice-channelPUT /ai-agents/{id}/voice-channelPOST /ai-agents/{id}/voice-channel/propagateGET /ai-agents/{id}/voice-linesPOST /ai-agents/{id}/voice-lines/{kind}/activatePOST /ai-agents/{id}/voice-lines/{kind}/deactivateGET /ai-agents/tools-catalogGET /change-requests/statsGET /skillsPOST /skillsDELETE /skills/{id}GET /skills/{id}PUT /skills/{id}