#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

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:

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:

PlaceholderOrigen
$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_numberEl perfil del contacto

#Una tool que actúa

tools/agendar_servicio.yml:

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:

{ "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

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.

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:

ClaimObligatorioValor
subsíEl id de tu usuario. Se convierte en $auth.subject
audsíEl slug de tu workspace, exacto
expsíSegundos Unix. Que sea corto: se revisa de nuevo en cada llamada a una tool
name, email, phone_numbernoLlenan el perfil del contacto
metadatanoUn 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.

// 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:

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:

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.

<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:

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:

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.

#Pruébalo

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

#Qué sigue

Si el agente todavía no existe, empieza por 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. La referencia completa está en Herramientas y Autenticación.

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.

Actualizado: