Conectá un bot a Puentes
Puentes es un chat tipo WhatsApp donde las personas conviven con bots: Claude y GPT vienen preintegrados, y vos podés sumar los tuyos. Un bot es un participante más: entra a grupos, tiene chat 1 a 1, se lo menciona con @Nombre y, si lo publicás, aparece en el catálogo para que otros lo agreguen.
Actualizada el 12 de octubre de 2026. La API no tiene versión en la URL: los cambios suman campos y rutas, nunca rompen las que hay; lo que cambia se anota acá. GET /api/version dice qué build está corriendo.
Cambios recientes
- 2026-10-12: Un toque en un botón cita su tarjeta: el mensaje de la persona trae
replyTocon el mensaje de la tarjeta que contesta (el delcardIdde suchoice), como una respuesta citada, salvo que el cliente haya citado otro. Con varias tarjetas parecidas en el chat, así se sabe a cuál contestó cada toque. Un toque sincardId(una lista, un formulario, una app vieja) sigue sin cita. - 2026-10-12: Toda negativa a un invitado (una cuenta sin nada verificado) es un
403concode: "guest": también invitar por email, buscar gente y la agenda, una tarjeta de contacto, bots, organizaciones, perfil de desarrollador, copiar, pedir acceso y conectar un servicio. Los textos no cambian. - 2026-10-11: Buscar mensajes:
GET /api/search(todos mis chats) yGET /api/groups/:id/search(uno), con el pedazo del texto y dónde está cada palabra (snippet.ranges). Solo personas. - 2026-10-09: Una tarjeta de botones (
optionscon"layout": "buttons") se contesta una vez por persona: elchoicetraecardId(el id del mensaje de la tarjeta), Puentes guarda la respuesta de esa persona en la tarjeta (answers, avisa conmessage:updated) y un segundo toque de la misma persona no se guarda: devuelve su primera respuesta. Los botonesurl,copyyask, las encuestas y los fijados siguen como antes. - 2026-10-08: /openapi.json trae el cuerpo de cada pedido (
requestBody), la forma de cada respuesta y todos los objetos encomponents.schemas(Mensaje, cada tarjeta, Chat, Bot, tablas, programados, conectores…). Los objetos de esta página salen de esos mismos schemas. Corregidos:PATCH /api/me(no cambia el nombre; sítzylinkPreviews),POST /api/reports(bot, desarrollador u organización) y el texto de unpostUrl(hasta 4000). - 2026-10-08: Esta página se genera del código: quién puede llamar cada ruta y su límite salen de las rutas mismas. Nuevas: OpenAPI, Markdown y llms.txt; secciones de tablas, programados, fijados, conectores y cuentas, errores con
code, eventos del socket y/api/config. Corregidas cifras viejas (descripción 2000, instrucciones 4000, cuentas nuevas por IP) y el paginado del historial. - 2026-10-05: Un
webhookUrlde un bot externo es solohttpsen producción. - 2026-10-05: La ficha de un link:
message.link(la escribe el server después, pormessage:updated),linkPreview: falseal enviar yGET /api/link-preview. - 2026-10-02: Niveles (
tier1, 2, 3): cada respuesta de una IA descuenta su nivel una vez, use las herramientas que use. El historial va por páginas para una persona o una API key (?limit,?before,?to,?since); un bot sin parámetros sigue recibiendo los últimos 500. - 2026-09-30: Reacciones:
message.reactions({ userId: emoji }) yPUT …/messages/:mid/reaction(solo personas). - 2026-09-25: Las tarjetas
picks,catalogyconfirmse guardan comooptionsconlayout: leé las dos formas. - 2026-09-24: Encuestas (
optionsconpoll: true) y «Marcar varias» (select). - 2026-09-19: Tablas:
GET/POST /api/groups/:id/tablesy sus filas, también para un bot.
x-puentes-who y su límite en x-puentes-limits) y el índice llms.txt. Las tres se generan del mismo origen que esta página: lo que dice una lo dicen las otras.Conceptos
| Participante | Quién lo corre | Credencial |
|---|---|---|
| Persona | Un humano desde la app | Sesión del dispositivo, o una API key pk_… que actúa como esa persona desde un script |
| Bot AI AI | Puentes llama al modelo (Claude, GPT) con el historial del chat. Los preintegrados y los que creás con tus instrucciones | Ninguna: lo corre el server |
| Bot externo BOT | Tu programa, donde quieras | Token de bot pb_… que actúa como el bot |
Todo chat es un grupo (kind: "group", con link de invitación) o un chat directo (kind: "direct", exactamente dos participantes, sin invitación). Al crear un bot se crea solo su chat directo con vos. Los mensajes de cualquier bot llevan senderType: "bot"; los de personas, "user"; los avisos del sistema, "system".
@Claude …), nunca por el modo "siempre". La cuota gratis de esas respuestas la paga el dueño del bot; con clave propia de modelo en Ajustes, se usa esa.Autenticación
Todas las rutas viven bajo https://app.puentes.ai/api y hablan JSON. La credencial va en el header Authorization: Bearer <token>:
pk_…API key personal: se crea en la web de escritorio, en Creadores → «Bots externos y API» (Creadores aparece con el perfil de desarrollador completo, desde Ajustes → «Ser desarrollador»). Actúa como vos: crea bots, lee y escribe en tus chats, reacciona, vota. Lo que no hace es administrar la cuenta (sesiones, API keys, emails, teléfonos, claves de modelo, formas de entrar): eso es de la app (403). Cada ruta dice en la referencia si la acepta.pb_…token de bot: lo devuelvePOST /api/bots(o en Creadores → «Nueva IA», eligiendo «Externo»). Actúa como el bot: lee y escribe en los chats donde está. No entra a ningún chat por su cuenta (ni por link de invitación: alguien del grupo lo agrega) y no puede tocar la cuenta del dueño.
Los dos se muestran una sola vez y se guardan hasheados. Si perdés el token de un bot, generá otro (POST /api/bots/:id/token); el anterior deja de funcionar al instante. Los errores vuelven como { "error": "mensaje", "code"? } con el status HTTP que corresponde: error es una frase en castellano para mostrar, y code, cuando viene, es estable para que tu programa decida (ver Errores).
curl https://app.puentes.ai/api/me -H "Authorization: Bearer pk_…"
# { "id": "…", "name": "Mauro", "color": "#65aadd", "bot": false, … }
# Python 3, sin dependencias
import json, os, urllib.request
req = urllib.request.Request('https://app.puentes.ai/api/me', headers={'Authorization': f"Bearer {os.environ['PUENTES_TOKEN']}"})
print(json.load(urllib.request.urlopen(req)))
Crear un bot
POST /api/bots con tu API key. Devuelve el bot, su chat directo con vos y, si es externo, el token.
curl -X POST https://app.puentes.ai/api/bots \
-H "Authorization: Bearer pk_…" -H "Content-Type: application/json" \
-d '{
"runtime": "api",
"name": "Alertas",
"description": "Avisos del datacenter",
"webhookUrl": "https://mi-servidor.com/puentes",
"public": false
}'
# { "bot": { "id": "…", "name": "Alertas", "runtime": "api", "tokenPrefix": "pb_ab12cd…", "webhookSecret": "…", … },
# "token": "pb_…", <- guardalo: no se vuelve a mostrar
# "group": { "id": "…", "direct": true, "peer": { "name": "Alertas" }, … } }
| Campo | Descripción |
|---|---|
runtime | "api" (externo, con token) o "ai" (lo corre Puentes con un modelo). Default "ai". |
name | Hasta 40 caracteres, único entre tus bots. AI, IA y todos están reservados. |
description | Opcional, hasta 2000 caracteres. Se ve en la ficha. |
avatar | Opcional: la foto del bot, la URL que devolvió POST /api/upload con una imagen (solo se aceptan las del bucket de Puentes); null la quita. También en PATCH /api/bots/:id. |
public | true lo publica en el catálogo: cualquiera puede agregarlo a sus grupos o hablarle 1 a 1. |
webhookUrl | Solo api. https:// pública (ver Webhook). Se puede setear después con PATCH. |
provider, tier | Solo ai. provider: "anthropic" o "openai". tier: el nivel, 1 Básico, 2 Medio o 3 Avanzado (default: Básico); Puentes elige el modelo de ese nivel y lo actualiza solo cuando cambia. Cada respuesta pagada con mensajes gratis o créditos descuenta 1, 2 o 3 por respuesta, use las herramientas que use. GET /api/config trae los niveles de cada proveedor (providers.*.tiers). Un model todavía se acepta y cuenta como su nivel. |
mode | Solo ai. El default del bot: "mention" (responde cuando lo mencionan) o "always" (en toda la conversación). Cómo responde en cada grupo se elige al agregarlo (POST /api/groups/:id/bots { botId, mode }) y viaja en members[].mode; en un chat directo responde siempre. |
shortDesc, category, tags, language, countries | La tarjeta del catálogo: una línea (≤ 80), un id de categoría (los de botCategories en /api/config), hasta 10 etiquetas, idioma (es | en | pt-BR) y países ISO-2 (vacío = todos; fuera de ellos no se lista, pero con el link se usa igual). |
visibility | "private" (el dueño, los grupos donde ya está y quien recibió el bot compartido: una tarjeta, un link o «agregar persona» del dueño valen en todos los niveles) | "link" (lo mismo, con el compartir a la vista en la interfaz) | "authorized" (los compartidos más cualquier email verificado de los dominios del bot, PUT /api/bots/:id/domains; con requestsOpen quien llega por el link puede pedir acceso) | "catalog" (todos; exige el perfil de desarrollador completo: 409 con missing). public sigue existiendo y equivale a visibility = "catalog". |
systemPrompt | Solo ai. Instrucciones extra, hasta 4000 caracteres. |
menu, actions | Botones arriba del campo de texto en el chat directo con el bot: [{ title (≤ 24), text? (≤ 200) }], hasta 6 en menu (fijos) y 20 en actions (las que Puentes puede sugerir después de una respuesta). Tocar uno manda text (o el título) como mensaje de la persona. |
alias | El nombre corto para mencionarlo (@alias): de 2 a 20 letras, números, - o _. Es el sugerido: en cada grupo puede llevar otro (PATCH /api/groups/:id/bots/:botId { alias }). Errores con code: alias_*. |
orgId | Crear el bot a nombre de una organización tuya (sus miembros lo editan). |
requestsOpen | Con visibility: "authorized": quien llega por el link puede pedir acceso. |
pay, ownerKey, capMonthUsd, perUserDailyReplies, perUserMonthUsd | Solo ai. Quién paga las respuestas: "user" (la cuota gratis o la clave de cada persona, el default) o "owner" (la clave del desarrollador, ownerKey, que se guarda cifrada y nunca vuelve; DELETE /api/bots/:id/owner-key la olvida). Con owner nadie gasta su cuota; los topes (null = sin límite) son el total del mes en USD, y por persona las respuestas de las últimas 24 h y los USD del mes. Al llegar a un tope la IA publica una línea de sistema con el adjunto { type: "cap", code, botId } y no responde. Solo un admin del bot los cambia. |
clonable, idea | Solo en PATCH, solo un admin del bot. clonable: true deja que cualquiera que lo use haga su copia (POST /api/bots/:id/clone); no se puede con un bot externo, uno que usa un servicio con la cuenta de quien lo hizo, o una copia que sigue a su original (code: clone_*). idea: { title (≤ 40), blurb? (≤ 80), copyName? (≤ 36), opening? (≤ 600) } lo muestra como idea para copiar en el catálogo (necesita clonable y el catálogo: code: idea_needs_catalog). |
mcp | Solo ai. { url, label?, secret?, auth? }: un servidor MCP cuyas herramientas el bot puede usar. url pública https://; secret es la clave si el servicio da una (se guarda cifrada, nunca se devuelve); auth ("none" | "bearer" | "oauth") se detecta solo si no lo mandás. Ver Bots con herramientas. null en PATCH desconecta. |
Un bot AI creado por API es igual al que se crea desde la app: un "Pirata" con systemPrompt: "Hablás como pirata" y public: true queda en el catálogo para todos.
Bots con herramientas (MCP)
Un bot ai con mcp recibe las herramientas del servidor en cada respuesta y Puentes ejecuta las que el modelo pide (hasta 6 rondas, 30 s por herramienta, 90 s por respuesta; cada respuesta descuenta de la cuota gratis de quien le habla lo que cuesta su nivel, una vez, sin importar cuántas rondas use). La credencial es siempre la del dueño: si el servidor autentica por OAuth, el dueño lo conecta desde el chat (el bot publica una tarjeta mcp_connect en vez de responder) o con POST /api/bots/:id/mcp/connect, que devuelve la url de autorización; al volver, el server guarda los tokens cifrados. Quien pueda usar el bot ejecuta las herramientas con la cuenta del dueño: cada respuesta lleva el rastro en attachments[].type = 'tools' y el socket avisa ai:tool { groupId, aiId, tempId, tool, label, phase: 'start' | 'end', ok } mientras la llama (y phase: 'cards' cuando ya terminó de escribir y está preparando las tarjetas que devolvieron las herramientas: el texto ya está completo y message:new llega enseguida, antes que ai:done). Solo disponible si GET /api/config trae mcpEnabled: true. Un servidor en una red privada no es alcanzable (Puentes corre en la nube): Home Assistant, por ejemplo, con Nabu Casa o un túnel. Fotos y archivos: el bot ve en su contexto el link público de cada foto, video o archivo del chat, y si una herramienta recibe una URL (un menú, un ticket, un PDF) se lo pasa como argumento; el archivo en sí no viaja, tu servidor lo descarga de esa URL si lo necesita.
Para quien escribe un servidor MCP: quién habla y cómo avisar después
Cada tools/call lleva _meta.puentes con el autor del mensaje que disparó al bot: { userId, userName, senderType: 'user' | 'ai' | 'bot', groupId, callbackUrl?, postUrl?, country?, tz? }. Sirve para guardar algo por persona o por chat en vez de por bot (un carrito por chat, una cuenta de Google por persona). Es una afirmación de Puentes, no una prueba: lo que identifica a Puentes es la credencial del bot; si habló otra AI, userId va vacío. country (ISO 3166-1 de dos letras: el país que la persona eligió en su perfil, si no el de su teléfono verificado, si no desde dónde escribió) y tz (zona horaria IANA: la del perfil o del dispositivo de la persona, si no la del chat) son pistas para ordenar resultados y mostrar horas («River» para alguien de Uruguay es el de Montevideo primero); llegan solo cuando se conocen.
callbackUrl viene solo cuando habló una persona y vale 24 horas: es la forma de que el bot retome solo más tarde. Si tu herramienta no puede terminar ahí (necesita que la persona ingrese en tu página, o el trabajo tarda), contestá con lo que la persona tiene que hacer (un link) y guardate la URL; cuando termine, hacé POST callbackUrl con JSON { "text": "Ana conectó su cuenta. Retomá lo que estaba pidiendo." } (hasta 500 caracteres). Puentes responde 202, publica ese texto como mensaje de sistema en el chat y despierta al bot obligado a responder: con la conversación a la vista, vuelve a llamar a la herramienta que había quedado a medias. 404 si el token venció o el bot ya no está en ese chat. La URL es una capacidad (quien la tenga hace hablar al bot en ese chat hasta que venza): guardala cifrada o dentro de un token firmado, y no la loguees.
Para quien escribe un servidor MCP: cargar el servidor no es usarlo (dos credenciales)
Quien registra un conector y quien lo usa son personas distintas, y Puentes manda dos credenciales que no se mezclan. La clave del servidor la pone quien carga el conector (Creadores › Conectores) y dice «esto es Puentes», nada más. La cuenta es de cada persona: la conecta ella desde el chat (OAuth, o una clave que pega) y Puentes la guarda cifrada por persona; un conector puede trabajar con la de cada uno (person), con una por chat (chat) o con la de su creador (owner).
Con OAuth, cada llamada de una persona lleva su token en Authorization: Bearer … y la clave del servidor en X-Api-Key. Sin nadie detrás (el «Probar» de quien carga el conector, o un chat donde todavía nadie conectó su cuenta) llega solo X-Api-Key: con eso tu servidor debería dejar hacer initialize y tools/list, y nunca abrir la cuenta de alguien, nombre el chat que nombre _meta.puentes. Cuando Puentes quiere saber cómo entra cada persona, sondea con la clave del servidor como bearer y sin token: contestá 401 con WWW-Authenticate: Bearer resource_metadata="…" y lo lee como OAuth.
Herramientas que no necesitan cuenta. Buscar un precio o listar las ofertas del día no son de nadie. Marcalas en su definición con _meta: { puentes: { account: false } }: Puentes las llama igual en un chat sin cuenta conectada (con la clave del servidor sola), y solo cuando la IA va a usar una de las otras le muestra a la persona el botón para conectar. Sin la marca, toda herramienta de un conector sin cuenta pide conectarla primero. Solo cuenta false explícito.
El nombre de una cuenta (puentes_account). Una persona puede tener varias cuentas en tu servicio (la del trabajo y la personal; o, si para vos «la cuenta» es un archivo, una por archivo). Para que en «Mis cuentas» no se llamen todas igual, sumá una herramienta reservada puentes_account, sin argumentos, que responda _meta: { puentes: { account: { "label": "Recetas · d…@gmail.com" } } } (hasta 60 caracteres): Puentes la llama una vez, apenas la cuenta se conecta, y usa ese nombre. Toda herramienta cuyo nombre empieza con puentes_ es de Puentes: nunca se le ofrece al modelo ni se le lista a una persona. Primer uso: el conector «Planilla de Google» (puentesai-mcp-google, /planilla/mcp), donde conectar es elegir un archivo del Drive (scope drive.file + el selector de Google) y ese archivo es la cuenta.
Herramientas que leen, que cambian algo y que piden confirmación
Puentes lee de tu tools/list una sola anotación: annotations.readOnlyHint: true dice que la herramienta solo lee (buscar, listar). Toda herramienta sin esa marca se trata como que cambia algo (changes: agrega al carrito, manda, borra), y así se la muestra a la persona. destructiveHint, idempotentHint y openWorldHint se ignoran.
Quien registra el conector puede, para cada herramienta (toolLabels en POST /api/connectors): ponerle un nombre para personas (label), marcarla como que cambia algo aunque el servidor diga que solo lee (changes: true, nunca al revés), esconderla del todo (hidden: true) y hacer que pida confirmación antes de correr (confirm: true). Esa es la única forma de que una herramienta pregunte: las anotaciones nunca lo hacen. En la ficha pública de un conector cada herramienta viaja como { name, label, changes?, confirm? }.
Para quien escribe un servidor MCP: hablar por tu cuenta (postUrl)
Tu servidor solo contesta cuando lo llaman. Para decir algo cuando a vos te pasa algo («salió tu pedido», una alarma, un carrito que cambió del lado de la tienda) no hace falta un segundo bot ni un token: cada tools/call trae _meta.puentes.postUrl, una dirección de ese bot en ese chat. Guardala por groupId y, cuando quieras, hacé POST postUrl con JSON { "text"?, "card"?, "pin"? }: text y card salen como un mensaje del bot (una burbuja, con su notificación; la tarjeta es cualquiera de las de abajo, con la misma limpieza), y pin pone, actualiza o saca un fijado. Responde 202; 400 si no trae nada, 404 si la dirección ya no vale, 429 pasados los 30 mensajes por minuto del bot.
No vence por tiempo: vale mientras el bot siga en ese chat (si lo sacan, muere, y volver a sumarlo da una dirección nueva en la próxima llamada). Viene también cuando habló otra AI. No despierta a la IA ni le cobra a nadie: es el bot hablando. La diferencia con callbackUrl: aquella es de una persona, dura 24 horas, publica una línea de sistema y hace que la IA retome (un login que terminó); postUrl es del chat, dura lo que dure el bot ahí y solo publica. Las dos son capacidades: quien las tenga hace hablar al bot en ese chat, así que guardalas cifradas y nunca las loguees.
Para quien escribe un servidor MCP: escribirle a alguien en nombre de la persona (send)
«El lunes escribile a Flor que llevo el postre»: tu herramienta no manda nada, se lo pide a Puentes. Solo se atiende cuando la herramienta la usa el asistente «Puentes»: con cualquier otra IA el pedido se ignora, porque nadie más escribe en nombre de una persona. En el resultado de tools/call sumá _meta.puentes.send = { "to": "Flor", "text": "Llevo el postre", "when"?: "lunes 9:00" }. to es un nombre en palabras (un contacto o un grupo): Puentes lo busca entre los chats de quien estaba hablando, y tu servidor nunca ve esa lista; si hay varias coincidencias o ninguna, el chat le pregunta a la persona. when usa la gramática de los programados (una vez, todos los días, días de semana, día del mes) y sin él sale ya. Opcionales: via ("whatsapp" | "telegram") cuando la persona nombró el canal, y ref (el número con código de país, o el usuario de Telegram): entonces no sale por Puentes; a esa hora la persona recibe un botón que abre ese chat con el texto listo y lo manda ella. Por WhatsApp sin número, Puentes le contesta a tu herramienta que lo pida. El mensaje sale como un mensaje de la persona, con una marca de «enviado por un asistente», así que un @Bot adentro despierta a ese bot.
Las reglas: solo vale cuando la persona misma lo pidió, y siempre a su nombre (no desde un callbackUrl, un programado u otra IA: ahí se descarta en silencio). En su chat directo con el bot se programa; en un grupo, Puentes le muestra a esa persona sola una tarjeta «¿Lo mando en tu nombre?» y no sale nada hasta que toca «Mandar» (en un grupo no hay contactos de afuera ni lista de chats). No hay un permiso aparte que conceder: lo que lo mantiene honesto es que Puentes lo dice al programarlo, lo dice en el chat al salir, y la persona lo ve y lo cancela desde Ajustes › Mensajes programados. Topes: texto de hasta 2000, 5 envíos programados a la vez y 10 mensajes por día por persona, nada más seguido que una vez por día. El modelo recibe una nota con lo que pasó (programado, esperando la tarjeta, no encontré a quién) para que se lo cuente a la persona.
Para quien escribe un servidor MCP: una lista para elegir (tarjeta options)
Cuando la respuesta es "¿cuál de estos?" (varios productos, varios horarios), la herramienta puede mostrarla como tarjeta en vez de texto: en el resultado de tools/call, además del content de siempre (que es lo que lee el modelo), mandá _meta.puentes.card:
{ "type": "options", "title": "Yerba Playadito", "multi": false,
"items": [ { "id": "00186014", "title": "Yerba Playadito 500 g", "subtitle": "Con palo", "detail": "$3.186",
"was": "$4.000", "badge": "20% OFF",
"image": "https://…/foto.jpg", "url": "https://…/producto" } ] }
Hasta 50 ítems (el chat muestra 10 y un botón «Ver más» despliega el resto sin pasar por el modelo); id (64) y title (120) son obligatorios, subtitle (160), detail (40, va a la derecha: un precio, una hora), was (40, el precio anterior, que se dibuja tachado arriba de detail; sin detail se ignora), badge (24, una etiqueta debajo: "20% OFF", "3x2"), image y url (solo https) opcionales. was y badge son para una oferta: poné en detail el precio que la persona paga hoy y en was el de antes, y no uses el tachado para un precio que exige una condición (llevar tres, una tarjeta de socio) — eso va en el subtitle dicho con todas las letras. Puentes copia cada imagen a su propio bucket (JPEG, PNG, WebP o GIF de hasta 1,5 MB; la que no se puede copiar se omite) y pega la tarjeta debajo de la respuesta del bot, con botId. Hasta 3 tarjetas por respuesta. La persona toca un ítem (con multi: true, tilda varios y aprieta "Elegir") y eso sale como un mensaje suyo, "Elegí: …", con un adjunto { "type": "choice", "botId", "ids": ["00186014"], "labels": ["Yerba Playadito 500 g"] } que despierta a ese bot aunque en el grupo nadie lo haya mencionado; el modelo lo lee como [eligió: Yerba Playadito 500 g (id 00186014)] y vuelve a llamar a tu herramienta con ese id. Contá en la descripción de la herramienta qué hacer cuando llega una elección.
Una tira de carteles (2026-09-15): con "layout": "banners" los ítems se dibujan como imágenes anchas que se corren de costado — para cuando la imagen es el mensaje y ya trae su propio texto (los carteles de campaña de un súper, las portadas de algo). Ahí el title es solo la etiqueta que lee un lector de pantalla, y no hace falta detail. La letra chica va en subtitle y el link a las bases en url: el chat los pone detrás de una (i) en la esquina del cartel: tocarla abre un globo con el texto y un "Ver legales" que va a tu url en otra pestaña (con mouse también aparece al pasar por encima). Así no tapa el arte, se puede leer en el celular, y nadie termina en otro sitio por tocar un botón de 22 píxeles. No repitas "ver legales" en el subtitle: el link ya lo dice. La altura la fija el chat y el ancho lo pone cada imagen, así que no se recorta el arte de nadie. Tocar uno manda la misma elección que cualquier lista: tu herramienta recibe el id y contesta con lo que haya detrás (por ejemplo, los productos de esa campaña como carousel).
Una góndola (2026-09-15): con "layout": "carousel" la misma lista se dibuja como tarjetas que se corren de costado, con la foto grande, la etiqueta de descuento encima y el precio anterior tachado debajo del nombre. Es para cuando la foto y el precio son el mensaje (una fila de ofertas) y nadie va a leer diez nombres. En escritorio aparecen flechas al pasar el mouse, porque una rueda de mouse no se mueve de costado. No cambia nada más: los mismos items, la misma elección de vuelta; si los ítems piden cantidad, cada tarjeta suma un botón + que agrega sin abrir nada. En una lista normal el chat muestra 10 y pagina; un carrusel los muestra todos, porque se corre de costado.
Una cantidad (2026-09-15): si lo que sigue a elegir es "cuántos", la tarjeta puede pedir un control en vez de que la persona escriba otro mensaje. Va input en la tarjeta, y un ítem puede traer el suyo para pisarlo:
{ "type": "options", "input": { "label": "Agregar", "min": 1, "max": 30, "step": 1, "default": 1, "unit": "u." },
"items": [ { "id": "…", "title": "Queso rallado 150 g" },
{ "id": "…", "title": "Queso cremoso x kg",
"input": { "label": "Agregar", "min": 0.25, "max": 20, "step": 0.25, "default": 1, "unit": "kg" } } ] }
label (24) es el botón y es obligatorio; unit (8) la palabra al lado del número ("kg"), y min / max / step / default el rango (por default 1, 100, 1 y 1; lo que no entra en el rango se acomoda solo). El input del ítem existe para las listas mezcladas: una búsqueda de "queso" trae el rallado en paquete y el cremoso cortado al momento, y el stepper de uno no sirve para el otro. Con input, tocar una fila abre debajo un − [2] + con el botón en vez de mandar la elección en el acto, y la elección sale con la cantidad: { "type": "choice", "botId", "ids": ["00186014"], "labels": […], "values": [2] }, que el modelo lee como [eligió: Yerba Playadito 500 g × 2 (id 00186014)]. Una tarjeta con input (o con un ítem que lo traiga) es siempre de a una fila: multi se apaga, porque una cantidad por fila es un formulario y no una lista de tildes. Decí en la descripción de la herramienta que cuando la elección viene con cantidad no hay que volver a preguntar cuántos.
Cuando resolvés varias cosas de una (tarjeta picks)
"Necesito leche, pan, yerba y tomates" no se contesta con cuatro listas: son cuatro listas para recorrer, y una respuesta admite tres tarjetas. La tarjeta picks es lo contrario de options: en vez de una pregunta con muchas respuestas, son muchas respuestas ya dadas, cada una con sus alternativas a un toque. Resolvé vos (agregar al carrito, reservar los horarios) y mostrá qué hiciste:
Forma nueva (2026-09-24): también se escribe { "type": "options", "layout": "picks", "title", "rows" }, con las mismas rows; las dos formas valen y significan lo mismo. Desde 2026-09-25 Puentes la guarda en la forma nueva: si leés el historial o recibís el webhook, una tarjeta nueva llega como options con layout (las de antes, con su tipo de siempre). Leé las dos.
{ "type": "picks", "title": "Puse 4 de 5 · carrito $21.840",
"rows": [
{ "id": "leche", "label": "leche", "picked": "00253104", "qty": 1,
"input": { "label": "Usar", "unit": "u." },
"items": [ { "id": "00253104", "title": "Leche La Serenisima 1 L", "detail": "$3.060" },
{ "id": "00251289", "title": "Leche Ilolay 1 L", "detail": "$2.890" } ] },
{ "id": "fideos", "label": "fideos", "note": "No encontré fideos en Coto", "items": [ … ] }
] }
Cada renglón es una cosa que te pidieron: id (64) lo identifica, label (60) es cómo lo dijo la persona, picked el id de lo que elegiste entre los items de ese mismo renglón (si apunta a otro lado se ignora), qty la cantidad, input el control para cambiarla y note (120) por qué no pudiste resolverlo. Los items son exactamente los de options (con was, badge, input): es una lista de listas con un elegido, no un formato nuevo. Hasta 10 renglones, 10 alternativas cada uno y 60 en total (cada foto es una copia a nuestro bucket).
Un renglón sin picked se abre solo, con su note: algo que falta en silencio es peor que algo que pide que lo elijan. La persona toca "Cambiar" en los que no le convencen, los cambios se acumulan y sale un solo mensaje con un choice que dice a qué renglón pertenece cada id: { "type": "choice", "botId", "ids": ["00251289"], "rows": ["leche"], "values": [1], "labels": […] }, que el modelo lee como [eligió: leche: Leche Ilolay 1 L (id 00251289)]. La tarjeta entera la lee como [puse "…": leche → Leche La Serenisima 1 L × 1 $3.060 (id …) · fideos → sin resolver]. Decí en la descripción de la herramienta que no repita la lista en el texto: la tarjeta ya la muestra.
Un botón (tarjeta link): cuando la persona tiene que ir a una página tuya (ingresar en tu servicio, autorizar algo), en vez de pegar la URL en el texto mandá _meta.puentes.card = { "type": "link", "label": "Conectar Google", "url": "https://…/conectar?t=…", "title": "Diego, conectá tu cuenta de Google" }: label (40, el botón) y url (https) obligatorios, title (120) opcional, una línea arriba. Se pega debajo de la respuesta del bot y el modelo lo ve como [botón "Conectar Google"], así no repite la URL. Hasta 3 tarjetas por respuesta, contando options y link.
Botones de respuesta rápida, y una lista con secciones
Botones (2026-09-17, como los reply buttons de WhatsApp): con "layout": "buttons" la tarjeta options se dibuja como hasta 4 pastillas debajo de la burbuja, con el título de cada una (40 caracteres). Tocar una manda ese título como mensaje de la persona con el choice de siempre ({ "ids": ["18"], "labels": ["Sí, a las 18"], "cardId": "…" }). Cada persona la contesta una vez (2026-10-09): cardId es el mensaje de la tarjeta, Puentes guarda en ella lo que eligió cada uno (answers: { userId: { ids, msgId, at } }, avisa con message:updated) y quien ya contestó ve su botón marcado y los otros apagados; un segundo toque suyo no se guarda (vuelve su primera respuesta). El mensaje del toque cita la tarjeta (2026-10-12: replyTo es el mensaje de la tarjeta, como una respuesta citada), así con varias tarjetas parecidas se sabe a cuál contestó. En un grupo cada uno contesta lo suyo. Un fijado de bot se puede tocar siempre. Un ítem puede hacer otra cosa en vez de responder, una por botón: url (abre el link, el CTA URL de WhatsApp), copy (64: copia ese texto al portapapeles y avisa, sin mandar nada: un cupón, un código), o ask con "location", "photo", "file" o "contact" (le pide a la persona su ubicación, una foto con la cámara, un archivo o un contacto de su agenda: la respuesta es su mensaje con ese adjunto más el choice del botón, así sabés a cuál contesta; un contacto llega como palabras, «Nombre: número», y donde el navegador no abre la agenda la persona lo escribe en una hojita). El modelo lee la tarjeta como [botones: "Sí" (id 18) · "Pagar" (abre un link) · "Foto" (pide una foto)].
{ "type": "options", "layout": "buttons", "items": [
{ "id": "18", "title": "Sí, a las 18" },
{ "id": "pay", "title": "Pagar la seña", "url": "https://…" },
{ "id": "cup", "title": "Copiar código", "copy": "BIENVENIDO20" },
{ "id": "ubi", "title": "Compartir mi ubicación", "ask": "location" } ] }
Una IA también los escribe, con o sin herramientas: si su respuesta termina en una pregunta cerrada puede cerrar con un bloque ```buttons, una respuesta por línea (hasta 4), que Puentes convierte en la tarjeta y saca del texto; una línea Pagar | https://… es un botón que abre ese link, solo si la URL la devolvió una herramienta en esa misma respuesta.
Encuesta (2026-09-24): con "poll": true (y "multi" si se eligen varias) un toque es el voto de esa persona, no un mensaje; todos ven cuántos votos tiene cada fila y quién votó. Los votos los escribe Puentes (votes en el adjunto, por message:updated); no se mandan desde afuera. Cuando votaron todos, una IA que mandó la encuesta recibe el resultado.
Marcar varias (2026-09-24): con "select": { "label": "Quitar" } una lista de filas muestra «Seleccionar»; la persona marca varias y un botón con tu palabra las manda juntas: choice con ids y "action": "Quitar", y el texto «Quitar: Leche, Pan». Sirve para sacar cosas de un carrito, archivar, marcar pagados.
Secciones y lista plegada: la misma options admite "sections": [{ "id", "title", "items": […] }] (hasta 10 encabezados sobre las filas de siempre; se guardan planas con section en cada ítem) y "collapsed": "Elegir horario" (24): con eso la lista no ocupa la burbuja sino que se abre en una hoja al tocar ese botón, como el list message de WhatsApp. Ni un carrusel ni los botones se pliegan.
Un catálogo con carrito (tarjeta catalog)
Forma nueva (2026-09-24): también se escribe { "type": "options", "layout": "catalog", … } con los mismos campos (las sections anidadas como abajo, o planas como en cualquier options); las dos formas valen y significan lo mismo. Puentes la guarda plana, como options con layout: "catalog" (ítems con section y sections como títulos): así llega al historial y al webhook.
Como el mensaje de catálogo de WhatsApp, pero los productos viajan en el mensaje: una burbuja chica (miniatura, título, una línea, un pie y el botón) que abre el catálogo entero para recorrer por secciones, con la ficha de cada producto, una cantidad y un carrito que se mantiene en el dispositivo por chat y bot: cualquier tarjeta de catálogo tuya en ese chat lo abre con lo que ya tenía, y se vacía cuando la persona lo manda. Todo sale en un mensaje: { "type": "choice", "ids": ["l1", "q1"], "values": [2, 0.5], "labels": […], "items": […] } (hasta 30 renglones), que el modelo lee como [eligió: Leche 1 L × 2 (id l1), Queso × 0,5 (id q1)].
{ "type": "catalog", "title": "Ofertas de la semana", "body": "Lácteos, almacén y limpieza…", "footer": "42 productos",
"thumbnail": "https://…", "label": "Ver catálogo", "basket": "Carrito", "send": "Enviar pedido", "sent": "Pedido",
"input": { "label": "Agregar", "min": 1, "max": 30 },
"sections": [ { "id": "lacteos", "title": "Lácteos", "items": [ { "id": "l1", "title": "Leche 1 L", "detail": "$1.750", "image": "https://…" } ] } ] }
Los items son los de siempre (con was, badge, url y su propio input para lo que va por kilo); hasta 10 secciones y 60 productos por tarjeta (cada foto se copia al bucket; sin thumbnail se usa la primera foto). Es genérico a propósito: las cuatro palabras (label, basket, send, sent, 24 cada una) son tuyas, así que el mismo esquema sirve para un conteo de inventario (detail «12 en stock», el input de cada ítem arrancando en el stock actual, send «Guardar conteo») o para pedir repuestos.
Una dirección, o cualquier formulario corto (tarjeta form)
Un botón en la burbuja que abre una hoja con respuestas listas arriba (saved: las direcciones que ya tenés de esa persona, como filas) y hasta 8 campos debajo. Tocar una fila guardada manda su id; completar los campos manda form: { id: valor }. Las dos como un choice, así te despierta a vos.
{ "type": "form", "label": "Elegir dirección", "title": "¿A dónde va el pedido?", "send": "Usar esta dirección",
"saved": [ { "id": "casa", "title": "Casa · Av. Cabildo 2040", "subtitle": "Belgrano" } ],
"fields": [ { "id": "calle", "label": "Calle y número", "required": true, "placeholder": "Av. Cabildo 2040" },
{ "id": "cp", "label": "Código postal", "type": "number" },
{ "id": "zona", "label": "Zona", "type": "select", "options": ["Norte", "Sur"] } ] }
Tipos: text (default), number, tel, email, date, textarea, select (con options). Los ids son [a-zA-Z0-9_-] de hasta 32. La vuelta se lee como [eligió: Nueva (id form); calle: Av. Cabildo 2040, cp: 1428] y el texto del mensaje repite «Calle y número: … · Código postal: …». No se valida nada por país: la validación es tuya.
Algo siempre a la vista arriba del chat (un fijado, pin)
Para lo que está en curso y conviene tener a un toque: un carrito, un pedido, un turno, un conteo. Un fijado es una pastilla arriba del chat (una etiqueta, un número, un detalle, un ícono) que al tocarla abre cualquiera de las tarjetas de arriba (options, picks, catalog, form, link) más hasta 4 botones. No es un mensaje: no hace ruido en el chat ni cuenta en el tope de 3 tarjetas por respuesta. Las palabras son tuyas; Puentes no sabe qué es un carrito.
// en el resultado de una herramienta, al lado de `content`
"_meta": { "puentes": { "pin": {
"id": "cart", "label": "Carrito", "badge": "5", "detail": "$12.345", "icon": "basket",
"scope": "chat", // o "person": solo de quien estaba hablando
"card": { "type": "options", "title": "Carrito · total $12.345", "items": [ … ] },
"buttons": ["Finalizar compra", "Actualizar"]
} } }
El último que mandás con un id reemplaza al anterior: devolvelo en cada herramienta que cambie ese estado. { "id": "cart", "remove": true } lo saca. Un toque en la tarjeta o en un botón vuelve como cualquier otro: un mensaje de la persona con un choice para tu bot. Íconos: basket, list, tasks, money, clock, store, food, home, plane, cake, ball, star, bell, file, location, people, form, key. Hasta 4 fijados por bot y por chat. Un bot externo, que no tiene resultados de herramientas, publica lo mismo con su token: PUT /api/groups/:id/pins/:pinId (el mismo cuerpo sin id; forUserId lo hace de esa persona sola) y DELETE para sacarlo. Los clientes los leen con GET /api/groups/:id/pins y los eventos pin:updated / pin:removed.
Meterlo en un chat
- Desde la app: en el grupo, botón Bot (o en la info del grupo, "+ Agregar bot"). Por API:
POST /api/groups/:id/bots { "botId": "…" }como miembro del grupo. - Un bot nunca entra solo: con su token,
POST /api/invites/…/joinresponde403. Siempre lo suma alguien que ya está en el chat. - Chat 1 a 1: cualquiera que pueda usar el bot (builtin, público o suyo) hace
POST /api/direct/:botId. Entre personas, hay que compartir algún grupo.
Enviar mensajes
curl -X POST https://app.puentes.ai/api/groups/<chatId>/messages \
-H "Authorization: Bearer pb_…" -H "Content-Type: application/json" \
-d '{ "text": "🔴 @todos rack 4 sin energía", "replyTo": null }'
# -> el mensaje creado (ver Objetos)
texthasta 20.000 caracteres. Markdown liviano:**negrita**,_cursiva_,`código`, bloques```, links.@Nombremenciona a una persona o bot del chat (le llega la notificación aunque tenga el chat silenciado);@todosavisa a todo el grupo;@Claudeo@AIhace responder a la AI.replyTo: id de un mensaje del mismo chat para citarlo.attachments: primero subí el archivo conPOST /api/upload(multipart, campofile) y pasá lo que devuelve:[{ "type": "image", "url": "…", "name": "…", "mime": "…", "size": 123 }].- Tarjetas (2026-09-17): un bot puede mandar en
attachmentslas mismas tarjetas que devuelve un servidor MCP (optionscon cualquierlayout, botones,picks,link,catalog,form), hasta 3 por mensaje; Puentes las limpia igual, copia sus fotos al bucket y les pone tu id comobotId: lo que la persona toque te llega por el webhook como su mensaje con un adjuntochoice(ids, labels, values, form). - Menú fijo:
PATCH /api/bots/:id { "menu": [{ "title": "Ofertas" }, { "title": "Carrito", "text": "Mostrame el carrito" }] }(hasta 6, títulos de 24) pone esos botones arriba del campo de texto en el chat directo con tu bot; tocar uno mandatext(o el título) como mensaje de la persona. Es un atajo para escribir: te llega como cualquier mensaje. clientId(hasta 60 caracteres): mandalo siempre. Si el request se corta y no sabés si entró, reintentá con el mismo: la respuesta es el mensaje ya guardado y nadie lo recibe dos veces. Reintentá ante un timeout o un 502/503/504; un 4xx es un no.linkPreview: false: no armar la ficha del link (la de un mensaje de bot nunca se arma).POST /api/groups/:id/typingmuestra "está escribiendo…" unos segundos.- Listas de tareas:
POST /api/groups/:id/tasklists { "name", "items": ["…"] }, y sobre ellasPOST /api/tasklists/:id/tasks { "text" },PATCH /api/tasks/:id { "text" },POST /api/tasks/:id/toggle { "done" },POST /api/tasks/:id/due { "dueAt" },DELETE /api/tasks/:id; la lista entera:PATCH /api/tasklists/:id { "name" }yDELETE /api/tasklists/:id. Cada cambio responde con la lista completa (el borrado, con el mensaje que la llevaba) y llega por socket comotasklist:updated(otasklist:deleted). Una tarea con fecha es un recordatorio: a la hora, todo el grupo recibe la notificación.
Recibir mensajes (polling)
Cada mensaje tiene un seq global creciente. GET /api/me/updates?after=<seq> devuelve lo nuevo en todos los chats del bot (menos lo que escribió él mismo), hasta 100 por llamada. Con timeout=<segundos> (máximo 25) la respuesta espera hasta que haya algo: long-poll, sin gastar llamadas.
# primera vez, sin `after`: solo el cursor actual
curl "https://app.puentes.ai/api/me/updates" -H "Authorization: Bearer pb_…"
# { "messages": [], "next": 1234 }
# después, en loop
curl "https://app.puentes.ai/api/me/updates?after=1234&timeout=25" -H "Authorization: Bearer pb_…"
# { "messages": [ { "seq": 1235, "groupId": "…", "senderType": "user", "senderName": "Ana", "text": "hola bot", … } ], "next": 1235 }
Guardá next y volvé a pedir con ese valor. Para saber si un mensaje viene de un chat directo o de un grupo, y quiénes están, GET /api/groups/:id (cacheable). El historial de un chat: GET /api/groups/:id/messages; con un token de bot y sin parámetros trae los últimos 500, y con ?limit, ?before o ?since va por páginas como para una persona (ver la referencia).
Recibir mensajes (webhook)
Si el bot tiene webhookUrl, Puentes hace POST a esa URL por cada mensaje nuevo en sus chats (menos los propios y los del sistema):
POST https://mi-servidor.com/puentes
Content-Type: application/json
X-Puentes-Event: message
X-Puentes-Signature: sha256=<HMAC-SHA256 hex del body, con el webhookSecret del bot>
{ "event": "message", "ts": 1788924608778,
"group": { "id": "…", "name": "Soporte", "kind": "group" },
"message": { "id": "…", "seq": 1235, "groupId": "…", "senderType": "user", "senderId": "…", "senderName": "Ana", "text": "…", "attachments": [], "createdAt": 1788924608700, "replyTo": null } }
Verificá la firma con el webhookSecret (lo ves en la sección IAs, editando el bot, o en la respuesta de POST /api/bots):
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const app = express();
app.post('/puentes', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + createHmac('sha256', process.env.WEBHOOK_SECRET).update(req.body).digest('hex');
const got = req.get('x-puentes-signature') || '';
if (got.length !== expected.length || !timingSafeEqual(Buffer.from(got), Buffer.from(expected))) return res.status(401).end();
const { group, message } = JSON.parse(req.body);
console.log(`[${group.name}] ${message.senderName}: ${message.text}`);
res.status(204).end(); // respondé rápido; contestá en el chat con POST /api/groups/:id/messages
});
app.listen(8080);
- Timeout de 5 segundos, sin reintentos. Si necesitás garantía de entrega, usá el polling (o las dos cosas: el webhook para reaccionar rápido,
updatespara no perder nada). - Solo
https://y pública: se rechazahttp://y cualquier host privado o local, y una redirección nunca baja ahttp. Para desarrollar, un túnel (ngrok, cloudflared) te da unahttps, o webhook.site para ver el payload. - Las transcripciones de audios llegan después, como actualización del mensaje: no viajan por webhook ni por
updates. Si te importan, releéGET /api/groups/:id/messages.
Tiempo real con Socket.io
La app usa Socket.io; un bot puede hacer lo mismo con su token en auth.token. Recibe los mismos eventos que el navegador. Los sockets se reparten entre varios dominios: antes de conectar, pedí GET /api/sockets con tu token y conectá al host que devuelve (null = https://app.puentes.ai); la respuesta trae también la lista completa en hosts. Conectá solo por WebSocket (transports: ['websocket']): el long-polling no funciona detrás del balanceador.
import { io } from 'socket.io-client';
const { host } = await (await fetch('https://app.puentes.ai/api/sockets', { headers: { authorization: `Bearer ${process.env.PUENTES_BOT_TOKEN}` } })).json();
const socket = io(host || 'https://app.puentes.ai', { auth: { token: process.env.PUENTES_BOT_TOKEN }, transports: ['websocket'] });
socket.on('message:new', (m) => console.log(m.groupId, m.senderName, m.text));
socket.on('typing', ({ groupId, name }) => {});
socket.on('group:updated', (g) => {}); // miembros, nombre
socket.on('group:new', (g) => {}); // te sumaron a un chat / alguien abrió un directo con vos
socket.on('ai:tool', (e) => {}); // un bot con herramientas (MCP) está llamando a una: { tool, label, phase, ok }
// Para ESCRIBIR usá HTTP, no el socket: POST /api/groups/:id/messages con un clientId (ver abajo).
Todos los eventos (los de la app que no son de la API no están):
| Evento | Payload | Cuándo |
|---|---|---|
message:new | Message (+ clientId) | Un mensaje nuevo en uno de mis chats. |
message:updated | Message | Un mensaje cambió: su transcripción, la ficha del link, una dirección, votos, una tarjeta contestada, borrado para todos. |
message:hidden | { groupId, id, lastMessage } | Lo borré «para mí» (solo a mis dispositivos). |
message:status | { groupId, statuses: { id: tick } } | Los tildes de mis mensajes cambiaron (solo los que cambiaron). |
typing | { groupId, userId, name } | Alguien está escribiendo. |
ai:typing | { groupId, aiId, tempId, senderName } | Una IA empezó a responder (todavía sin texto). |
ai:chunk | { groupId, aiId, tempId, text } | Un pedazo nuevo de la respuesta de una IA (streaming): text es lo que se agregó desde el anterior, no el total. Juntalos por tempId. |
ai:tool | { groupId, aiId, tempId, tool, label, phase, ok } | Una IA con herramientas está llamando a una (phase: start | end | cards). |
ai:done | { groupId, tempId } | La IA terminó (el mensaje llega por message:new). |
group:new | Chat | Me sumaron a un chat, o alguien abrió un directo conmigo. |
group:updated | Chat | Cambió un chat: miembros, nombre, permisos, una IA. |
group:removed | { id, name?, kicked? } | Ya no estoy en ese chat: me sacaron (kicked), se borró, o me fui. |
group:cleared | { id, by } | Alguien vació el chat. |
group:requests | { id, pending } | A un admin: cambió la cola de pedidos para entrar. |
tasklist:updated | { groupId, list } | Cambió una lista de tareas. |
tasklist:deleted | { groupId, listId } | Se borró una lista. |
table:updated | { groupId, table } | Cambió una tabla. |
table:deleted | { groupId, tableId } | Se borró una tabla. |
schedules:updated | { groupId, schedules } | Cambiaron los programados del chat. |
pinned:changed | { groupId } | Se fijó o desfijó un mensaje (releé GET …/pinned). |
pin:updated | { groupId, pin } | Un fijado de un bot. |
pin:removed | { groupId, botId, id } | Un bot sacó su fijado. |
me:updated | — | Cambió mi cuenta (releé GET /api/me). |
me:bots | — | Cambió un bot que veo (releé lo que muestres). |
me:keys | — | Una clave de modelo mía se acerca o llegó a su tope del mes. |
me:connections | {} | Cambiaron mis cuentas conectadas. |
session:ended | — | Cerraron esta sesión desde otro dispositivo: el socket se corta enseguida. |
El socket es para recibir. Una conexión larga puede morirse sin avisar (un cambio de red, un proxy que la corta) y seguir diciendo connected unos segundos: lo que se emite en ese rato no llega a nadie. Un request HTTP contesta o falla. Por eso las apps de Puentes mandan todo por HTTP desde septiembre de 2026; socket.emit('message:send', …) sigue funcionando, pero no lo recomendamos. Si tu bot depende de no perderse nada, combiná el socket con GET /api/me/updates al reconectar.
Referencia de rutas
Todas bajo https://app.puentes.ai. Quién sale del código de cada ruta: todos = una sesión, una API key pk_ o un token de bot pb_; persona = sesión o API key, nunca un bot; persona (sesión) = solo la app; miembro = del chat de la ruta; permiso = lo que el grupo deja hacer a cada uno (Admins y permisos). Lo que la ruta chequea además (el dueño de un bot, un admin, una cuenta verificada) va al final. Límite es el de esa ruta, además de los generales (Límites).
Quién soy, config y lo nuevo
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
GET /api/me | todos | Quién soy. Un bot ve bot: true y ownerId. | — |
GET /api/config | público | Lo que este server tiene prendido: proveedores y niveles, cuota gratis, emailEnabled, mcpEnabled, connectors… Ver /api/config. | — |
GET /api/sockets | público | A qué host conectar el socket: { host, hosts } (host: null = https://app.puentes.ai). | — |
GET /api/me/groups | todos | Mis chats (grupos y directos) con miembros y último mensaje. | — |
GET /api/me/updates | todos | ?after=<seq>&timeout=<0-25>: los mensajes nuevos de todos mis chats desde ese cursor (menos los míos y los borrados), hasta 100, con long-poll. Sin after: { messages: [], next } con el cursor actual. Responde { messages, next }. | — |
GET /api/me/usage | todos | La cuota: { dayLimit, dayUsed, dayLeft, credits, left, day, lifetime } (más los nombres viejos freeUsed, freeLimit, freeLeft). Un bot ve la de su dueño. | — |
GET /api/developers/:id | todos | El perfil público de un desarrollador (kind: "person") o de una organización (kind: "org") y sus bots visibles para quien pregunta. | — |
Mensajes
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
GET /api/groups/:id/messages | todos · miembro | El historial. Un bot (pb_) sin parámetros: un array con los últimos 500, del más viejo al más nuevo. Una persona o una API key: una página, { messages, more, next, clearedAt } con los últimos 40 (?limit=1…100); ?before=<id> los anteriores (20 por default); ?before=<id>&to=<id> salta hasta un mensaje ({ tooFar: true } a más de 500); ?since=<next> lo que cambió desde el cursor next de la respuesta anterior: { messages, hidden, statuses, next }, o { reset: true, … } con la primera página si cambiaron más de 200. Un bot puede usar los mismos parámetros. | — |
POST /api/groups/:id/messages | todos · miembro | Enviar { text?, attachments?, replyTo?, clientId?, linkPreview? } (ver Enviar mensajes). clientId (hasta 60) lo hace idempotente por chat y remitente: si vuelve a llegar, la respuesta es el mensaje ya guardado y nadie lo recibe dos veces. linkPreview: false pide no armar la ficha del link. | 30 por minuto por bot, 60 por persona (Retry-After: 60) |
POST /api/groups/:id/read | todos · miembro | Leí el chat hasta { upTo? } (id de mensaje; sin él, hasta el último): tildes azules para los demás y el contador en cero en mis dispositivos. Un bot nunca «lee»: los tildes solo cuentan personas. | — |
POST /api/groups/:id/unread | todos · miembro | «Marcar como no leído» en todos mis dispositivos. | — |
POST /api/me/delivered | todos | Al reconectar: todo lo escrito en mis chats desde { since? } (epoch ms, a lo sumo 7 días) llegó a este dispositivo. Devuelve { ok, groups }. | — |
DELETE /api/groups/:id/messages/:mid | todos · miembro | Borrar, como en WhatsApp. ?scope=me (default): solo desaparece de mi historial (message:hidden a mis dispositivos). ?scope=all: un mensaje mío de los últimos 2 días (o cualquiera, si soy admin) queda como { deleted: true, deletedBy, text: "", attachments: [] } para todos (message:updated), sus archivos se borran y las citas traen replyTo.deleted. | 120 por hora por cuenta |
POST /api/groups/:id/typing | todos · miembro | «Está escribiendo…» unos segundos (typing en el socket). | — |
POST /api/upload | todos | Subir un archivo (multipart, campo file, hasta 25 MB; duration opcional para un audio). Devuelve el adjunto para attachments: { type: "image" | "audio" | "video" | "file", url, name, mime, size, duration? }. | 60 por hora por cuenta |
GET /api/groups/:id/polls | todos · miembro | Las encuestas abiertas del chat (hasta 20, de los últimos 30 días): [{ message }]. | — |
POST /api/groups/:id/polls | persona · miembro | Crear una encuesta { title (≤ 80), options: [2 a 12, ≤ 100 cada una], multi?, hours? (1-168), clientId? }. Devuelve { message }; no despierta a ninguna IA. | 30 por hora por cuenta |
POST /api/groups/:id/messages/:mid/vote | persona · miembro | Votar { ids: [...] } (vacío retira el voto). 409 si ya cerró. Devuelve { ok, card }; los votos los escribe solo el server. | 120 por hora por cuenta |
PUT /api/groups/:id/messages/:mid/reaction | persona · miembro | { emoji }: un emoji por persona y mensaje; null o "" saca el mío. 400 si no es un emoji. Devuelve { ok, reactions }. | 300 por hora por cuenta |
POST /api/groups/:id/messages/:mid/forward | persona · miembro | Reenviar { to: groupId, clientId? }: la copia es un mensaje mío con el texto y los archivos (nunca una tarjeta) y la marca forward. Devuelve { message, groupId }. | 60 por hora por cuenta |
GET /api/search | persona | Buscar en todos mis chats: ?q= (las palabras, en cualquier orden, sin importar mayúsculas ni tildes; también en la transcripción de los audios). Devuelve { results: [{ id, groupId, seq, senderId, senderName, senderType, createdAt, snippet: { text, ranges, audio? } }], more }, 30 por página del más nuevo al más viejo; la siguiente con ?before=<createdAt>&beforeSeq=<seq> del último. Solo lo que veo en el chat: nada borrado, nada de antes de vaciarlo, ninguna línea del sistema. 503 si tardó demasiado (con más palabras va más rápido). Solo personas. | 600 por hora por cuenta |
GET /api/groups/:id/search | persona · miembro | Lo mismo en un solo chat (el «Buscar» del chat): ?q=, ?before= y ?beforeSeq=; el mensaje se abre con GET /api/groups/:id/messages?before=…&to=<id>. | 600 por hora por cuenta |
GET /api/link-preview | persona | ?url=: la ficha de un link ({ url, title, site, description?, image?, imageW?, imageH?, icon? }) o 204 si no hay. | 600 por hora por cuenta |
Chats y miembros
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
GET /api/groups/:id | todos · miembro | Un chat: kind, miembros (personas y bots), peer si es directo, perms. Ver Objetos. | — |
POST /api/groups | persona | Crear un grupo { name, members?: [userId | { userId, email | phone }], assistant?, puentes? }. Los miembros iniciales tienen que ser contactos (comparten un chat) o bots que podés usar. Con assistant: { name, systemPrompt?, mode?, opening? } nace con ese asistente (un bot tuyo) en vez de Claude; puentes: false sin ninguna IA. | 30 por día por cuenta |
PATCH /api/groups/:id | persona · miembro | { name } o { avatar } (permiso editInfo), { linkOff } («solo por invitación») y { perms: { editInfo?, send?, addMembers?, shareLink?: "all" | "admins", approve? } } (admins; ver Admins y permisos). | — |
POST /api/groups/:id/link/reset | persona · miembro · permiso «admins» | Link de invitación nuevo; el anterior muere. | 10 por hora por cuenta |
POST /api/groups/:id/leave | persona · miembro | Salir del grupo (sin personas, se borra; sin admins, la persona más antigua pasa a admin). En un directo lo oculta hasta que el otro escribe. | — |
DELETE /api/groups/:id | persona · miembro · permiso «admins» | Eliminar el grupo para todos. | — |
POST /api/groups/:id/clear | persona · miembro · en un grupo, admin | Vaciar el chat para todos (mensajes y lo que recuerda la IA; listas y miembros quedan). group:cleared { id, by }. | 20 por hora por cuenta |
POST /api/groups/:id/members | persona · miembro · permiso «agregar gente» | Agregar una persona { userId, email?, phone? }: entra al instante si conocés su email o teléfono verificado, o si es un contacto; si no, 404 como si no existiera. | 60 por hora por cuenta |
PATCH /api/groups/:id/members/:userId | persona · miembro · admin | { role: "admin" | "member" }. A quien creó el grupo no se le quita. | 60 por hora por cuenta |
DELETE /api/groups/:id/members/:userId | persona · miembro · admin | Sacar a una persona: queda vetada (no vuelve por el link) hasta que un admin la deje volver. | 60 por hora por cuenta |
GET /api/groups/:id/bans | persona · miembro · permiso «admins» | Las personas sacadas del grupo. | — |
DELETE /api/groups/:id/bans/:userId | persona · miembro · permiso «admins» | Levantar un veto. | — |
GET /api/groups/:id/requests | persona · miembro · permiso «admins» | Con perms.approve: quién pidió entrar ({ id, user, via, addedByName, createdAt }). | — |
POST /api/groups/:id/requests/:rid/approve | persona · miembro · permiso «admins» | Dejar entrar. | 120 por hora por cuenta |
POST /api/groups/:id/requests/:rid/reject | persona · miembro · permiso «admins» | Rechazar (no se avisa). | 120 por hora por cuenta |
GET /api/invites/:token | público | Vista previa de una invitación (?i=: el token de una invitación por email). | — |
POST /api/invites/:token/join | persona | Entrar por el link. Un token de bot recibe 403: a un bot siempre lo suma alguien. Con perms.approve responde 202 { pending: true }. | 30 por hora por IP |
DELETE /api/invites/:token/request | persona | Cancelar mi pedido de entrada. | — |
GET /api/groups/:id/invites | persona · miembro | Las invitaciones por email pendientes. | — |
POST /api/groups/:id/invites | persona · miembro · permiso «agregar gente» · verificada | { email }: si ya usa Puentes y comparte un chat con vos entra directo (status: "added"); si no, invitación de 7 días ("invited"). | 20 por hora y 50 por día (10 el primer día de la cuenta) |
POST /api/groups/:id/invites/:inviteId/resend | persona · miembro | Reenviar una invitación. | — |
DELETE /api/groups/:id/invites/:inviteId | persona · miembro | Cancelarla. | — |
POST /api/groups/:id/invite-link | persona · miembro · permiso «agregar gente» | Un link personal a este grupo para alguien que todavía no está en Puentes ({ email | phone }); el server no manda nada, devuelve la url. Una cuenta sin nada verificado también lo pide, con topes más bajos, y siempre recibe el link. | 60 por hora por cuenta |
POST /api/direct/:userId | persona | Abrir (o encontrar) el chat directo con una persona o un bot. Con una persona hace falta compartir un chat, que los hayan presentado, o su { email } / { phone } verificado. | 60 por hora por cuenta |
POST /api/direct-invite | persona | { email } o { phone, country } (+ name): directo con alguien que no está en Puentes; devuelve su link personal (url) para mandárselo vos. Una cuenta sin nada verificado también, con topes más bajos: a ella siempre le devuelve el link, nunca el chat de alguien que ya está. | — |
POST /api/groups/:id/share | persona · miembro | Compartir en el chat { kind: "contact" | "bot", id, by? }: publica la tarjeta. Un contacto = presentación; un bot tuyo = permiso de uso para los del chat. | 30 por hora por cuenta |
Listas de tareas
Un bot también las arma y edita. Cada cambio responde con la lista entera y llega por el socket como tasklist:updated (o tasklist:deleted). Una tarea con fecha es un recordatorio: a la hora, todo el grupo recibe la notificación.
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
GET /api/groups/:id/tasklists | todos · miembro | Las listas del chat: [{ id, groupId, messageId, name, items: [Task], done, total }]. | — |
POST /api/groups/:id/tasklists | todos · miembro · permiso «escribir» | { name (≤ 80), items: ["…"] (hasta 50, ≤ 200 cada una) }. Una fecha en castellano al final de una tarea («… mañana 18hs») la vuelve recordatorio. Devuelve el mensaje con la tarjeta tasklist. | — |
PATCH /api/tasklists/:id | todos · miembro del chat de la lista | Renombrar { name }. | — |
DELETE /api/tasklists/:id | todos · miembro del chat de la lista | Borrarla: { ok, groupId, listId, message } (el mensaje queda con list: null). | — |
POST /api/tasklists/:id/tasks | todos · miembro del chat de la lista | Agregar { text }. | — |
PATCH /api/tasks/:id | todos · miembro del chat de la lista | Cambiar el texto { text } (una fecha nueva en el texto reemplaza la anterior). | — |
POST /api/tasks/:id/toggle | todos · miembro del chat de la lista | Tildar { done: true | false }. | — |
POST /api/tasks/:id/due | todos · miembro del chat de la lista | Fecha { dueAt } (epoch ms; vacío la saca). | — |
DELETE /api/tasks/:id | todos · miembro del chat de la lista | Borrar la tarea. | — |
Tablas
Una tabla es una tarjeta del chat con columnas tipadas (hasta 8: status, select, person, number, date, text, check) y hasta 200 filas; el server calcula el resumen (cuántas en cada estado, sumas, por persona). Un bot crea filas y las edita; cambiar las columnas, renombrar o borrar la tabla es de quien la armó o de un admin, nunca de un bot. Los cambios llegan como table:updated { groupId, table } y table:deleted.
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
GET /api/groups/:id/tables | todos · miembro | Las tablas del chat: [{ id, groupId, messageId, name, createdBy, columns, rows: [{ id, title, cells }], summary }]. | — |
POST /api/groups/:id/tables | todos · miembro · permiso «escribir» | { name (≤ 80), columns?: [{ name, type, options? }], rows?: [{ title, cells? }] }. Las ids de las columnas las arma el server con el nombre (minúsculas, sin acentos): para llenar celdas al crear, creá la tabla y usá las ids de la respuesta. Devuelve el mensaje con la tarjeta table. | — |
PATCH /api/tables/:id | todos · miembro del chat de la tabla; quien la armó o un admin; nunca un bot | { name?, columns? }: columns reemplaza todas (las que no mandás se borran, con sus celdas). | — |
DELETE /api/tables/:id | todos · miembro del chat de la tabla; quien la armó o un admin; nunca un bot | Borrarla: { ok, groupId, tableId, message }. | — |
POST /api/tables/:id/rows | todos · miembro del chat de la tabla | Una fila { title (≤ 120), cells?: { columnId: valor } }. Devuelve la tabla. | — |
PATCH /api/tables/:id/rows/:rid | todos · miembro del chat de la tabla | { title?, cells? }: las celdas que mandás cambian (null la vacía), las demás quedan. | — |
DELETE /api/tables/:id/rows/:rid | todos · miembro del chat de la tabla | Borrar la fila. | — |
Programados
Un programado lo crea una IA con un bloque ```schedule (decir algo a una hora, despertarse, o mandar algo en nombre de la persona): por la API se leen, se pausan y se cancelan. Objeto: { id, groupId, botId, botName, createdBy, action: "say" | "wake" | "send", text, rule, tz, nextAt, lastFiredAt, runs, paused, held, onlyFor, targetLabel, createdAt }; rule es { once: "YYYY-MM-DD HH:MM" }, { freq: "daily", time }, { freq: "weekly", days: [0-6], time } o { freq: "monthly", monthDay, time }, a la hora del lugar (tz). Los cambios llegan como schedules:updated { groupId, schedules }.
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
GET /api/groups/:id/schedules | todos · miembro | Los programados vivos del chat que puedo ver. | — |
PATCH /api/groups/:id/schedules/:sid | todos · miembro · quien lo pidió o un admin; nunca un bot | { paused: true | false }. Devuelve la lista del chat. | — |
DELETE /api/groups/:id/schedules/:sid | todos · miembro · quien lo pidió o un admin; nunca un bot | Cancelarlo. Devuelve la lista del chat. | — |
GET /api/me/schedules | persona | Lo que se manda en mi nombre en cualquier chat: { sends, grants } (Ajustes › Mensajes programados). | — |
POST /api/schedules/:sid/confirm | persona | Contestar la tarjeta «¿Lo mando en tu nombre?»: { yes: true | false } → { ok, result: "sent" | "scheduled" | "dropped" | "expired" }. | — |
POST /api/bots/:id/send-grant | persona | Dejar que ese bot mande en mi nombre lo que yo le pida. | — |
DELETE /api/bots/:id/send-grant | persona | Retirarlo (se borran sus envíos programados en mi nombre). | — |
Fijados
Dos cosas distintas: los mensajes fijados de un chat (hasta 3, como en WhatsApp; los fija una persona) y los fijados de un bot (una pastilla arriba del chat con una tarjeta, ver arriba).
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
GET /api/groups/:id/pinned | todos · miembro | Los mensajes fijados: [{ message, pinnedBy, pinnedByName, pinnedAt }]. | — |
POST /api/groups/:id/messages/:mid/pin | persona · miembro · permiso «fijar» | Fijar un mensaje (el cuarto saca al más viejo). pinned:changed { groupId }. | 60 por hora por cuenta |
DELETE /api/groups/:id/messages/:mid/pin | persona · miembro · permiso «fijar» | Desfijarlo. | — |
GET /api/groups/:id/pins | todos · miembro | Los fijados de los bots del chat (los del chat y los míos). | — |
PUT /api/groups/:id/pins/:pinId | bot · miembro | Poner o actualizar un fijado del bot: { label (≤ 24), badge?, detail?, icon?, card?, buttons?, forUserId? }. Hasta 4 por bot y chat. pin:updated. | 120 por hora por cuenta |
DELETE /api/groups/:id/pins/:pinId | bot · miembro | Sacarlo. pin:removed. | — |
Bots
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
POST /api/bots | persona · verificada; hasta 50 bots | Crear un bot (ver Crear un bot). Devuelve { bot, token?, group }. | 10 por hora por cuenta |
GET /api/bots | persona | Mis bots (con el prefijo del token, el webhook y su secreto). | — |
GET /api/bots/catalog | persona | Los bots que puedo agregar: de Puentes, públicos, míos y compartidos conmigo. | — |
GET /api/bots/browse | persona | La sección IAs: ?q=&chip= → { mine, team, catalog, next }. | — |
GET /api/bots/:id | todos | La ficha de un bot que puedo usar: más kind, shortDesc, tags, developer, mcp.tools, canUse; si puedo pedir acceso, una recortada con canRequest. | — |
PATCH /api/bots/:id | persona · quien lo puede editar | Cambiar cualquiera de los campos de Crear un bot. Además, solo para un admin del bot: pay, ownerKey, los topes, orgId, clonable e idea. | — |
DELETE /api/bots/:id | persona · dueño | Borrarlo: sale de todos los chats y sus directos desaparecen. | — |
POST /api/bots/:id/token | persona · dueño | Token nuevo (el anterior muere al instante). | — |
POST /api/bots/:id/clone | persona · verificada | «Hacer mi copia» de un bot clonable: { name?, avatar? } → { bot, group }. 403 con code: "not_clonable" si no se puede. | 10 por hora por cuenta |
GET /api/bots/:id/usage | persona · dueño | Cuánto gastó un bot que paga sus respuestas: { monthUsd, replies, people, since }. | — |
DELETE /api/bots/:id/owner-key | persona · admin del bot | Olvidar la clave de modelo con la que el bot paga. | — |
POST /api/bots/:id/verify | persona · dueño | Pedir el check verde (en el catálogo y con el desarrollador verificado): { verification: "pending" }, o 409 con missing. | 10 por hora por cuenta |
PUT /api/bots/:id/domains | persona · admin del bot | { domains: ["acme.com"] } de un bot authorized (hace falta un email verificado tuyo en cada dominio). | 30 por hora por cuenta |
POST /api/bots/:id/access-requests | persona | Pedir acceso a un bot authorized con pedidos abiertos. | 20 por hora por cuenta |
GET /api/bots/:id/access-requests | persona · admin del bot | Los pedidos pendientes. | — |
POST /api/bots/:id/access-requests/:rid/:decision | persona · admin del bot | :decision = approve | reject. | 120 por hora por cuenta |
GET /api/bots/:id/access | persona · dueño | Quién puede usar el bot además de su dueño (via: "share" | "chat" | "link" | "approved"). | — |
POST /api/bots/:id/access | persona · dueño | Dar acceso a un contacto { userId }. | 60 por hora por cuenta |
DELETE /api/bots/:id/access/:userId | persona · dueño | Quitarlo (su directo con el bot se borra). | — |
POST /api/bots/:id/share-link | persona · dueño | Link /bot/<token> para compartirlo (7 días, uno vivo por bot). | 10 por hora por cuenta |
DELETE /api/bots/:id/share-link | persona · dueño | Revocarlo. | — |
GET /api/bots/shared/:token | persona | Quién comparte qué. | — |
POST /api/bots/shared/:token/accept | persona | Aceptar: acceso para la cuenta logueada. | — |
POST /api/groups/:id/bots | persona · miembro · permiso «agregar gente» | Sumar { botId, mode? } (de Puentes, público, mío o compartido conmigo) al grupo. | 60 por hora por cuenta |
PATCH /api/groups/:id/bots/:botId | persona · miembro · permiso «cómo responde un bot» | Cómo responde ese bot en ese grupo: { mode?: "mention" | "always", alias?, connections? }. | — |
POST /api/groups/:id/bots/:botId/mode-ask | persona · miembro · permiso «cómo responde un bot» | Contestar la tarjeta privada «¿cómo responde?» de una IA nueva: { keep: true | false }. | 60 por hora por cuenta |
DELETE /api/groups/:id/bots/:botId | persona · miembro · permiso «agregar gente» | Sacar el bot del grupo. | — |
GET /api/me/developer | persona | Mi perfil de desarrollador, con complete y missing. | — |
PUT /api/me/developer | persona | { name, company?, bio, website?, contactEmail?, contactable?, country? }: completo, un bot puede ir al catálogo. | 30 por hora por cuenta |
POST /api/me/developer/verify | persona | Pedir el check verde para el perfil. | 5 por hora por cuenta |
GET /api/me/organizations | persona | Mis organizaciones. | — |
POST /api/organizations | persona | Crear una { name, bio, website?, contactEmail?, contactable?, country?, avatar? }: quien la crea es admin. Un bot pasa a ella con orgId. | 10 por hora por cuenta |
PATCH /api/organizations/:id | persona · admin de la organización | Editarla. | 60 por hora por cuenta |
DELETE /api/organizations/:id | persona · admin de la organización | Borrarla. | — |
GET /api/organizations/:id/members | persona | Sus miembros. | — |
POST /api/organizations/:id/members | persona · admin de la organización | Invitar { userId | email, role: "admin" | "dev" }. | 60 por hora por cuenta |
PATCH /api/organizations/:id/members/:uid | persona · admin de la organización | Cambiar el rol. | 60 por hora por cuenta |
DELETE /api/organizations/:id/members/:uid | persona | Sacar a alguien (el último admin no se va). | — |
POST /api/organizations/:id/accept | persona | Aceptar una invitación a la organización. | 30 por hora por cuenta |
POST /api/organizations/:id/decline | persona | Rechazarla. | 30 por hora por cuenta |
POST /api/organizations/:id/verify | persona · admin de la organización | Pedir el check verde para la organización. | 5 por hora por cuenta |
Bots con herramientas (MCP)
Lo que hace falta para escribir un servidor MCP está arriba; estas son las rutas.
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
POST /api/bots/:id/mcp/connect | persona (sesión) · dueño | Servidor OAuth: { url } para autorizar (vuelve a /api/mcp/callback); si ya hay tokens, { connected: true, tools }. | 10 por hora por cuenta |
POST /api/bots/:id/mcp/test | persona · dueño | Probar la conexión: { status: "connected", transport, tools } o 400 con el motivo. | 30 por hora por cuenta |
DELETE /api/bots/:id/mcp/tokens | persona · dueño | Desconectar: borra los tokens OAuth guardados. | — |
POST /api/mcp/post/:token | público · el token es la credencial | { text?, card?, pin? }: el bot publica por su cuenta en ese chat (el postUrl de _meta.puentes). 202; 404 si ya no vale. El texto, hasta 4000. | 240 por hora por IP; 30 mensajes por minuto del bot |
POST /api/mcp/event/:token | público · el token es la credencial | { text (≤ 500) }: línea de sistema en el chat y la IA retoma (el callbackUrl, 24 horas). 202; 404 vencido. | 120 por hora por IP |
GET /api/mcp/callback | público | La vuelta de OAuth (la abre el navegador, no tu código). | 30 por hora por IP |
POST /api/mcp/callback | público | La confirmación de esa vuelta cuando el navegador no es el que pidió la URL. | 30 por hora por IP |
Conectores y cuentas
Un conector es un servidor MCP registrado (por una persona o una organización) que varias IAs pueden usar; una cuenta es la credencial de una persona en ese servicio. Cada IA dice con qué cuenta trabaja cada conector: la de cada persona (person), una por chat (chat) o la de quien la hizo (owner). Solo con connectors: true en /api/config. Nunca vuelven la clave del servidor, los tokens ni la URL de un conector ajeno.
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
GET /api/connectors/browse | persona | Los conectores que puedo usar: [{ id, name, description, avatar, category, verified, by, auth, accountModes, exclusive, connectHelp, tools: [{ name, label, changes?, confirm? }], bots }]. | — |
GET /api/connectors/:id | persona | Uno (?botId= si lo veo por una IA que lo usa). | — |
GET /api/connectors | persona | Los míos y los de mis organizaciones, con url, visibility, status, toolLabels y hasServerKey. | — |
POST /api/connectors | persona (sesión) | Registrar uno { name, url (https pública), orgId?, description?, category?, connectHelp?, avatar?, accountModes?: ["person" | "chat" | "owner"], exclusive?, visibility?, auth?: "none" | "bearer" | "oauth", serverKey?, toolLabels? }. Hasta 30 por dueño; exige el perfil de desarrollador completo. | 20 por hora por cuenta |
PATCH /api/connectors/:id | persona (sesión) | Los mismos campos. Cambiar url o auth lo reinicia. | — |
POST /api/connectors/:id/test | persona | Volver a probarlo (detecta el acceso y lista las herramientas). | 30 por hora por cuenta |
DELETE /api/connectors/:id | persona | Borrarlo. | — |
GET /api/me/connections | persona | «Mis cuentas»: [{ id, connectorId, connector, label, status: "ok" | "reconnect" | "error", createdAt, lastUsedAt?, bots }]. | — |
GET /api/me/connections/:id | persona | Una cuenta, con las IAs y los chats que la usan. | — |
POST /api/me/connections | persona (sesión) · con un email, teléfono, Google o Apple | Conectar { connectorId, secret?, botId?, groupId? }: con clave, { connection }; con OAuth, { url } para autorizar. Con botId y groupId se la presta a esa IA en ese chat y la despierta. | 20 por hora por cuenta |
POST /api/me/connections/:id/reconnect | persona (sesión) | Volver a entrar (clave nueva u OAuth). | 20 por hora por cuenta |
POST /api/me/connections/:id/use | persona (sesión) | Prestarle esa cuenta a una IA { botId, groupId? }. | 30 por hora por cuenta |
PATCH /api/me/connections/:id | persona | Nombre de la cuenta { label }. | — |
DELETE /api/me/connections/:id | persona | Desconectarla. | — |
DELETE /api/me/connections/:id/bots/:botId | persona | Dejar de prestársela a esa IA. | — |
DELETE /api/me/connections/:id/chats/:groupId | persona | Sacarla de ese chat. | — |
PUT /api/bots/:id/connectors | persona (sesión) · quien la puede editar | Los conectores de una IA { connectors: [{ connectorId, account?: "person" | "chat" | "owner" }] } (reemplaza la lista; hasta 8). | — |
La cuenta de una persona
Lo que la app hace con su propia sesión. Una API key no administra sesiones, claves ni formas de entrar (403).
| Ruta | Quién | Qué hace | Límite |
|---|---|---|---|
PATCH /api/me | persona | { locale?, avatar?, country?, tz?, linkPreviews? } (al menos uno). El nombre no se cambia acá. | — |
DELETE /api/me | persona (sesión) | Borrar la cuenta entera, al instante: membresías, directos, IAs propias, emails, teléfonos, sesiones, API keys. Los mensajes quedan para los demás como «Cuenta eliminada». No se puede deshacer. | 5 por hora por cuenta |
GET /api/me/keys | persona (sesión) | Mis API keys. | — |
POST /api/me/keys | persona (sesión) | Crear una { name }: devuelve pk_… una sola vez. Hasta 10. | 10 por hora por cuenta |
DELETE /api/me/keys/:id | persona (sesión) | Revocarla. | — |
GET /api/me/emails | persona (sesión) | Mis emails. | — |
POST /api/me/emails | persona (sesión) | { email }: manda un código de 6 dígitos. | — |
POST /api/me/emails/verify | persona (sesión) | { email, code }: lo confirma. | — |
DELETE /api/me/emails/:email | persona (sesión) | Sacarlo. | — |
GET /api/me/phones | persona (sesión) | Mis teléfonos. | — |
POST /api/me/phones | persona (sesión) | { phone, channel: "sms" | "whatsapp" }: manda un código (si smsEnabled). | — |
POST /api/me/phones/verify | persona (sesión) | { phone, code }. Un número es de una sola cuenta. | — |
DELETE /api/me/phones/:phone | persona (sesión) | Sacarlo. | — |
POST /api/contacts/lookup | persona · verificada | { email } o { phone } → { status: "found", user } | { status: "none" } | { status: "self" }. Nunca devuelve el email ni el número. | 30 por hora |
GET /api/me/blocks | persona | A quiénes bloqueé. | — |
POST /api/users/:id/block | persona | Bloquear (ver bloqueo). | 60 por hora por cuenta |
DELETE /api/users/:id/block | persona | Desbloquear. | — |
POST /api/users/:id/report | persona | { reason?, groupId?, block? }: reporte al equipo de Puentes. | 5 por día |
POST /api/reports | persona | Reportar un bot, un desarrollador o una organización { about: "bot" | "developer" | "org", id, reason? }. | — |
Solo personas (no para bots)
Las rutas de esta página que un token de bot no puede llamar (403): reaccionar, votar, reenviar, fijar mensajes, crear grupos y bots, invitar, compartir, bloquear, y todo lo de la cuenta.
| Ruta | Con | Sección |
|---|---|---|
POST /api/groups/:id/polls | sesión o API key | Mensajes |
POST /api/groups/:id/messages/:mid/vote | sesión o API key | Mensajes |
PUT /api/groups/:id/messages/:mid/reaction | sesión o API key | Mensajes |
POST /api/groups/:id/messages/:mid/forward | sesión o API key | Mensajes |
GET /api/search | sesión o API key | Mensajes |
GET /api/groups/:id/search | sesión o API key | Mensajes |
GET /api/link-preview | sesión o API key | Mensajes |
POST /api/groups | sesión o API key | Chats y miembros |
PATCH /api/groups/:id | sesión o API key | Chats y miembros |
POST /api/groups/:id/link/reset | sesión o API key | Chats y miembros |
POST /api/groups/:id/leave | sesión o API key | Chats y miembros |
DELETE /api/groups/:id | sesión o API key | Chats y miembros |
POST /api/groups/:id/clear | sesión o API key | Chats y miembros |
POST /api/groups/:id/members | sesión o API key | Chats y miembros |
PATCH /api/groups/:id/members/:userId | sesión o API key | Chats y miembros |
DELETE /api/groups/:id/members/:userId | sesión o API key | Chats y miembros |
GET /api/groups/:id/bans | sesión o API key | Chats y miembros |
DELETE /api/groups/:id/bans/:userId | sesión o API key | Chats y miembros |
GET /api/groups/:id/requests | sesión o API key | Chats y miembros |
POST /api/groups/:id/requests/:rid/approve | sesión o API key | Chats y miembros |
POST /api/groups/:id/requests/:rid/reject | sesión o API key | Chats y miembros |
POST /api/invites/:token/join | sesión o API key | Chats y miembros |
DELETE /api/invites/:token/request | sesión o API key | Chats y miembros |
GET /api/groups/:id/invites | sesión o API key | Chats y miembros |
POST /api/groups/:id/invites | sesión o API key | Chats y miembros |
POST /api/groups/:id/invites/:inviteId/resend | sesión o API key | Chats y miembros |
DELETE /api/groups/:id/invites/:inviteId | sesión o API key | Chats y miembros |
POST /api/groups/:id/invite-link | sesión o API key | Chats y miembros |
POST /api/direct/:userId | sesión o API key | Chats y miembros |
POST /api/direct-invite | sesión o API key | Chats y miembros |
POST /api/groups/:id/share | sesión o API key | Chats y miembros |
GET /api/me/schedules | sesión o API key | Programados |
POST /api/schedules/:sid/confirm | sesión o API key | Programados |
POST /api/bots/:id/send-grant | sesión o API key | Programados |
DELETE /api/bots/:id/send-grant | sesión o API key | Programados |
POST /api/groups/:id/messages/:mid/pin | sesión o API key | Fijados |
DELETE /api/groups/:id/messages/:mid/pin | sesión o API key | Fijados |
POST /api/bots | sesión o API key | Bots |
GET /api/bots | sesión o API key | Bots |
GET /api/bots/catalog | sesión o API key | Bots |
GET /api/bots/browse | sesión o API key | Bots |
PATCH /api/bots/:id | sesión o API key | Bots |
DELETE /api/bots/:id | sesión o API key | Bots |
POST /api/bots/:id/token | sesión o API key | Bots |
POST /api/bots/:id/clone | sesión o API key | Bots |
GET /api/bots/:id/usage | sesión o API key | Bots |
DELETE /api/bots/:id/owner-key | sesión o API key | Bots |
POST /api/bots/:id/verify | sesión o API key | Bots |
PUT /api/bots/:id/domains | sesión o API key | Bots |
POST /api/bots/:id/access-requests | sesión o API key | Bots |
GET /api/bots/:id/access-requests | sesión o API key | Bots |
POST /api/bots/:id/access-requests/:rid/:decision | sesión o API key | Bots |
GET /api/bots/:id/access | sesión o API key | Bots |
POST /api/bots/:id/access | sesión o API key | Bots |
DELETE /api/bots/:id/access/:userId | sesión o API key | Bots |
POST /api/bots/:id/share-link | sesión o API key | Bots |
DELETE /api/bots/:id/share-link | sesión o API key | Bots |
GET /api/bots/shared/:token | sesión o API key | Bots |
POST /api/bots/shared/:token/accept | sesión o API key | Bots |
POST /api/groups/:id/bots | sesión o API key | Bots |
PATCH /api/groups/:id/bots/:botId | sesión o API key | Bots |
POST /api/groups/:id/bots/:botId/mode-ask | sesión o API key | Bots |
DELETE /api/groups/:id/bots/:botId | sesión o API key | Bots |
GET /api/me/developer | sesión o API key | Bots |
PUT /api/me/developer | sesión o API key | Bots |
POST /api/me/developer/verify | sesión o API key | Bots |
GET /api/me/organizations | sesión o API key | Bots |
POST /api/organizations | sesión o API key | Bots |
PATCH /api/organizations/:id | sesión o API key | Bots |
DELETE /api/organizations/:id | sesión o API key | Bots |
GET /api/organizations/:id/members | sesión o API key | Bots |
POST /api/organizations/:id/members | sesión o API key | Bots |
PATCH /api/organizations/:id/members/:uid | sesión o API key | Bots |
DELETE /api/organizations/:id/members/:uid | sesión o API key | Bots |
POST /api/organizations/:id/accept | sesión o API key | Bots |
POST /api/organizations/:id/decline | sesión o API key | Bots |
POST /api/organizations/:id/verify | sesión o API key | Bots |
POST /api/bots/:id/mcp/connect | solo la sesión de la app | Bots con herramientas (MCP) |
POST /api/bots/:id/mcp/test | sesión o API key | Bots con herramientas (MCP) |
DELETE /api/bots/:id/mcp/tokens | sesión o API key | Bots con herramientas (MCP) |
GET /api/connectors/browse | sesión o API key | Conectores y cuentas |
GET /api/connectors/:id | sesión o API key | Conectores y cuentas |
GET /api/connectors | sesión o API key | Conectores y cuentas |
POST /api/connectors | solo la sesión de la app | Conectores y cuentas |
PATCH /api/connectors/:id | solo la sesión de la app | Conectores y cuentas |
POST /api/connectors/:id/test | sesión o API key | Conectores y cuentas |
DELETE /api/connectors/:id | sesión o API key | Conectores y cuentas |
GET /api/me/connections | sesión o API key | Conectores y cuentas |
GET /api/me/connections/:id | sesión o API key | Conectores y cuentas |
POST /api/me/connections | solo la sesión de la app | Conectores y cuentas |
POST /api/me/connections/:id/reconnect | solo la sesión de la app | Conectores y cuentas |
POST /api/me/connections/:id/use | solo la sesión de la app | Conectores y cuentas |
PATCH /api/me/connections/:id | sesión o API key | Conectores y cuentas |
DELETE /api/me/connections/:id | sesión o API key | Conectores y cuentas |
DELETE /api/me/connections/:id/bots/:botId | sesión o API key | Conectores y cuentas |
DELETE /api/me/connections/:id/chats/:groupId | sesión o API key | Conectores y cuentas |
PUT /api/bots/:id/connectors | solo la sesión de la app | Conectores y cuentas |
PATCH /api/me | sesión o API key | La cuenta de una persona |
DELETE /api/me | solo la sesión de la app | La cuenta de una persona |
GET /api/me/keys | solo la sesión de la app | La cuenta de una persona |
POST /api/me/keys | solo la sesión de la app | La cuenta de una persona |
DELETE /api/me/keys/:id | solo la sesión de la app | La cuenta de una persona |
GET /api/me/emails | solo la sesión de la app | La cuenta de una persona |
POST /api/me/emails | solo la sesión de la app | La cuenta de una persona |
POST /api/me/emails/verify | solo la sesión de la app | La cuenta de una persona |
DELETE /api/me/emails/:email | solo la sesión de la app | La cuenta de una persona |
GET /api/me/phones | solo la sesión de la app | La cuenta de una persona |
POST /api/me/phones | solo la sesión de la app | La cuenta de una persona |
POST /api/me/phones/verify | solo la sesión de la app | La cuenta de una persona |
DELETE /api/me/phones/:phone | solo la sesión de la app | La cuenta de una persona |
POST /api/contacts/lookup | sesión o API key | La cuenta de una persona |
GET /api/me/blocks | sesión o API key | La cuenta de una persona |
POST /api/users/:id/block | sesión o API key | La cuenta de una persona |
DELETE /api/users/:id/block | sesión o API key | La cuenta de una persona |
POST /api/users/:id/report | sesión o API key | La cuenta de una persona |
POST /api/reports | sesión o API key | La cuenta de una persona |
Contactos, compartir y bloqueo
- No hay libreta de contactos. Podés escribirle a alguien si comparten un chat, si alguien que conoce a los dos lo presentó (tarjeta de contacto en un chat) o si conocés su email verificado. Saber el email de una persona alcanza para escribirle, como el número en WhatsApp.
- Compartir es siempre autenticado. Un bot compartido cede solo el permiso de uso: nunca su token, el secreto del webhook, el prompt ni claves. Una tarjeta de contacto lleva nombre y avatar, nunca email ni teléfono. Las tarjetas (
attachments[].type = 'bot' | 'contact' | 'mcp_connect' | 'tools' | 'invite' | 'confirm' | 'forward' | 'cap') las construye el server: un cliente no puede mandarlas; las de un bot (options,picks,link,catalog,form) llevan siempre elbotIddel bot que las mandó. - Un bot con herramientas usa la cuenta de su dueño. Compartirlo es dejar que esa gente ejecute sus herramientas (con lo que el servidor MCP permita a esa credencial). El rastro queda en cada respuesta; el dueño puede desconectarlo, quitar accesos o borrarlo.
- Bloqueo. Si A bloquea a B: B no puede abrirle un chat ni sumarlo a grupos nuevos; lo que B escribe en el directo queda solo del lado de B (tilde simple, sin push, sin AI, sin webhook) y nunca aparece en el historial de A, ni al desbloquear. A tampoco puede escribirle hasta desbloquear. B no recibe ningún aviso.
- Un bot y las personas. Un bot privado lo usan el dueño y quienes tengan acceso (
bot_access); un bot público, cualquiera. Un bot externo recibe por webhook los mensajes de todos los chats donde está: si lo compartís, tu código va a ver mensajes de esa gente.
Admins y permisos de grupo
- Admins. Quien crea un grupo es su primer admin (
members[].role = 'admin'); cualquier admin nombra o quita admins y saca gente. A quien creó el grupo nadie lo saca ni le quita admin mientras esté. Si el último admin se va, la persona más antigua pasa a admin. Un bot nunca es admin: lo agrega y lo saca alguien con permiso. - Permisos (
permsen el chat):editInfo,send,addMembers,shareLink, cada uno'all'o'admins', yapprove. Un grupo nuevo está abierto. Consend: 'admins'un bot externo recibe 403 al escribir (una AI del grupo sigue contestando a los admins). ConshareLink: 'admins'el chat llega sininviteTokena quien no puede compartirlo. - Aprobar nuevos miembros (
approve: true): entrar por el link responde202 { pending: true }; lo que agrega o invita un admin entra directo. Un rechazo no avisa nada. - Sacar a alguien lo veta: no vuelve por el link ni pidiendo entrar; solo un admin lo deja volver.
Objetos
Los que más se usan, campo por campo. Salen de los mismos schemas que /openapi.json (components.schemas), donde están todos los demás (listas, tablas, programados, conectores…). Un campo opcional viene solo cuando aplica; la descripción dice cuándo.
Message
| Campo | Tipo | Qué es |
|---|---|---|
id | string | |
seq | integer | Cursor global creciente (GET /api/me/updates). |
groupId | string | |
senderType | "user" | "bot" | "system" | |
senderId | string | null | null en una línea de sistema. |
senderName | string | |
text | string ≤ 20000 | "" en un mensaje borrado para todos. |
attachments | array de Attachment | [] en un mensaje borrado; las tarjetas de lista y tabla vienen con su estado actual. |
createdAt | integer | Epoch ms. |
replyTo | MessageQuote | object | null | |
reactions | Reactions | |
via opcional | object | Solo si lo mandó un asistente en nombre de la persona. |
onlyFor opcional | string | Solo en un mensaje privado: la única persona que lo ve. |
suggest opcional | array de MenuItem | Solo en una respuesta de bot con acciones sugeridas. |
link opcional | LinkPreview | Solo cuando el server armó la ficha del primer link (llega después, por message:updated). Nunca en uno borrado ni en uno de bot. |
deleted opcional | true | Solo si se borró para todos. |
deletedAt opcional | integer | Solo si se borró. |
deletedBy opcional | string | null | Solo si se borró. |
status opcional | "sent" | "delivered" | "read" | Solo en mis mensajes, en el historial de una persona: los tildes. |
clientId opcional | string ≤ 60 | Solo en la respuesta al envío y en su message:new. |
Chat
| Campo | Tipo | Qué es |
|---|---|---|
id | string | |
name | string | |
avatar | string | null | |
kind | "group" | "direct" | |
direct | boolean | |
inviteToken opcional | string | null | Solo para quien puede compartir el link. |
linkOff | boolean | «Solo por invitación». |
createdBy | string | null | |
perms opcional | ChatPerms | Solo en grupos. |
members | array de ChatMember | |
lastMessage | Message | object | null | |
isMember opcional | boolean | Solo en las respuestas a vos. |
myRole opcional | "admin" | "member" | Solo grupos, solo en las respuestas a vos. |
pendingRequests opcional | integer | Para un admin de un grupo que aprueba a los nuevos. |
mutedUntil opcional | number | 0 sin silencio, < 0 para siempre, si no epoch ms. Solo a vos. |
favorite opcional | boolean | Solo a vos. |
peer opcional | ChatMember | object | null | Solo en un directo: el otro. |
blocked opcional | boolean | Directo con alguien que bloqueaste. |
pendingEmail opcional | string | null | Directo que todavía nadie aceptó. |
pendingPhone opcional | string | null | |
referral opcional | ChatReferral | object | null | Solo grupos: el premio por invitar. |
ChatBotMember
| Campo | Tipo | Qué es |
|---|---|---|
id | string | |
name | string | |
color opcional | string | |
avatar opcional | string | null | |
kind | "bot" | |
runtime | "ai" | "api" | |
builtin opcional | boolean | |
public opcional | boolean | |
verified opcional | "official" | "trusted" | null | El check: dorado (de Puentes) o verde (verificado). Solo lectura. |
ownerId opcional | string | null | null en los de Puentes. |
description opcional | string | |
provider opcional | string | null | |
model opcional | string | null | |
tier opcional | integer | null | 1, 2 o 3 (solo ai). |
mode | "mention" | "always" | Cómo responde en ESTE chat. |
mcp opcional | boolean | Tiene herramientas. |
menu opcional | array de MenuItem | |
alias opcional | string | null | Su alias en ESTE chat. |
by opcional | string | null | De quién es (la organización, o quien lo hizo). |
Bot
Lo que dice «Solo para el dueño» llega solo a quien lo puede editar. Nunca vuelven la clave, los tokens ni la URL de un MCP ajeno.
| Campo | Tipo | Qué es |
|---|---|---|
id | string | |
name | string | |
color opcional | string | |
avatar opcional | string | null | |
runtime | "ai" | "api" | |
builtin | boolean | De Puentes. |
public opcional | boolean | |
verified | "official" | "trusted" | null | El check: dorado (de Puentes) o verde (verificado). Solo lectura. |
ownerId opcional | string | null | |
description opcional | string | |
createdAt opcional | integer | |
provider opcional | string | Solo ai. |
model opcional | string | Solo ai. |
tier opcional | integer | Solo ai. |
mode opcional | string | null | Solo ai: el default. |
menu opcional | array de MenuItem | |
alias opcional | string | null | |
kind | "ai" | "external" | "tools" | |
shortDesc opcional | string | |
category opcional | string | null | |
tags opcional | array de string | |
language opcional | string | null | |
countries opcional | array de string | |
visibility | "private" | "link" | "authorized" | "catalog" | |
pays opcional | "owner" | "user" | |
developer opcional | object | null | |
org opcional | object | null | |
chats opcional | integer | En cuántos chats está. |
template opcional | string | null | |
clonable opcional | boolean | |
source opcional | object | null | Una copia: su original. |
following opcional | boolean | Una copia que sigue a su original. |
idea opcional | object | null | |
by opcional | string | null | |
mcp opcional | object | Solo si tiene un servidor propio. |
connectors opcional | array de object | Solo si usa conectores. |
requestsOpen opcional | boolean | Solo para el dueño. |
pay opcional | "owner" | "user" | Solo para el dueño. |
capMonthUsd opcional | number | null | |
perUserDailyReplies opcional | integer | null | |
perUserMonthUsd opcional | number | null | |
verification opcional | "trusted" | "pending" | "none" | Solo para el dueño. |
verificationRequestedAt opcional | integer | null | |
actions opcional | array de MenuItem | Solo para el dueño. |
systemPrompt opcional | string | Solo para el dueño. |
tokenPrefix opcional | string | null | Solo para el dueño. |
webhookUrl opcional | string | Solo para el dueño. |
webhookSecret opcional | string | null | Solo para el dueño. |
lastUsedAt opcional | integer | null | Solo para el dueño. |
User
| Campo | Tipo | Qué es |
|---|---|---|
id | string | |
name | string | |
color | string | |
avatar | string | null | |
bot | boolean |
Adjuntos y tarjetas (attachments[])
Se distinguen por type (y las tarjetas options por layout). picks, catalog y confirm llegan con su tipo viejo o como options con ese layout: leé las dos formas. Cada forma, campo por campo, está en /openapi.json (components.schemas).
| type | Schema | Qué es |
|---|---|---|
image | audio | video | file | FileAttachment | Una foto, un audio (con su transcripción), un video o un archivo, subidos con POST /api/upload. |
location | LocationAttachment | Una ubicación; la dirección la agrega el server después. |
choice | ChoiceAttachment | Lo que una persona tocó en una tarjeta: despierta al bot que la mandó. Es la única tarjeta que manda un cliente. |
options | OptionsCard | Una lista para elegir (filas, carrusel, carteles o botones), o una encuesta (poll). |
options · layout picks | OptionsPicksCard | picks, en la forma nueva. |
options · layout catalog | OptionsCatalogCard | catalog, en la forma nueva (ítems con section). |
options · layout confirm | OptionsConfirmCard | confirm, en la forma nueva. |
picks | PicksCard | Lo que una herramienta resolvió sola, con alternativas por renglón (forma vieja). |
catalog | CatalogCard | Un catálogo con carrito (forma vieja). |
form | FormCard | Un formulario corto, con respuestas guardadas para tocar. |
link | LinkCard | Un botón que abre una página. |
tasklist | TasklistCard | Una lista de tareas, siempre con su estado actual. |
table | TableCard | Una tabla, siempre con su estado actual. |
bot | BotCard | Un bot compartido en el chat: da permiso de uso. |
contact | ContactCard | Una persona compartida en el chat. |
tools | ToolsAttachment | Qué herramientas usó un bot en esta respuesta. |
mcp_connect | McpConnectAttachment | El bot necesita que alguien conecte su cuenta. |
confirm | ConfirmCard | Una pregunta de sí o no para una sola persona. Solo la crea el server. |
forward | ForwardAttachment | La marca de «Reenviado». |
cap | CapAttachment | En una línea de sistema: se llegó a un tope de gasto. |
invite | InviteAttachment | El botón «Invitar gente al grupo» del saludo de una IA. |
Ejemplos: un bot eco
Responde citando en sus chats directos y cuando lo mencionan en un grupo. Node 18+, sin dependencias:
const BASE = 'https://app.puentes.ai';
const TOKEN = process.env.PUENTES_BOT_TOKEN;
const api = async (path, opts = {}) => {
const res = await fetch(BASE + path, { ...opts, headers: { authorization: `Bearer ${TOKEN}`, 'content-type': 'application/json' } });
if (!res.ok) throw new Error(`${path} ${res.status}`);
return res.json();
};
const me = await api('/api/me');
const mention = new RegExp(`(^|\\s)@${me.name}(?=$|[\\s.,;:!?])`, 'i');
const chats = new Map();
const chatOf = async (id) => chats.get(id) || (chats.set(id, await api(`/api/groups/${id}`)), chats.get(id));
let { next } = await api('/api/me/updates');
for (;;) {
const r = await api(`/api/me/updates?after=${next}&timeout=25`).catch(() => ({ messages: [], next }));
next = r.next;
for (const m of r.messages) {
if (m.senderType === 'system') continue;
const chat = await chatOf(m.groupId);
if (!chat.direct && !mention.test(m.text || '')) continue;
await api(`/api/groups/${m.groupId}/messages`, { method: 'POST', body: JSON.stringify({ text: `Eco: ${m.text}`, replyTo: m.id }) });
}
}
El mismo en Python 3, sin dependencias:
import json, os, re, uuid, urllib.request
BASE, TOKEN = 'https://app.puentes.ai', os.environ['PUENTES_BOT_TOKEN']
def api(path, body=None):
req = urllib.request.Request(BASE + path, data=json.dumps(body).encode() if body is not None else None,
headers={'Authorization': f'Bearer {TOKEN}', 'Content-Type': 'application/json'},
method='POST' if body is not None else 'GET')
with urllib.request.urlopen(req, timeout=35) as res:
return json.load(res)
me = api('/api/me')
mention = re.compile(rf"(^|s)@{re.escape(me['name'])}(?=$|[s.,;:!?])", re.I)
chats = {}
nxt = api('/api/me/updates')['next']
while True:
try:
r = api(f'/api/me/updates?after={nxt}&timeout=25')
except Exception:
continue
nxt = r['next']
for m in r['messages']:
if m['senderType'] == 'system':
continue
chat = chats.setdefault(m['groupId'], api(f"/api/groups/{m['groupId']}"))
if not chat['direct'] and not mention.search(m.get('text') or ''):
continue
api(f"/api/groups/{m['groupId']}/messages", {'text': f"Eco: {m.get('text')}", 'replyTo': m['id'], 'clientId': str(uuid.uuid4())})
El de Node, listo para correr: node scripts/example-bot.mjs en el repo.
Límites y buenas prácticas
- Los generales y los de cada ruta están en la tabla de abajo; lo que pasa de un límite recibe
429, conRetry-After(segundos) cuando se sabe cuánto esperar. - Además, por cuenta y por hora: 60 chats directos, 60 altas de bots en grupos, 30 tarjetas compartidas, 10 bots nuevos, 10 links de bot, 10 restablecimientos de link; por día: 30 grupos, 20/50 invitaciones por email (10 el primer día), 5 reportes. Por IP: 20 links de acceso o handoff cada 10 minutos, 30 entradas por link por hora.
- Bots con herramientas: cada respuesta descuenta de la cuota de quien le habla lo que cuesta su nivel (1, 2 o 3), una vez, use las rondas que use.
- Un bot no ve sus propios mensajes en
updatesni por webhook. Sí ve los de otros bots: si respondés a todo, cuidá los loops entre bots. - Nombres: hasta 40 caracteres; en un grupo no puede haber dos participantes con el mismo nombre.
- Los tildes de entregado/leído solo cuentan personas: un bot nunca "lee".
- Tratá el token como una contraseña. Si se filtró, regeneralo desde la sección IAs, editando el bot, o con
POST /api/bots/:id/token.
| Ruta | Límite |
|---|---|
Todo /api | 600 pedidos por minuto por IP |
| Mensajes | 30 por minuto por bot, 60 por persona; hasta 20.000 caracteres y 10 adjuntos |
| Cuentas nuevas | 20 por hora y 50 por día por IP (más para una IP compartida conocida) |
| Cuenta | hasta 50 bots, 10 API keys, 30 conectores, 40 cuentas conectadas |
| Bots con herramientas | 6 rondas por respuesta, 30 s por herramienta, 90 s por respuesta, resultados recortados a 8.000 caracteres |
POST /api/groups/:id/messages | 30 por minuto por bot, 60 por persona (Retry-After: 60) |
DELETE /api/groups/:id/messages/:mid | 120 por hora por cuenta |
POST /api/upload | 60 por hora por cuenta |
POST /api/groups/:id/polls | 30 por hora por cuenta |
POST /api/groups/:id/messages/:mid/vote | 120 por hora por cuenta |
PUT /api/groups/:id/messages/:mid/reaction | 300 por hora por cuenta |
POST /api/groups/:id/messages/:mid/forward | 60 por hora por cuenta |
GET /api/search | 600 por hora por cuenta |
GET /api/groups/:id/search | 600 por hora por cuenta |
GET /api/link-preview | 600 por hora por cuenta |
POST /api/groups | 30 por día por cuenta |
POST /api/groups/:id/link/reset | 10 por hora por cuenta |
POST /api/groups/:id/clear | 20 por hora por cuenta |
POST /api/groups/:id/members | 60 por hora por cuenta |
PATCH /api/groups/:id/members/:userId | 60 por hora por cuenta |
DELETE /api/groups/:id/members/:userId | 60 por hora por cuenta |
POST /api/groups/:id/requests/:rid/approve | 120 por hora por cuenta |
POST /api/groups/:id/requests/:rid/reject | 120 por hora por cuenta |
POST /api/invites/:token/join | 30 por hora por IP |
POST /api/groups/:id/invites | 20 por hora y 50 por día (10 el primer día de la cuenta) |
POST /api/groups/:id/invite-link | 60 por hora por cuenta |
POST /api/direct/:userId | 60 por hora por cuenta |
POST /api/groups/:id/share | 30 por hora por cuenta |
POST /api/groups/:id/messages/:mid/pin | 60 por hora por cuenta |
PUT /api/groups/:id/pins/:pinId | 120 por hora por cuenta |
POST /api/bots | 10 por hora por cuenta |
POST /api/bots/:id/clone | 10 por hora por cuenta |
POST /api/bots/:id/verify | 10 por hora por cuenta |
PUT /api/bots/:id/domains | 30 por hora por cuenta |
POST /api/bots/:id/access-requests | 20 por hora por cuenta |
POST /api/bots/:id/access-requests/:rid/:decision | 120 por hora por cuenta |
POST /api/bots/:id/access | 60 por hora por cuenta |
POST /api/bots/:id/share-link | 10 por hora por cuenta |
POST /api/groups/:id/bots | 60 por hora por cuenta |
POST /api/groups/:id/bots/:botId/mode-ask | 60 por hora por cuenta |
PUT /api/me/developer | 30 por hora por cuenta |
POST /api/me/developer/verify | 5 por hora por cuenta |
POST /api/organizations | 10 por hora por cuenta |
PATCH /api/organizations/:id | 60 por hora por cuenta |
POST /api/organizations/:id/members | 60 por hora por cuenta |
PATCH /api/organizations/:id/members/:uid | 60 por hora por cuenta |
POST /api/organizations/:id/accept | 30 por hora por cuenta |
POST /api/organizations/:id/decline | 30 por hora por cuenta |
POST /api/organizations/:id/verify | 5 por hora por cuenta |
POST /api/bots/:id/mcp/connect | 10 por hora por cuenta |
POST /api/bots/:id/mcp/test | 30 por hora por cuenta |
POST /api/mcp/post/:token | 240 por hora por IP; 30 mensajes por minuto del bot |
POST /api/mcp/event/:token | 120 por hora por IP |
GET /api/mcp/callback | 30 por hora por IP |
POST /api/mcp/callback | 30 por hora por IP |
POST /api/connectors | 20 por hora por cuenta |
POST /api/connectors/:id/test | 30 por hora por cuenta |
POST /api/me/connections | 20 por hora por cuenta |
POST /api/me/connections/:id/reconnect | 20 por hora por cuenta |
POST /api/me/connections/:id/use | 30 por hora por cuenta |
DELETE /api/me | 5 por hora por cuenta |
POST /api/me/keys | 10 por hora por cuenta |
POST /api/contacts/lookup | 30 por hora |
POST /api/users/:id/block | 60 por hora por cuenta |
POST /api/users/:id/report | 5 por día |
Errores
Siempre { "error": "una frase para mostrar", "code"?: "estable" }. Decidí por el status y, cuando viene, por code; nunca por el texto de error, que puede cambiar.
| Status | code | Cuándo | Qué hacer |
|---|---|---|---|
| 400 | — | El pedido está mal (falta un campo, un valor fuera de rango). error dice qué, en castellano. | No reintentes igual. |
| 401 | no_session | La credencial no vale (vencida, revocada o mal escrita). | Pedí una nueva; un 401 sin este code no es «se cerró la sesión». |
| 403 | — | No tenés permiso: un bot en una ruta solo para personas, alguien que no es admin, un grupo donde solo escriben los admins, un bloqueo. | No reintentes. |
| 403 | guest | Una cuenta sin nada verificado (un invitado) intentó algo que lo pide: invitar por email, buscar gente o cruzar la agenda, compartir un contacto de la agenda, crear o copiar un bot, una organización o un perfil de desarrollador, pedir acceso a un bot, conectar un servicio, guardar una clave o reenviar a un email. | Verificá un email, un teléfono, Google o Apple. El texto de error dice para qué hace falta. |
| 403 | not_clonable | «Hacer mi copia» de un bot que no se puede copiar. | — |
| 404 | — | No existe, o no lo podés ver (un chat del que no sos miembro, un contacto que no te conoce: la respuesta es la misma a propósito). | — |
| 409 | — | Choca con algo: ir al catálogo sin el perfil de desarrollador completo (missing), una encuesta cerrada, un teléfono de otra cuenta. | Leé missing si viene. |
| 400 | alias_format | Alias de 2 a 20 letras, números, - o _, sin @. | Corregilo. |
| 400 | alias_reserved | ai, ia y todos están reservados. | Elegí otro. |
| 400 | alias_taken | Ya hay alguien con ese alias en el grupo. | Elegí otro. |
| 400 | clone_tools | Un bot que usa un servicio con la cuenta de quien lo hizo no se puede ofrecer para copiar. | — |
| 400 | clone_external | Un bot externo no se puede copiar. | — |
| 400 | clone_following | Una copia que sigue a su original no se puede ofrecer para copiar. | — |
| 400 | idea_needs_catalog | Una «idea» para copiar necesita el bot en el catálogo y copiable. | — |
| 400 | cap | Un tope de gasto del bot. | — |
| 429 | — | Pasaste un límite (ver Límites). | Esperá lo que dice Retry-After (segundos) si viene. |
| 429 | guest_limit | Demasiadas cuentas nuevas desde esta conexión. | Esperá. |
| 500 | — | «Error interno»: un error nuestro; el detalle queda en el log. | Reintentá con el mismo clientId si estabas enviando. |
| 502 | — | O 503, 504: el server se está actualizando o no llegó. | Reintentá con backoff (y el mismo clientId). |
Lo que dice /api/config
GET /api/config es público y dice qué tiene prendido este server. Una función que depende de una bandera no existe sin ella: preguntá antes de ofrecerla.
| Campo | Qué dice |
|---|---|
providers | Por proveedor (anthropic, openai): label, models, tiers ({ modelo: 1 | 2 | 3 }), prices, defaultModel, freeModel, freeAvailable. |
newBotProvider | El proveedor con que nace una IA nueva. |
freeMessagesPerDay | Mensajes gratis por día y por persona (freeMessagesPerUser es el nombre viejo). |
freeMessagesPerUser | Igual que freeMessagesPerDay (la app nativa lo sigue leyendo). |
publicUrl | De dónde salen los links. |
transcribeAudio | Los audios llegan transcriptos (attachments[].transcript, por message:updated). |
googleClientId | Entrar con Google (de la app). |
appleClientId | Entrar con Apple (de la app). |
appleWebClientId | Entrar con Apple en la web. |
androidAppUrl | La app en Play. |
minAppVersion | La versión mínima de la app nativa por plataforma. |
emailEnabled | El server manda emails (códigos, invitaciones). |
smsEnabled | Códigos por teléfono. |
smsChannels | Por dónde: sms, whatsapp. |
mcpEnabled | Bots con herramientas (MCP). |
connectors | Conectores y cuentas (ver). |
chatLists | Favoritos y listas de chats. |
notifyPrefs | Interruptores de notificaciones. |
reactions | Reacciones (un mensaje sin reactions se lee como {}). |
linkPreviews | Fichas de links (message.link). |
searchEnabled | Buscar mensajes (GET /api/search y GET /api/groups/:id/search). |
campaignSkipEscape | De la web: no mostrar la pantalla de salida en una visita de campaña. |
campaignAutoEscape | De la web: en Android, una visita de anuncio dentro del navegador de una app sale sola a Chrome. |
ias | La sección IAs (/bots/browse, fichas, members[].mode). |
botCategories | Las categorías del catálogo (ids). |
ownerPayEnabled | Un desarrollador puede pagar las respuestas de su bot con su clave. |
vapidPublicKey | Web Push (de la web). |
socketHosts | Los hosts del socket (mejor GET /api/sockets). |
assistantBotId | Qué bot es «Puentes». |
places | Avisos por lugar (de la app). |
La API y esta página están en castellano. En inglés: todavía no (lo decide Mauro).
¿Dudas o ideas? Escribile a Claude en Puentes 😉.