Qué es un bot y para qué sirve
Un bot es otro tipo de "cuenta de usuario" dentro de Korat, sin nadie escribiendo detrás de la pantalla: tu propio programa manda una petición y el mensaje aparece en la conversación a la que invitaron al bot. Usos reales:
- Avisar de un pedido nuevo en el chat — tu sistema de ventas recibe un pedido nuevo ⇒ el bot lo anuncia al instante en el chat con el cliente o en la sala del equipo
- Avisar de existencias bajas — el inventario baja del mínimo que fijaste ⇒ el bot avisa en la sala del equipo
- Avisar del estado del servidor o del sistema — tu propio script de monitoreo termina ⇒ el bot reporta el resultado en la sala que vigila el equipo de sistemas
Qué no puede hacer un bot (datos reales de la base de datos, no limitaciones temporales):
- Un bot no puede leer los mensajes de la sala — es solo de salida, salvo que configures tú un webhook de entrada (mira la referencia técnica al final de la página). Y aunque lo configures, hoy todavía no se envía nada de verdad, porque el reloj que tiene que sacar la cola y disparar no está configurado en producción.
- Un bot no puede invitarse solo a una sala — siempre tiene que invitarlo alguien que ya esté en esa sala (o que tenga permiso para administrarla)
- Un bot puede tener como máximo 2 tokens válidos a la vez, y una cuenta puede crear hasta 5 bots.
Cómo empezar — 4 pasos
- 1Crea el bot Ponle nombre y nombre de usuario (el nombre de usuario tiene que terminar en
bot, por ejemploorderbot) — si quieres que el bot sea del negocio (y no tuyo nada más), elige el negocio al crearlo - 2Copia el token El sistema te enseña el token una sola vez al terminar de crearlo — cópialo y guárdalo en un lugar seguro de inmediato. Una vez que cierres la pantalla, no hay forma de volver a ver el valor real (hay que emitir uno nuevo).
- 3Invita al bot a una sala Elige el chat, la sala de grupo o la sala del equipo donde quieres que hable — tienes que estar tú ya en esa sala (o tener permiso para administrarla) para poder invitarlo
- 4Manda el primer mensaje Toma el token del paso 2 y llama a la Bot API — en la siguiente sección hay ejemplos que puedes seguir tal cual
Los pasos 1 a 3 se hacen en la app de Korat
El menú está en Configuración › Cuenta › "Mis bots" — ahí se crea el bot, se emiten, rotan y revocan los tokens, y se invita al bot a chats, salas de grupo y salas de equipo, sin escribir una sola línea de código. Ese menú llega con la siguiente versión de la app de Korat — la versión que se descarga hoy todavía no lo tiene. Si abres la configuración y no ves "Mis bots", espera la actualización; mientras tanto, los pasos 1 a 3 se pueden hacer llamando directamente a las RPC de Supabase (mira la referencia técnica al final de la página).
🔒 El token es la contraseña del bot
Cualquiera que tenga ese token puede mandar mensajes en nombre del bot de inmediato — nunca mandes el token por chat, en una imagen ni por correo sin cifrar. Guárdalo solo en tu servidor o en tu gestor de secretos. Nadie del equipo de Korat te va a pedir el token, ni por chat, ni por correo, ni por teléfono — si alguien dice ser del equipo de Korat y te lo pide, da por hecho que es un fraude y avísanos desde Contacto de inmediato. Si el token ya se filtró, revócalo en la app al momento (no se puede deshacer: hay que emitir uno nuevo).
Ejemplo — mandar el primer mensaje
El único endpoint es POST https://koratland.com/api/bot/send. Pon el token en la cabecera y di qué vas a decir y en qué sala:
Con curl (para quien está cómodo en la terminal)
curl -X POST "https://koratland.com/api/bot/send" \ -H "X-Bot-Token: kbot_12_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "kind": "chat", "chat_id": 9931, "text": "¡Entró un pedido nuevo!" }'
Cambia kbot_12_xxxxxxxxxxxxxxxxxxxx por el token real de tu bot, y 9931 por el número de la sala a la que invitaste al bot (lo ves en el enlace del chat dentro de la app, o en la lista de salas de la pantalla "Mis bots")
Sin escribir código (Google Apps Script)
Si no tienes servidor propio, puedes usar Google Apps Script gratis con una cuenta de Google — entra a script.google.com → proyecto nuevo → pega este código → pulsa "Ejecutar"; la primera vez te pedirá permiso para salir a internet (puedes concederlo, es seguro: es tu propio script):
function sendKoratMessage() { var url = "https://koratland.com/api/bot/send"; var payload = { kind: "chat", chat_id: 9931, // cámbialo por el número de tu sala text: "¡Entró un pedido nuevo!" // cámbialo por el mensaje que quieras }; var options = { method: "post", contentType: "application/json", headers: { "X-Bot-Token": "kbot_12_xxxxxxxxxxxxxxxxxxxx" }, // cámbialo por el token de tu bot payload: JSON.stringify(payload) }; var res = UrlFetchApp.fetch(url, options); Logger.log(res.getContentText()); // mira el resultado en el registro de ejecución }
Con pulsar "Ejecutar" una vez ya puedes probarlo. Si quieres que se dispare solo —por ejemplo cada vez que se agrega una fila nueva a una hoja de Google donde guardas los pedidos— usa los activadores (Triggers) de Apps Script para llamar esta función cuando la hoja se edite (onEdit) o cada cierto número de minutos.
El mismo método sirve con cualquier herramienta de automatización que ya tenga una acción de "HTTP request" o "Webhook", por ejemplo n8n, Make o Zapier — configúrala como POST a la misma URL, con la cabecera X-Bot-Token y el mismo cuerpo JSON del ejemplo de arriba.
Problemas frecuentes
Esto es lo que ves (reason) | Qué significa | Cómo se arregla |
|---|---|---|
not_invited | El bot todavía no fue invitado a esta sala (o lo invitaron y después lo sacaron) | Invítalo otra vez a esa sala desde la pantalla "Mis bots" o con bot_invite |
invite_stale | Quien invitó al bot a esta sala ya perdió sus permisos ahí (salió del grupo, lo bajaron de rol, le quitaron la administración de la sala del equipo, o la sala se cerró) ⇒ por eso el bot se calló solo, sin que nadie lo sacara | Que lo invite de nuevo alguien que sí siga teniendo permisos reales en esa sala — los permisos siempre se calculan a partir de quien invitó por última vez |
conversation_not_open | Esta sala es la bandeja de un negocio y el cliente todavía no ha escrito — el negocio (y también su bot) no puede escribirle primero al cliente | Espera a que el cliente escriba y después deja que el bot conteste |
rate_limited | El bot manda mensajes más seguido que el tope configurado (por minuto o por día) | Espera el tiempo que indica retry_after_sec y vuelve a intentarlo — si de verdad necesitas un tope más alto, escríbele al equipo desde Contacto |
blocked | La persona de esa conversación tiene bloqueado a este bot | No se puede arreglar del lado del usuario — usa otra sala u otro bot si es indispensable |
| El bot hablaba y de pronto se quedó callado | La causa más frecuente es invite_stale, de arriba — no es que el bot esté roto | Revisa si quien invitó al bot a esa sala sigue en ella y sigue teniendo permisos |
La tabla completa de códigos de error está en la referencia técnica de más abajo.
Referencia técnica (para desarrolladores)
Lo que sigue está escrito para quien va a programar contra la Bot API: el detalle de cada parámetro, la tabla completa de códigos de error y el webhook de entrada.
Hacer los pasos 1 a 3 con RPC directas (sin la app)
Si tu versión de la app todavía no tiene la pantalla "Mis bots", o quieres automatizarlo, llama directamente a las RPC de Supabase con el access token del usuario que tiene la sesión abierta (no con el token del bot), a través de PostgREST — son tres pasos:
1. Crear el bot — bot_create
curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_create" \ -H "apikey: <SUPABASE_ANON_KEY>" \ -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \ -H "Content-Type: application/json" \ -d '{ "p_name": "Bot de avisos", "p_username": "notifybot", "p_business_id": null, "p_about": "Avisa el estado de los pedidos" }'
p_username tiene que ser a–z 0–9 _, de 4 a 31 caracteres, yterminar siempre en bot (regex ^[a-z][a-z0-9_]{2,29}bot$). Manda p_business_id cuando el bot sea del negocio — quien llama tiene que ser miembro de ese negocio y tener el permiso bot.manage. La respuesta correcta devuelve token conel valor real, una única vez en la vida de ese token: la base de datos solo guarda el hash sha256, así que si lo pierdes hay que emitir uno nuevo (bot_rotate_token). No hay forma de volver a leer el valor real.
{ "ok": true, "bot_id": 12, "user_id": 4821, "username": "notifybot", "token": "kbot_12_…" }
2. Invitar al bot a un chat — bot_invite
Hay que estar de verdad en esa sala (o ser del equipo del negocio si la sala es la bandeja de ese negocio) y poder administrar ese bot (can_manage_bot) p_target). chats.id
curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_invite" \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "p_bot": 12, "p_kind": "chat", "p_target": 9931 }'
2a. Invitar al bot a una sala de grupo o de equipo — permisos más estrictos que en un chat
Mandar p_kind: "community_room" o "business_room" con p_target igual a community_rooms.id/business_rooms.id ya funciona — pero quien invita tiene que pasar el control de permisos propio del significado de esa sala, no basta con ser miembro:
- Sala de grupo — quien invita tiene quepoder escribir de verdad en esa sala según las reglas de la propia sala (
read_role/post_role). En una sala de conversación normal cualquier miembro puede invitar; en una sala de anuncios o interna reservada a administradores, solo el dueño o los administradores del grupo. - Sala del equipo del negocio — hay que pasar todos los controles normales de la sala (permiso de lectura, permiso de publicar, reglas de destinatarios) más la capacidad
teamroom.manage, siempre — meter un bot a una sala interna de la empresa cuenta como "administrar la sala", no solo como "escribir en la sala"
En los dos casos hay que ser además quien administra ese bot (can_manage_bot) — una sala compartida no es un lugar donde cualquiera pueda arrastrar el bot de otra persona a hablar.
🔴 invite_stale — lo más importante que hay que saber antes de usarlo
Los permisos de quien invita no se revisan solo en el momento de invitar — bot_send vuelve a preguntar las reglas de la sala en cada envío, en nombre de la misma persona que invitó ⇒ en cuanto esa personasale del grupo, la bajan de rol, le quitan teamroom.manage o la sala se cierra, el siguiente mensaje del bot recibe reason: "invite_stale" de inmediato — el bot se calla solo, sin que nadie lo haya sacado. No hay trabajo en segundo plano ni disparadores que haya que ejecutar: es puro recálculo de permisos.
Mandar un mensaje — POST /api/bot/send
El token se puede mandar de dos formas; elige una:
# Forma 1 — Authorization: Bearer curl -X POST "https://koratland.com/api/bot/send" \ -H "Authorization: Bearer kbot_12_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-9931-shipped" \ -d '{ "kind": "chat", "chat_id": 9931, "text": "Tu paquete ya salió de la tienda" }'
# Forma 2 — X-Bot-Token curl -X POST "https://koratland.com/api/bot/send" \ -H "X-Bot-Token: kbot_12_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "kind": "chat", "chat_id": 9931, "text": "Tu paquete ya salió de la tienda" }'
Si no mandas ningún token recibes 401:
{ "ok": false, "reason": "bad_token", "detail": "manda el token en Authorization: Bearer … o en X-Bot-Token, no de otra forma" }
Campos del cuerpo
| Campo | Tipo | Obligatorio | Qué significa |
|---|---|---|---|
kind | string | No (por defecto "chat") | Decide el tipo de destino — hay tres valores válidos de verdad: "chat" · "community_room" · "business_room". Cualquier otro devuelve reason: "bad_target" directamente de la base de datos (no hay un fallback silencioso a "chat" — el bug antiguo que dejaba este valor fijo ya está corregido) |
chat_id | integer > 0 | Sí (o target_id) | id de la sala de destino a la que ya invitaron al bot — chats.id cuando kind: "chat", community_rooms.id cuando "community_room", business_rooms.id cuando "business_room" (el nombre del campo del cuerpo sigue siendo chat_id/target_id siempre; no cambia según el kind) |
text | string | Sí | El mensaje; no puede ir vacío y no puede pasar del tope que fija el registro text_max_message (el tope es configurable, no es un número fijo — mira el valor max en el error too_long) |
client_key | string | No | Llave para evitar mensajes duplicados, alternativa a la cabecera Idempotency-Key — de 8 a 128 caracteres, y solo admite A–Z a–z 0–9 _ . : - |
Chat privado y bandeja del negocio — qué campo lo decide
kind decide eltipo de tabla de destino (chat / sala de grupo / sala de equipo del negocio). En cambio, el chat privado y la bandeja del negociono se distinguen con kind — los dos son filas de la misma tabla chats, y se diferencian en si chats.business_id tiene valor o no (con valor = bandeja del negocio). La comprobación del permiso de invitación, la comprobación de bloqueos y la regla de "el negocio no puede escribirle primero al cliente (conversation_not_open)" también se leen de esa columna — el chat_id que mandes tiene que ser ya la sala correcta; la API no tiene un parámetro aparte para elegir "privado o negocio".
Idempotency-Key y topes de frecuencia
Manda la cabecera estándar Idempotency-Key o el campo client_key en el cuerpo — uno de los dos. Si mandas los dos, los valores tienen que coincidir; si no coinciden recibes 422 idempotency_key_conflict. Repetir la misma petición con la misma llave (misma sala + misma llave) devuelve la misma respuesta 200 con "duplicate": true, sin crear un mensaje duplicado.
Hay dos capas de tope: por IP (120 peticiones por minuto, en el propio Worker) y por bot (por defecto 20 por minuto y 1000 por día por cada bot, ajustable uno por uno por un administrador). Las dos responden 429 con la cabecera Retry-After en segundos (por IP = 60 siempre · por bot y por minuto = 60 · por bot y por día = 3600).
Recibir mensajes — webhook
Cuando alguien escribe en una sala a la que ya invitaron al bot (y quien escribe no es la cuenta del propio bot, para que sus mensajes no reboten hacia él mismo), Korat manda un POST a la URL que hayas configurado. Se configura por RPC con la cuenta del usuario que tiene la sesión abierta y permiso para administrar ese bot (can_manage_bot), igual que al pedir el token:
curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_set_webhook" \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "p_bot": 12, "p_url": "https://your-server.example.com/korat-webhook" }'
{ "ok": true, "secret": "a1b2c3…", "note": "este secret se ve una sola vez — guárdalo para verificar la firma" }
La URL tiene que ser https://, obligatoriamente, y no puede ser un host interno, loopback ni link-local (localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, incluidos los endpoints de metadatos de cualquier nube) — se rechaza desde la configuración y se vuelve a comprobar resolviendo el DNS de verdad antes de cada envío, por si el DNS se apunta a otro lado después. secret solo se ve una vez, al configurarlo o al rotarlo — bot_rotate_webhook_secret(p_bot) lo rota cuando quieras, bot_disable_webhook(p_bot)/bot_enable_webhook(p_bot) lo apagan y lo encienden, y bot_webhook_status(p_bot) consulta el estado (sin devolver el secret).
Hoy todavía no sale nada de verdad
La cola de envío (bot_webhook_deliveries) funciona correctamente y está probada en la base de datos, y el secret de la ruta de envío (BOT_WEBHOOK_DISPATCH_SECRET) ya lo configuró el dueño, pero lo que tiene que sacar la cola y disparar de verdad (pg_cron, que debe llamar al Worker cada minuto) todavía no está configurado en producción — puedes dejar el webhook configurado y los mensajes se van a la cola, pero tu servidor no va a recibir nada hasta que se configure el reloj de pg_cron.
Forma de la petición que va a recibir tu servidor
POST /korat-webhook HTTP/1.1 Content-Type: application/json X-Korat-Signature: sha256=<hex> X-Korat-Timestamp: 1750000000000 { "event": "message.created", "chat_id": 9931, "message_id": 88213, "text": "¿Tienen de color rojo?", "time": "14:02", "sender": { "id": 4821, "username": "carlos_m", "name": "Carlos M." } }
Eso es todo — no hay teléfonos, ni correos, ni tokens de nadie en el cuerpo. El bot solo ve la sala, el mensaje y la identidad de quien escribe, lo justo para poder responder.
Verifica la firma antes de confiar en la petición
X-Korat-Signature es HMAC-SHA256(secret, "<timestamp>." + rawBody) en hexadecimal — ejemplo en Node.js:
const crypto = require("crypto"); function verify(rawBody, signatureHeader, timestampHeader, secret) { const age = Date.now() - Number(timestampHeader); if (!(age >= 0 && age < 5 * 60_000)) return false; // anti-replay: no más de 5 minutos de antigüedad const expected = "sha256=" + crypto .createHmac("sha256", secret) .update(`${timestampHeader}.${rawBody}`) .digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader)); }
Responder 2xx en menos de 8 segundos cuenta como entrega correcta — cualquier otra respuesta (incluida una redirección 3xx, que Korat no sigue) o no responder cuenta como fallo y entra en la cola de reintentos.
Reintentos cuando tu servidor se cae
Cada fallo ⇒ espera exponencial (1, 2, 4, 8, 16, 32, 64 y 128 minutos), con un máximo de 8 intentos por mensaje, y después se abandona ese mensaje. Si tu servidorfalla 15 veces seguidas (a lo largo de varios mensajes), el webhook seapaga solo y se le avisa en la app a quien es dueño del bot — nunca se dispara eternamente contra un destino muerto. Llama a bot_enable_webhook para volver a encenderlo cuando hayas arreglado tu servidor.
Tabla completa de códigos de error
Toda respuesta fallida trae siempre {"ok": false, "reason": "…"} — reason es la palabra con la que hay que decidir, no solo el código HTTP.
| HTTP | reason | Viene de | Qué significa / cómo se arregla |
|---|---|---|---|
| 401 | bad_token | Worker / DB | No mandaste el token, o el token es incorrecto, está revocado o el bot está apagado — se responde lo mismo en todos los casos a propósito, para que no se pueda adivinar |
| 403 | not_invited | DB | El bot nunca fue invitado a esta sala, o ya lo sacaron — llama antes a bot_invite |
| 403 | blocked | DB | La persona de la sala tiene bloqueado a este bot (solo en salas que no son la bandeja de un negocio) |
| 403 | account_suspended | DB | Una de las dos partes de la sala tiene la cuenta suspendida — es un tema de permisos, no una petición mal formada |
| 404 | no_such_chat | DB | chat_id no existe |
| 404 | no_such_endpoint | Worker | Ruta equivocada; solo existe POST /api/bot/send |
| 405 | method_not_allowed | Worker | Se usó un método distinto de POST |
| 409 | conversation_not_open | DB | Esta sala es la bandeja de un negocio y el cliente todavía no ha escrito — el negocio (y su bot) no puede escribir primero |
| 409 | chat_closed | DB | La sala de citas (dating) ya no acepta mensajes |
| 422 | empty_text | DB | text queda vacío después de quitar los espacios |
| 422 | too_long | DB | El mensaje pasa del tope — mira el valor max que viene adjunto |
| 422 | bad_target | DB | kind no es uno de "chat"/"community_room"/"business_room" |
| 404 | no_such_room | DB | chat_id no corresponde a ninguna sala de grupo ni de equipo existente (la sala fue borrada) |
| 409 | room_archived | DB | Esta sala está cerrada — no puede escribir ni una persona ni un bot |
| 403 | invite_stale | DB | Los permisos de quien invitóse recalcularon y ya no pasan — esa persona salió del grupo, la bajaron de rol, le quitaron teamroom.manageo la sala se cerró. Mira detail para el motivo concreto (not_a_member/room_role/not_teamroom_manager) — se arregla haciendo que alguien que sí tenga permisos vuelva a invitar al bot a esa sala (bot_invite), no esperando |
| 422 | bad_json | Worker | El cuerpo de la petición no es JSON válido |
| 422 | bad_chat_id | Worker | chat_id/target_id tiene que ser un entero positivo |
| 422 | bad_text | Worker | text tiene que ser una cadena |
| 422 | idempotency_key_conflict | Worker | Mandaste Idempotency-Key y client_key al mismo tiempo con valores distintos — con uno basta |
| 422 | bad_client_key | Worker | La llave anti-duplicados no cumple el formato (de 8 a 128 caracteres, A–Z a–z 0–9 _ . : -) |
| 429 | rate_limited | Worker / DB | Se pasó del tope — revisa la cabecera Retry-After y los campos window/retry_after_sec |
| 502 | database_error | Worker | La base de datos respondió, pero no con 2xx — mira pg_code/detail adjunto: es el mensaje real de Postgres |
| 503 | service_key_not_configured | Worker | El servidor todavía no tiene configurada la service key — es un problema del lado de Korat, no de quien llama |
| 504 | database_unreachable | Worker | La llamada a la base de datos falló o se agotó el tiempo — mira safe_to_retry: solo es seguro reintentar si mandaste client_key/Idempotency-Key también |
Respuesta cuando todo sale bien
{ "ok": true, "duplicate": false, "message_id": 88213 }
duplicate: true quiere decir que esta petición ya se había enviado antes (evitada con client_key/Idempotency-Key) — no se creó un mensaje nuevo, pero sigue contando como éxito (200) porque el resultado que quería quien llamó ya ocurrió de verdad.