<!-- https://vatio.ai/es/guias/conectar-tu-backend · Actualizado: 2026-10-11 -->

> Dale tu API a tu agente de IA con tools HTTP en YAML, y deja que los usuarios con sesión vean sus datos con un JWT que tu backend firma y Vatio verifica.

# Conecta tu agente de IA a tu backend

Para que un agente de IA trabaje con datos reales, le das tu API como tools: cada tool es un request HTTP descrito en un archivo YAML, con los parámetros que llena el modelo y los secrets que necesita el request. Con Vatio listas esas tools en `vatio.yml`, y el mismo agente las llama en WhatsApp, Instagram y la web. Para los datos de un visitante con sesión iniciada, tu backend firma un JWT y Vatio solo lo verifica, así que una tool privada lee los datos de esa persona y de nadie más.

Esta guía conecta de punta a punta la API de una tienda de bicicletas: secrets, una tool que lee, una tool que actúa, lo que vuelve, y tools privadas para clientes con sesión.

## Por qué el agente necesita tu API

Sin tools, un agente solo puede repetir sus instrucciones y su conocimiento. Si le preguntas si hay stock de la Trail 29 en talla M, o reconoce que no sabe o adivina, y adivinar un stock, un precio o una reserva es lo más caro que puede decir un agente.

El conocimiento es para lo que cambia lento: políticas, horarios, cómo funciona un servicio. Las tools son para lo que es cierto ahora mismo (stock, el estado de un pedido, horas libres) y para todo lo que hace algo, como agendar. Tu backend sigue siendo la fuente de verdad y sigue haciendo las validaciones; Vatio opera la conversación y hace la llamada. Cada llamada queda visible en el inbox, junto a la respuesta que produjo.

## Cómo funciona una tool

1. Una tool es un archivo, `tools/<clave>.yml`; el nombre del archivo es la clave. Se activa cuando su clave está en `agent.tools`.
2. El modelo lee `description` y `when_to_use`, decide llamarla y llena `parameters`, un JSON schema.
3. Vatio arma el request, reemplaza los placeholders y lo envía desde sus propios servidores.
4. `respond` elige qué ve el modelo, y el modelo escribe la respuesta a partir de `result: ok` o `result: error`.

Una tool es un solo request a propósito. Las condiciones, una segunda llamada o una respuesta calculada son código: van en tu backend, detrás de un endpoint que haga el trabajo completo.

## Guarda la dirección y la llave como secrets

```bash
vatio secrets set PEDALIA_API https://api.pedalia.example
vatio secrets set PEDALIA_API_KEY
```

Si omites el valor, `set` te lo pide con la entrada oculta. Las tools leen los secrets como `$env.KEY`, los cambios aplican desde la siguiente llamada sin hacer deploy, y `vatio secrets list` muestra nombres, nunca valores.

## Una tool que lee

`tools/consultar_stock.yml`:

```yaml
description: Consulta stock y precio de una bicicleta, repuesto o accesorio.
when_to_use: El cliente pregunta si algo está disponible o cuánto cuesta.
parameters:
  type: object
  properties:
    producto: { type: string, description: "Nombre o modelo del producto" }
  required: [producto]
access: public
request:
  method: GET
  base_url: "$env.PEDALIA_API"
  path: /productos
  requires: [params.producto]
  headers:
    Authorization: "Bearer $env.PEDALIA_API_KEY"
  query:
    q: "$params.producto"
respond:
  data:
    productos: "$.data"
  message:
    when_empty: Ningún producto coincide con ese nombre.
    default: "Encontré {{count}} productos."
```

Los valores en `base_url`, `path`, `headers`, `query` y `body` aceptan estos placeholders:

| Placeholder | Origen |
|---|---|
| `$params.<nombre>` | Los argumentos de la tool, que llena el modelo |
| `$env.<KEY>` | Los secrets del workspace |
| `$auth.subject`, `$auth.token`, `$auth.claims.<clave>` | El visitante verificado, en tools privadas |
| `$contact.name`, `$contact.email`, `$contact.phone_number` | El perfil del contacto |

## Una tool que actúa

`tools/agendar_servicio.yml`:

```yaml
description: Agenda un servicio de taller para la bicicleta del cliente.
when_to_use: El cliente eligió un servicio y un día, y confirmó su nombre.
parameters:
  type: object
  properties:
    servicio: { type: string, description: "p. ej. ajuste de frenos, servicio completo" }
    fecha: { type: string, description: "YYYY-MM-DD" }
    nombre: { type: string }
    telefono: { type: string, description: "Solo Instagram y web. En WhatsApp la tool usa el número del contacto" }
  required: [servicio, fecha]
access: public
request:
  method: POST
  base_url: "$env.PEDALIA_API"
  path: /reservas-taller
  requires: [params.servicio, params.fecha]
  headers:
    Authorization: "Bearer $env.PEDALIA_API_KEY"
  body:
    servicio: "$params.servicio"
    fecha: "$params.fecha"
    nombre: "$params.nombre || $contact.name"
    telefono: "$params.telefono || $contact.phone_number"
respond:
  data:
    reserva: "$.reserva"
```

Los valores que faltan se omiten del request, así que lista en `requires` lo que el endpoint no puede dejar de recibir, y la tool falla antes de enviar media reserva. `||` toma el primer valor que no esté vacío: lo que el cliente acaba de decir, o lo que ya tiene el perfil del contacto. En WhatsApp el perfil ya tiene el teléfono del cliente, así que el agente lo pide solo en Instagram y en la web. Una tool que se usa solo en WhatsApp puede omitir el parámetro: envía `$contact.phone_number` y pon `contact.phone_number` en `requires`, y si alguien oculta su número detrás de un nombre de usuario de WhatsApp, Vatio se lo pide con un botón de un toque.

## Lo que recibe el agente

Toda tool devuelve `result` (`ok` o `error`) y un `message` que no está vacío:

```json
{ "result": "error", "message": "El sábado está lleno. El lunes a las 10:00 está libre." }
```

`respond.data` mapea claves de salida a rutas JSON: `$` es el cuerpo completo, `$.data` su campo `data`. `{{count}}` es el largo del primer arreglo en los datos mapeados. Sin `respond.message`, el mensaje es el campo `message` de tu propia API, así que tu backend puede decir lo que pasó con sus propias palabras.

**Devuelve status HTTP reales.** Un 2xx se vuelve `ok` y cualquier otro, `error`. Un `409` cuando el sábado se llenó hace un segundo se convierte en un honesto «el lunes a las 10:00 está libre», mientras que una falla escondida dentro de un `200` se convierte en la confirmación convincente de una reserva que no existe. Una búsqueda vacía sigue siendo `ok`: funcionó, y la respuesta fue no. El modelo ve los resultados de las tools, así que nunca devuelvas una credencial en uno.

## Actívalas en `vatio.yml`

```yaml
workspace: pedalia
agent:
  name: Pedalia Bicicletas
  instructions: |
    Ayudas a los clientes de Pedalia Bicicletas con stock, precios,
    reservas de taller y sus pedidos.

    Stock y precios: llama a consultar_stock. Nunca digas un precio ni un
    stock que consultar_stock no haya devuelto.

    Taller: pregunta el servicio y el día, después el nombre del cliente
    y, salvo en WhatsApp, su teléfono. Repítele la reserva antes de llamar
    a agendar_servicio. Solo di que quedó agendado cuando agendar_servicio
    lo haya devuelto.

    Pedidos: llama a mis_pedidos. Si no puede confirmar quién es el
    cliente, pídele que inicie sesión en pedalia.example.
  tools: [consultar_stock, agendar_servicio, mis_pedidos, identify_contact]
auth:
  public_key: identity.pub
```

Las tools incorporadas se activan de la misma forma: `knowledge_lookup`, `identify_contact` (guarda un nombre que da el visitante), `request_contact_info` (le pide a un visitante de WhatsApp que comparta su número) y `remember` y `recall` para la memoria sobre el contacto. `handoff` se agrega al declarar `agent.handoff`.

## Tools privadas: los datos del propio visitante

`consultar_stock` y `agendar_servicio` son **públicas**: corren para cualquiera. `mis_pedidos` es **privada**: solo corre para un visitante cuya identidad firmó tu backend.

```bash
vatio auth --new-key
```

Eso escribe dos archivos, y van a lugares distintos. **`identity.pub` se queda en el workspace**, se commitea y la nombra el bloque `auth:` de arriba. **`identity.pem` va a tu backend** como secreto, por ejemplo en una variable de entorno `VATIO_IDENTITY_PRIVATE_KEY`, y después borras el archivo del directorio del workspace. **Nunca hagas `vatio secrets set` de la clave privada**: ese almacén lo lee Vatio, y con tu clave privada ahí Vatio podría emitir identidades en vez de solo verificarlas.

Tu backend firma el token con **RS256**:

| Claim | Obligatorio | Valor |
|---|---|---|
| `sub` | sí | El id de tu usuario. Se convierte en `$auth.subject` |
| `aud` | sí | El slug de tu workspace, exacto |
| `exp` | sí | Segundos Unix. Que sea corto: se revisa de nuevo en cada llamada a una tool |
| `name`, `email`, `phone_number` | no | Llenan el perfil del contacto |
| `metadata` | no | Un link `profile` y `badges` que el inbox muestra sobre la persona |

Cualquier otro claim llega como `$auth.claims.<nombre>`. `aud` es obligatorio porque una firma prueba quién firmó, no para qué: sin él, un token de recuperación de contraseña firmado con la misma llave pasaría como identidad.

```js
// Node (jsonwebtoken)
import jwt from "jsonwebtoken";

const visitorToken = jwt.sign(
  { sub: String(user.id), name: user.name, email: user.email },
  process.env.VATIO_IDENTITY_PRIVATE_KEY,
  { algorithm: "RS256", audience: "pedalia", expiresIn: "1h" }
);
```

`tools/mis_pedidos.yml` le reenvía ese token a tu API:

```yaml
description: Los pedidos recientes del cliente con sesión iniciada y su estado.
when_to_use: Un cliente con sesión pregunta por sus propios pedidos o envíos.
access: private
request:
  method: GET
  base_url: "$env.PEDALIA_API"
  path: /me/pedidos
  headers:
    Authorization: "Bearer $auth.token"
respond:
  data:
    pedidos: "$.pedidos"
  message:
    when_empty: Este cliente todavía no tiene pedidos.
    default: "Encontré {{count}} pedidos."
```

Tu endpoint verifica el JWT con la clave pública y acota la consulta a su `sub`. Fíjate en lo que la tool no recibe: un id de cliente. **Una tool privada nunca recibe el id del usuario como parámetro**, porque los parámetros los llena el modelo y se le puede convencer de llenar ese con el de otra persona. Vatio rechaza el deploy:

```text
tool mis_pedidos: a private tool cannot take "user_id" as a parameter — the model
fills parameters and can be talked into filling that one with someone else's id.
Use $auth.subject, which comes from the verified token
```

Lo mismo vale para `customer_id`, `account_id`, `member_id`, `patient_id` y `subject`.

### Pásale el token al widget

Pon el token en el script del widget solo para los visitantes con sesión. Los que no la tienen no reciben token, y el agente les responde con sus tools públicas.

```html
<script async src="https://cdn.vatio.ai/v1/widget.js"
        data-workspace="pedalia"
        data-token="vatpub_..."
        data-visitor-token="JWT_FIRMADO"></script>
```

Cuando alguien inicia sesión sin recargar la página, llama a `window.VatioWidget.identify(nuevoToken)` (`null` cierra la sesión). La conversación que ya tenía sigue, ahora con un nombre; un token con otro `sub` empieza una nueva, así que en un dispositivo compartido un cliente nunca ve el chat de otro. Pon un token nuevo en cada carga de página para que una conversación larga sobreviva al primer `exp`. El SDK de navegador recibe el mismo token como `visitorToken`, y el SDK de iOS como `VatioChat(vatio, visitorToken:)`.

