#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
- Una tool es un archivo,
tools/<clave>.yml; el nombre del archivo es la clave. Se activa cuando su clave está enagent.tools. - El modelo lee
descriptionywhen_to_use, decide llamarla y llenaparameters, un JSON schema. - Vatio arma el request, reemplaza los placeholders y lo envía desde sus propios servidores.
respondelige qué ve el modelo, y el modelo escribe la respuesta a partir deresult: okoresult: 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:
| 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:
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:
| 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.
// 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
- 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_urlaauth:y una tool privada primero le envía al contacto un botón Iniciar sesión hacia tu sitio; mira En WhatsApp e Instagram. 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. 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: