VitrinaAPI

Herramientas personalizadas

El Tool Store: enseñarle al agente a llamar la API propia del workspace, sin escribir código — una plantilla de request más una lista de parámetros.

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/custom-toolsList the workspace’s custom tools
POST/custom-toolsCreate a custom tool
DELETE/custom-tools/{id}Delete a custom tool
GET/custom-tools/{id}Fetch one custom tool
PATCH/custom-tools/{id}Update a custom tool
PUT/custom-tools/{id}Replace a custom tool
POST/custom-tools/{id}/infer-schemaDerive a response schema from the saved sample
POST/custom-tools/{id}/testInvoke the tool with sample arguments

GET /custom-tools

List the workspace’s custom tools

Every tool with its parameters, auth configuration and request template, plus a total. Credential VALUES are not here — auth_config carries a credential_id reference, not a secret.

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

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "a7a7a7a7-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "consultar_clima",
      "description": "Consulta el clima actual de una ciudad",
      "kind": "custom_http",
      "enabled": true,
      "parameters": [
        {
          "name": "ciudad",
          "type": "string",
          "description": "Nombre de la ciudad",
          "required": true
        }
      ],
      "auth_type": "none",
      "auth_config": {},
      "request_template": {
        "method": "GET",
        "url": "https://api.example.com/weather?city=${param.ciudad}",
        "query": [],
        "headers": [],
        "body_kind": "none"
      },
      "response_schema": null,
      "sample_response": null,
      "timeout_ms": 15000,
      "volatile": false,
      "created_at": "2026-09-15T12:00:00.000Z",
      "updated_at": "2026-09-15T12:00:00.000Z"
    }
  ],
  "meta": {
    "total": 1
  }
}

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

POST /custom-tools

Create a custom tool

name is the function name the model sees and must be snake_case; it is unique per workspace, and a collision is a 409. description is what the model reads to decide whether to call the tool, so it is required and is prompt text rather than documentation.

Each entry in parameters becomes an argument the model can supply, and its description is likewise read by the model. Their values reach the request as $\{param.NAME\} placeholders anywhere in the URL, query, headers or body.

Any auth_type other than none requires auth_config.credential_id pointing at a stored credential (/tool-credentials). Secrets are never written inline here — the template references them as $\{secret.NAME\} and the runtime substitutes them.

The template is validated on the way in: the URL must be absolute http(s), a GET may not carry a body, and a json body must parse once placeholders are substituted. timeout_ms defaults to 15.000 and is capped at 60.000. Answers 201.

Cuerpo

CampoTipoObligatorioRestricciones
namestringmín. 2, máx. 64, patrón ^[a-z][a-z0-9_]{1,63}$
descriptionstringmín. 1, máx. 500
parametersobject[]por defecto []
auth_typenone \api_key \bearer \
auth_configobjectpor defecto {}
request_templateobject
response_schemaany
timeout_msinteger≥ 500, ≤ 60000, por defecto 15000
enabledbooleanpor defecto true
volatilebooleanpor defecto false
curl -X POST https://api.vitrinadev.com/api/v1/custom-tools \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "consultar_clima",
    "description": "Consulta el clima actual de una ciudad",
    "parameters": [
      {
        "name": "ciudad",
        "type": "string",
        "description": "Nombre de la ciudad",
        "required": true
      }
    ],
    "request_template": {
      "method": "GET",
      "url": "https://api.example.com/weather?city=${param.ciudad}"
    }
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "id": "a7a7a7a7-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "consultar_clima",
    "description": "Consulta el clima actual de una ciudad",
    "kind": "custom_http",
    "enabled": true,
    "parameters": [
      {
        "name": "ciudad",
        "type": "string",
        "description": "Nombre de la ciudad",
        "required": true
      }
    ],
    "auth_type": "none",
    "auth_config": {},
    "request_template": {
      "method": "GET",
      "url": "https://api.example.com/weather?city=${param.ciudad}",
      "query": [],
      "headers": [],
      "body_kind": "none"
    },
    "response_schema": null,
    "sample_response": null,
    "timeout_ms": 15000,
    "volatile": false,
    "created_at": "2026-09-15T12:00:00.000Z",
    "updated_at": "2026-09-15T12:00:00.000Z"
  }
}

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

DELETE /custom-tools/{id}

Delete a custom tool

Removes the tool and its parameters. Past tool_invocation audit rows survive — the record of what was called stays even when the tool itself is gone.

Agents wired to the tool lose it. Setting enabled: false is the reversible way to take a tool out of service.

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

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

GET /custom-tools/{id}

Fetch one custom tool

The full definition including sample_response and response_schema when they have been captured.

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

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a7a7a7a7-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "consultar_clima",
    "description": "Consulta el clima actual de una ciudad",
    "kind": "custom_http",
    "enabled": true,
    "parameters": [
      {
        "name": "ciudad",
        "type": "string",
        "description": "Nombre de la ciudad",
        "required": true
      }
    ],
    "auth_type": "none",
    "auth_config": {},
    "request_template": {
      "method": "GET",
      "url": "https://api.example.com/weather?city=${param.ciudad}",
      "query": [],
      "headers": [],
      "body_kind": "none"
    },
    "response_schema": null,
    "sample_response": null,
    "timeout_ms": 15000,
    "volatile": false,
    "created_at": "2026-09-15T12:00:00.000Z",
    "updated_at": "2026-09-15T12:00:00.000Z"
  }
}

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

PATCH /custom-tools/{id}

Update a custom tool

Applies only the fields present and leaves everything else as it is — this is the safe way to change one thing. Prefer it over PUT unless you genuinely hold the whole object.

parameters and request_template are each replaced wholesale when sent, never merged, so a parameter list must be sent complete. Any auth_type other than none requires auth_config.credential_id pointing at a stored credential (/tool-credentials). Secrets are never written inline here — the template references them as $\{secret.NAME\} and the runtime substitutes them.

Changes take effect on the agent’s next call; nothing needs republishing.

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
namestringmín. 2, máx. 64, patrón ^[a-z][a-z0-9_]{1,63}$
descriptionstringmín. 1, máx. 500
parametersobject[]por defecto []
auth_typenone \api_key \bearer \
auth_configobjectpor defecto {}
request_templateobject
response_schemaany
timeout_msinteger≥ 500, ≤ 60000, por defecto 15000
enabledbooleanpor defecto true
volatilebooleanpor defecto false
curl -X PATCH https://api.vitrinadev.com/api/v1/custom-tools/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Consulta el clima actual (°C) de una ciudad chilena"
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a7a7a7a7-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "consultar_clima",
    "description": "Consulta el clima actual (°C) de una ciudad chilena",
    "kind": "custom_http",
    "enabled": true,
    "parameters": [
      {
        "name": "ciudad",
        "type": "string",
        "description": "Nombre de la ciudad",
        "required": true
      }
    ],
    "auth_type": "none",
    "auth_config": {},
    "request_template": {
      "method": "GET",
      "url": "https://api.example.com/weather?city=${param.ciudad}",
      "query": [],
      "headers": [],
      "body_kind": "none"
    },
    "response_schema": null,
    "sample_response": null,
    "timeout_ms": 15000,
    "volatile": false,
    "created_at": "2026-09-15T12:00:00.000Z",
    "updated_at": "2026-09-15T12:00:00.000Z"
  }
}

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

PUT /custom-tools/{id}

Replace a custom tool

A true REPLACE, and the difference from PATCH is easy to get wrong. This endpoint validates against the CREATE schema, so every field you omit is filled with its default and written — omitting parameters empties the parameter list, omitting timeout_ms resets it to 15.000, and omitting enabled re-enables the tool.

Omitting auth_type sets it to none and clears auth_config, so the tool silently stops authenticating and starts calling the upstream API unauthenticated. Send the complete object, or use PATCH for partial edits.

The one field that does NOT reset is response_schema, which has no default and is left alone when absent.

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
namestringmín. 2, máx. 64, patrón ^[a-z][a-z0-9_]{1,63}$
descriptionstringmín. 1, máx. 500
parametersobject[]por defecto []
auth_typenone \api_key \bearer \
auth_configobjectpor defecto {}
request_templateobject
response_schemaany
timeout_msinteger≥ 500, ≤ 60000, por defecto 15000
enabledbooleanpor defecto true
volatilebooleanpor defecto false
curl -X PUT https://api.vitrinadev.com/api/v1/custom-tools/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "consultar_clima",
    "description": "Consulta el clima actual de una ciudad",
    "parameters": [
      {
        "name": "ciudad",
        "type": "string",
        "description": "Nombre de la ciudad",
        "required": true
      }
    ],
    "request_template": {
      "method": "GET",
      "url": "https://api.example.com/weather?city=${param.ciudad}"
    }
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a7a7a7a7-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "consultar_clima",
    "description": "Consulta el clima actual de una ciudad",
    "kind": "custom_http",
    "enabled": true,
    "parameters": [
      {
        "name": "ciudad",
        "type": "string",
        "description": "Nombre de la ciudad",
        "required": true
      }
    ],
    "auth_type": "none",
    "auth_config": {},
    "request_template": {
      "method": "GET",
      "url": "https://api.example.com/weather?city=${param.ciudad}",
      "query": [],
      "headers": [],
      "body_kind": "none"
    },
    "response_schema": null,
    "sample_response": null,
    "timeout_ms": 15000,
    "volatile": false,
    "created_at": "2026-09-15T12:00:00.000Z",
    "updated_at": "2026-09-15T12:00:00.000Z"
  }
}

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

POST /custom-tools/{id}/infer-schema

Derive a response schema from the saved sample

Generates a JSON Schema from the stored sample_response, saves it as the tool’s response_schema, and returns the updated tool. Takes no body — it works from what is already stored, so run /\{id\}/test with save_sample: true first. Without a sample it is a 400 saying so.

Inference sees ONE response. A field that happened to be null, or an array that happened to be empty, is typed from that single example, so the result is a starting point to edit rather than a finished contract.

The schema is used to set schema_valid on future invocations. It REPORTS a mismatch; it does not reject the response or stop the agent from using it.

ParámetroEnTipoObligatorioRestricciones
idpathuuid
curl -X POST https://api.vitrinadev.com/api/v1/custom-tools/<id>/infer-schema \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "id": "a7a7a7a7-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "consultar_clima",
    "description": "Consulta el clima actual de una ciudad",
    "kind": "custom_http",
    "enabled": true,
    "parameters": [
      {
        "name": "ciudad",
        "type": "string",
        "description": "Nombre de la ciudad",
        "required": true
      }
    ],
    "auth_type": "none",
    "auth_config": {},
    "request_template": {
      "method": "GET",
      "url": "https://api.example.com/weather?city=${param.ciudad}",
      "query": [],
      "headers": [],
      "body_kind": "none"
    },
    "response_schema": {
      "type": "object",
      "properties": {
        "ciudad": {
          "type": "string"
        },
        "temp_c": {
          "type": "number"
        },
        "condicion": {
          "type": "string"
        }
      }
    },
    "sample_response": null,
    "timeout_ms": 15000,
    "volatile": false,
    "created_at": "2026-09-15T12:00:00.000Z",
    "updated_at": "2026-09-15T12:00:00.000Z"
  }
}

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

POST /custom-tools/{id}/test

Invoke the tool with sample arguments

This performs the real request. It is a tester, not a dry run: the configured URL is called with the real credential through the same runtime the agent uses, so testing a tool whose template is a POST or DELETE creates or destroys data on the upstream system exactly as a live call would. There is no sandbox mode.

Answers 200 with the outcome, pass or fail: \{ ok, status, latency_ms, body, body_truncated, error, redacted_request, schema_valid, invocation_id \}. Read ok, not the HTTP status — a timeout, a refused SSRF target or a 500 from the upstream all come back as a 200 here with ok: false.

ok is true only for a 2xx with no transport error. Redirects are not followed, so a 301 is reported as-is and counts as a failure. schema_valid is null when the tool has no response_schema.

redacted_request shows exactly what was sent with credential-derived values replaced — the right thing to show an author debugging a template, and safe to display. The same snapshot is what lands in the audit row, tagged triggered_by: "tester".

save_sample: true stores the response as sample_response for /infer-schema, but only when the call succeeded — a failed test never overwrites a good sample. Requires functions:write, because it can write both the sample and the upstream system.

ParámetroEnTipoObligatorioRestricciones
idpathuuid

Cuerpo

CampoTipoObligatorioRestricciones
sample_argsobjectpor defecto {}
save_samplebooleanpor defecto false
curl -X POST https://api.vitrinadev.com/api/v1/custom-tools/<id>/test \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "arguments": {
      "ciudad": "Santiago"
    }
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "ok": true,
    "status": 200,
    "latency_ms": 214,
    "body": {
      "ciudad": "Santiago",
      "temp_c": 18,
      "condicion": "despejado"
    },
    "body_truncated": false,
    "error": null,
    "redacted_request": {
      "method": "GET",
      "url": "https://api.example.com/weather?city=Santiago"
    },
    "schema_valid": null,
    "invocation_id": "a7a7a7a7-3000-4000-8000-000000000001"
  }
}

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

En esta página