### Si tus usuarios inician sesión con Supabase

Sáltate el par de claves, la firma y el `data-visitor-token`. Corre:

```bash
vatio auth --supabase --sign-in-url https://pedalia.example/login
```

O haz clic en **Conectar Supabase** en **Canales → Supabase**, en el inbox, y elige el proyecto; el comando lo usa después. Encuentra el proyecto en tu `.env` (o en el cliente que genera Lovable), escribe `auth: supabase: https://<ref>.supabase.co` en `vatio.yml`, y guarda `SUPABASE_URL` y `SUPABASE_ANON_KEY` como secrets. Desde ahí el widget lee la sesión de Supabase que tu página ya tiene, sigue los inicios y cierres de sesión y las renovaciones del token, y en WhatsApp e Instagram el botón **Iniciar sesión** abre tu página de login, donde el widget entrega la sesión. Una tool privada llama a Supabase como el visitante, así que tu **Row Level Security** decide qué ve:

```yaml
access: private
request:
  method: GET
  base_url: "$env.SUPABASE_URL"
  path: /rest/v1/orders
  query:
    select: "*"
  headers:
    apikey: "$env.SUPABASE_ANON_KEY"
    Authorization: "Bearer $auth.token"
```

El proyecto tiene que firmar con JWT signing keys (ES256), no con el secret legado; si no, el push te dice cómo cambiarlo. Ve [Supabase](https://vatio.ai/docs/integrations/supabase).

## Pruébalo

```bash
vatio diff
vatio push
vatio chat "¿Tienen la Trail 29 en talla M?"
vatio chat "Agéndame un ajuste de frenos el sábado, soy Ana, 5555 0142"
vatio chat --new "¿Dónde está mi pedido?"
vatio chat show
```

Ningún comando invoca una tool directamente: las tools se prueban con conversaciones, y `chat show` muestra cada llamada, sus argumentos y lo que respondió tu backend. La CLI habla como tú, el developer, no como un cliente, así que las tools privadas se prueban en una página tuya: crea un token con `vatio tokens create --env preview --origin http://localhost:3000`, inicia sesión y lee el chat con `vatio chat show CHAT_ID`. Un token rechazado (otra llave, vencido, `aud` equivocado) deja al visitante como anónimo en vez de fallar, y `chat show` dice por qué.

Cuando ya hay gente real hablando con el agente, la página **Herramientas** del inbox muestra cómo ha respondido tu backend: la tasa de error de cada tool, las que no respondieron a tiempo, la latencia p50/p95 y las llamadas que fallaron con lo que devolvió tu backend. Una tool que falla casi nunca se nota en la conversación, porque el agente se disculpa y sigue, así que ahí es donde te enteras.

Tu coding agent lee la misma página desde la terminal: `vatio tools --env live` la imprime como Markdown que empieza diciendo cómo trabajarla, o como JSON con `--json`, sobre la última hora, el último día, 7 o 30 días (`--period 1h`). Cada llamada que falló trae el `vatio chat show` que abre su conversación, así que el agente pasa de "esta tool falla el 4% de las veces" a la petición, la respuesta y el arreglo en tu backend sin que copies nada del inbox.

## Errores comunes

- **WhatsApp e Instagram son anónimos hasta que el contacto inicia sesión.** No hay una página donde renderizar un token, así que agrega `sign_in_url` a `auth:` y una tool privada primero le envía al contacto un botón **Iniciar sesión** hacia tu sitio; mira [En WhatsApp e Instagram](https://vatio.ai/docs/authentication/sessions#on-whatsapp). Para que un cliente que no ha iniciado sesión revise un pedido, usa una tool pública que reciba el número de pedido y un segundo dato, como el email del pedido, y que **tu backend verifique que coincidan**. O envíale un link a la página con sesión.
- **Las tools no llegan a `localhost`.** Corren en los servidores de Vatio. Expón tu backend local con un túnel y define su URL como secret solo de preview: `vatio secrets set PEDALIA_API https://pedalia-dev.tunnel.example --env preview`.
- **Los evals llaman a tus tools.** Apúntalas a un sandbox con un valor de pruebas (`vatio secrets set KEY URL --tests`); una escritura cuya URL no tiene uno se simula en vez de enviarse, así que una repetición nunca agenda un servicio de verdad.
- **Un endpoint por trabajo.** Si responder requiere buscar al cliente, después su pedido y después el courier, escribe un endpoint que responda la pregunta. Tres tools encadenadas dejan la lógica en un prompt.
- **Las validaciones viven en código.** Si una reserva le pertenece a quien la pide, lo decide tu backend, nunca las instrucciones.

## Qué sigue

Si el agente todavía no existe, empieza por [Construir un agente de IA](/es/guias/construir-un-agente-de-ia). Cuando los clientes ya lo usan, los supervisores reportan las respuestas malas y tu coding agent corrige la causa, que muchas veces es una respuesta de tool a la que le falta un campo: mira [Mejorar tu agente](/es/guias/mejorar-tu-agente-de-ia). La referencia completa está en [Herramientas](https://vatio.ai/docs/tools) y [Autenticación](https://vatio.ai/docs/authentication/).

## Preguntas frecuentes

### ¿Cómo conecto un agente de IA a mi propia API?

Describe cada endpoint como una tool: un archivo YAML con un request HTTP, los parámetros que llena el modelo y placeholders para tus secrets. Agrega su clave a agent.tools en vatio.yml, haz push y pruébala con vatio chat.

### ¿El agente puede leer los datos de un usuario con sesión iniciada?

Sí, en tu sitio web o tu app, y en WhatsApp una vez que inicia sesión. Marca la tool con access: private y haz que tu backend firme un JWT RS256 para el visitante. Vatio lo verifica con tu clave pública y se lo pasa a la tool, y tu endpoint acota la consulta al sub del token. Vatio nunca tiene la clave privada, así que puede comprobar quién es alguien pero nunca afirmarlo.

### ¿Por qué una tool privada no puede recibir un parámetro user_id?

Porque los parámetros los llena el modelo, y se le puede convencer de llenar ese con el id de otra persona. Una tool privada que declare user_id, customer_id, account_id, member_id, patient_id o subject hace fallar el deploy. Lee el usuario desde $auth.subject o desde el propio token.

### ¿Las tools privadas funcionan en WhatsApp e Instagram?

Sí, una vez que el contacto inicia sesión: con un sign_in_url en vatio.yml, Vatio le envía un botón Iniciar sesión que abre tu sitio, tu página redirige de vuelta con el mismo token que le pones al widget, y la conversación sigue. Mientras tanto es anónimo y solo corren tools públicas. Para que alguien revise su propio caso sin iniciar sesión, usa una tool pública que reciba un número de referencia y un segundo dato, y que tu backend verifique que coincidan.

### Mis usuarios inician sesión con Supabase. ¿Tengo que firmar un JWT?

No. Corre vatio auth --supabase: nombra tu proyecto en vatio.yml, y el widget lee la sesión de Supabase que tu página ya tiene, sin código. En WhatsApp e Instagram el botón Iniciar sesión abre tu página de login y el widget entrega la sesión. Una tool privada reenvía $auth.token a la API REST de Supabase, así que tu Row Level Security decide qué ve el visitante.

### ¿El agente puede llamar a una API que corre en localhost?

No. Las tools corren en los servidores de Vatio, así que localhost queda fuera de su alcance. Expón tu backend local con un túnel y define esa URL como secret solo de preview (vatio secrets set KEY URL --env preview), así live sigue llamando a producción.

### Si una tool es un solo request, ¿dónde va la lógica de varios pasos?

En tu backend. Todo lo que necesite condiciones, una segunda llamada o una respuesta calculada es código, así que ponlo detrás de un endpoint que haga el trabajo completo y dale ese endpoint al agente como tool.
