Ce qu’est un bot et ce qu’il sait faire
Un bot est une autre forme de « compte utilisateur » dans Korat, sans personne derrière l’écran pour taper — c’est votre propre programme qui envoie une requête, et le message apparaît dans la conversation où le bot a été invité. En pratique :
- Annoncer une nouvelle commande dans une conversation — votre système de vente enregistre une commande ⇒ le bot l’annonce aussitôt dans la conversation avec le client ou dans le salon de l’équipe
- Alerter sur un stock bas — le stock descend sous le seuil défini ⇒ le bot prévient dans le salon de l’équipe
- Rapporter l’état d’un serveur ou d’un système — votre script de contrôle finit son passage ⇒ le bot publie le résultat dans le salon que surveille l’équipe technique
Ce qu’un bot ne peut pas faire (constaté dans la base de données, ce ne sont pas des limites provisoires) :
- Un bot ne lit pas les messages d’un salon — il est en sortie seulement, sauf si vous configurez vous-même un webhook entrant (voir la référence technique en fin de page) ; et même avec un webhook configuré, rien n’est réellement envoyé aujourd’hui, car l’horloge qui doit vider la file n’est pas encore en place en production
- Un bot ne s’invite pas lui-même — c’est toujours quelqu’un déjà présent dans le salon (ou qui a le droit de le gérer) qui l’invite
- Un bot ne peut détenir plus de 2 jetons valides à la fois, et un compte ne peut créer que 5 bots au maximum
Par où commencer — 4 étapes
- 1Créez le bot : donnez-lui un nom et un nom d’utilisateur (celui-ci doit se terminer par
bot, par exempleorderbot) — pour que le bot appartienne au commerce et non à vous seul, choisissez le commerce dès la création - 2Copiez le jeton : il ne s’affiche qu’une seule fois, juste après la création — copiez-le immédiatement dans un endroit sûr. Une fois l’écran fermé, sa valeur réelle est définitivement irrécupérable (il faut en émettre un nouveau)
- 3Invitez le bot dans un salon : conversation, salon de groupe ou salon d’équipe où il doit parler — vous devez déjà y être présent (ou avoir le droit de le gérer) pour pouvoir l’inviter
- 4Envoyez le premier message : reprenez le jeton de l’étape 2 et appelez l’API bot — voyez l’exemple reproductible tel quel dans la section suivante
Les étapes 1 à 3 se font dans l’app Korat
Le menu se trouve dans Réglages › Compte › « Mes bots » — on y crée un bot, on émet, fait tourner ou révoque un jeton, et on invite le bot dans une conversation, un salon de groupe ou un salon d’équipe, sans écrire une ligne de code. Ce menu arrivera avec la prochaine version de l’app Korat — la version téléchargeable aujourd’hui ne l’a pas encore. Si vous ne voyez pas « Mes bots » dans les réglages, attendez la mise à jour ; en attendant, les étapes 1 à 3 se font tout aussi bien par les RPC Supabase (voir la référence technique en fin de page).
🔒 Le jeton est le mot de passe du bot
Quiconque détient ce jeton peut envoyer des messages au nom du bot — ne transmettez jamais un jeton par messagerie, en image ou par e-mail non chiffré : gardez-le sur votre serveur ou dans votre coffre à secrets. Aucun membre de l’équipe Korat ne vous demandera jamais votre jeton, ni par messagerie, ni par e-mail, ni par téléphone — si quelqu’un s’en réclame et vous le demande, considérez qu’il s’agit d’une escroquerie et signalez-le via la page Contact. Si un jeton a fuité, révoquez-le immédiatement dans l’app (c’est irréversible : il faut en émettre un nouveau).
Exemple — envoyer le premier message
Il n’existe qu’un seul point d’entrée : POST https://koratland.com/api/bot/send. Mettez le jeton dans l’en-tête, puis indiquez quoi dire et dans quel salon :
En curl (pour qui est à l’aise avec le 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": "Une nouvelle commande vient d arriver" }'
Remplacez kbot_12_xxxxxxxxxxxxxxxxxxxx par le jeton réel du bot, et 9931 par l’identifiant du salon où il a été invité (visible dans le lien de la conversation dans l’app, ou dans la liste des salons de l’écran « Mes bots »).
Sans écrire de code (Google Apps Script)
Sans serveur à vous, Google Apps Script suffit et reste gratuit avec un simple compte Google — allez sur script.google.com → nouveau projet → collez ce code → touchez « Exécuter ». Le premier lancement demande l’autorisation d’accéder à internet (vous pouvez accepter sans crainte : c’est votre propre script) :
function sendKoratMessage() {
var url = "https://koratland.com/api/bot/send";
var payload = {
kind: "chat",
chat_id: 9931, // remplacez par l id de votre salon
text: "Une nouvelle commande vient d arriver" // remplacez par votre message
};
var options = {
method: "post",
contentType: "application/json",
headers: { "X-Bot-Token": "kbot_12_xxxxxxxxxxxxxxxxxxxx" }, // remplacez par le jeton de votre bot
payload: JSON.stringify(payload)
};
var res = UrlFetchApp.fetch(url, options);
Logger.log(res.getContentText()); // le resultat s affiche dans le journal d execution
}
Un seul appui sur « Exécuter » suffit pour tester — pour un envoi automatique, par exemple à chaque nouvelle ligne dans la feuille Google qui enregistre vos commandes, utilisez les déclencheurs (Triggers) d’Apps Script pour appeler cette fonction à la modification de la feuille (onEdit) ou toutes les N minutes.
La même méthode fonctionne avec tout outil d’automatisation disposant d’une action « HTTP request » ou « Webhook » — n8n, Make ou Zapier : réglez un POST vers la même URL, ajoutez l’en-tête X-Bot-Token et le même corps JSON que dans l’exemple ci-dessus.
Problèmes fréquents
Ce que vous voyez (reason) | Ce que cela signifie | Comment le corriger |
|---|---|---|
not_invited | Le bot n’a pas été invité dans ce salon (ou il en a été exclu) | Réinvitez-le depuis l’écran « Mes bots » ou avec bot_invite |
invite_stale | La personne qui avait invité le bot n’a plus les droits dans ce salon (départ du groupe, rétrogradation, retrait du droit de gestion du salon d’équipe, salon archivé) ⇒ le bot se tait de lui-même, sans que personne ne l’ait exclu | Faites réinviter le bot par quelqu’un qui a réellement les droits dans ce salon — les droits sont toujours calculés d’après la dernière personne à avoir invité |
conversation_not_open | Ce salon est la boîte de réception d’un commerce et le client n’a jamais écrit le premier — le commerce (bot compris) ne peut pas engager la conversation | Attendez que le client écrive, puis laissez le bot répondre |
rate_limited | Le bot envoie trop vite par rapport au plafond (par minute ou par jour) | Attendez la durée indiquée dans retry_after_sec avant de réessayer — s’il vous faut un plafond plus élevé, écrivez à l’équipe via Contact |
blocked | Un utilisateur de ce salon a bloqué ce bot | Rien à corriger côté utilisateur — utilisez un autre salon ou un autre bot si nécessaire |
| Le bot parlait, puis s’est tu sans raison apparente | La cause la plus fréquente est invite_stale ci-dessus — le bot n’est pas en panne | Vérifiez que la personne qui l’a invité est toujours dans le salon et y a toujours ses droits |
La liste complète des codes d’erreur figure dans le tableau de référence technique plus bas.
Référence technique (pour les développeurs)
Ce qui suit s’adresse à qui va écrire un programme appelant l’API bot — tous les paramètres, le tableau complet des codes d’erreur, et le webhook entrant.
Faire les étapes 1 à 3 directement par RPC (sans l’app)
Si votre version de l’app n’a pas encore l’écran « Mes bots », ou pour automatiser, appelez directement les RPC Supabase via PostgREST avec le jeton d’accès de l’utilisateur connecté (et non le jeton du bot) — en trois étapes :
1. Créer le 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 notification",
"p_username": "notifybot",
"p_business_id": null,
"p_about": "Annonce l etat des commandes"
}'
p_username doit être composé de a–z 0–9 _, faire 4 à 31 caractères et se terminer par bot (expression régulière ^[a-z][a-z0-9_]{2,29}bot$). Envoyez p_business_id pour que le bot appartienne à un commerce — l’appelant doit alors être membre de ce commerce et détenir le droit bot.manage. La réponse contient token, la seule et unique apparition de sa valeur réelle : la base ne conserve qu’une empreinte sha256 — manqué, il faut en émettre un nouveau (bot_rotate_token), la valeur d’origine est irrécupérable.
{ "ok": true, "bot_id": 12, "user_id": 4821, "username": "notifybot", "token": "kbot_12_…" }
2. Inviter le bot dans une conversation — bot_invite
Vous devez être réellement présent dans ce salon (ou faire partie de l’équipe du commerce dont c’est la boîte de réception) et pouvoir gérer ce bot (can_manage_bot). p_target est un 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 }'
2 bis. Inviter dans un salon de groupe ou d’équipe — des droits plus stricts
Envoyez p_kind: "community_room" ou "business_room" avec un p_target valant community_rooms.id ou business_rooms.id : cela fonctionne réellement — mais l’invitant doit franchir la barrière de droits qui correspond au sens de ce salon, pas seulement en être membre :
- Salon de groupe — l’invitant doit pouvoir réellement y écrire selon les règles du salon (
read_role/post_role) : dans un salon de discussion ordinaire, un membre suffit ; dans un salon d’annonces ou de coulisses réservé aux administrateurs, il faut être propriétaire ou administrateur du groupe - Salon d’équipe d’un commerce — il faut franchir toutes les barrières habituelles du salon (droit de lecture, droit de publication, règles de destinataires) plus la capacité
teamroom.manage, toujours : amener un bot dans un salon interne d’entreprise relève de la « gestion du salon », pas seulement du fait d’y écrire
Dans les deux cas, il faut aussi être gestionnaire de ce bot (can_manage_bot) — un salon collectif n’est pas un endroit où l’on traîne le bot de quelqu’un d’autre pour le faire parler.
🔴 invite_stale — la chose la plus importante à savoir avant de commencer
Les droits de l’invitant ne sont pas vérifiés seulement au moment de l’invitation — bot_send réinterroge les règles du salon à chaque envoi, au nom de ce même invitant ⇒ dès que l’invitant quitte le groupe, est rétrogradé, perd teamroom.manage, ou que le salon est archivé, le message suivant du bot reçoit immédiatement reason: "invite_stale" — le bot se tait de lui-même, sans que personne ne l’ait exclu. Aucune tâche de fond, aucun déclencheur à faire tourner : c’est un pur recalcul de droits.
Envoyer un message — POST /api/bot/send
Deux façons de transmettre le jeton, au choix :
# Méthode 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": "Votre colis vient de partir" }'
# Méthode 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": "Votre colis vient de partir" }'
Sans jeton du tout, la réponse est 401 :
{ "ok": false, "reason": "bad_token", "detail": "le jeton doit etre transmis via Authorization: Bearer … ou X-Bot-Token, rien d autre" }
Champs du corps de la requête
| Champ | Type | Obligatoire | Signification |
|---|---|---|---|
kind | string | non (défaut "chat") | Détermine le type de destination — trois valeurs utilisables : "chat" · "community_room" · "business_room". Toute autre valeur renvoie reason: "bad_target" directement depuis la base (aucun repli silencieux vers "chat" — l’ancien bug qui figeait cette valeur est corrigé) |
chat_id | integer > 0 | oui (ou target_id) | L’id du salon de destination où le bot a été invité — chats.id avec kind: "chat", community_rooms.id avec "community_room", business_rooms.id avec "business_room" (le nom du champ reste toujours chat_id ou target_id, il n’y a pas de nom distinct selon le kind) |
text | string | oui | Le message, jamais vide, dans la limite fixée par le registre text_max_message (ce plafond est ajustable, ce n’est pas un nombre figé — lisez la valeur max dans l’erreur too_long) |
client_key | string | non | Clé anti-doublon, alternative à l’en-tête Idempotency-Key — de 8 à 128 caractères, uniquement A–Z a–z 0–9 _ . : - |
Conversation privée ou boîte de réception d’un commerce — quel champ tranche
kind détermine le type de table de destination (conversation / salon de groupe / salon d’équipe). En revanche, la conversation privée et la boîte de réception d’un commerce ne se distinguent pas par kind — ce sont deux lignes de la même table chats, qui diffèrent par la présence ou non d’une valeur dans chats.business_id (valeur présente = boîte de réception d’un commerce). La vérification des droits d’invitation, le contrôle des blocages entre utilisateurs et la règle « un commerce n’écrit pas le premier » (conversation_not_open) se lisent également dans cette colonne — le chat_id transmis doit donc déjà désigner le bon salon : l’API n’offre aucun paramètre « privé / professionnel » à choisir.
Idempotency-Key et plafonds de débit
Transmettez l’en-tête standard Idempotency-Key ou le champ client_key dans le corps — l’un ou l’autre ; si vous envoyez les deux, les valeurs doivent coïncider, sinon vous obtenez 422 idempotency_key_conflict. Rejouer la même requête avec la même clé (même salon + même clé) renvoie la même réponse 200 assortie de "duplicate": true, sans créer de second message.
Le plafond a deux étages : par IP (120 requêtes/minute, au niveau du Worker lui-même) et par bot (par défaut 20/minute et 1000/jour pour un bot donné, ajustable au cas par cas par un administrateur). Les deux répondent 429 avec l’en-tête Retry-After en secondes (par IP = toujours 60 · par bot et par minute = 60 · par bot et par jour = 3600).
Recevoir les messages entrants — le webhook
Quand quelqu’un écrit dans une conversation où le bot a été invité (et que l’auteur n’est pas le compte du bot lui-même — de quoi éviter que ses propres messages lui reviennent), Korat envoie un POST à l’URL configurée. La configuration passe par un RPC, avec le compte utilisateur connecté ayant le droit de gérer ce bot (can_manage_bot), comme pour l’obtention du jeton :
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": "ce secret n apparait qu une fois — conservez-le pour verifier la signature" }
L’URL doit être en https:// et ne peut pas désigner un hôte interne, de bouclage ou lien-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, y compris les points de métadonnées de tous les fournisseurs cloud) — le refus intervient dès la configuration, puis de nouveau à chaque envoi par une résolution DNS réelle, au cas où le DNS pointerait ailleurs par la suite. Le secret n’apparaît qu’une fois, à la configuration ou à la rotation — bot_rotate_webhook_secret(p_bot) le fait tourner à tout moment, bot_disable_webhook(p_bot) et bot_enable_webhook(p_bot) coupent et rétablissent, bot_webhook_status(p_bot) montre l’état (sans jamais renvoyer le secret).
Rien ne part réellement aujourd’hui
La file d’envoi (bot_webhook_deliveries) fonctionne correctement et c’est démontré dans la base ; le secret de la voie d’envoi (BOT_WEBHOOK_DISPATCH_SECRET) est bien en place. Mais ce qui doit vider la file (le pg_cron qui appelle le Worker chaque minute) n’est pas encore configuré en production — vous pouvez donc configurer un webhook, les messages s’empileront dans la file, mais votre serveur ne recevra jamais rien tant que l’horloge pg_cron ne sera pas en place.
La requête que votre serveur recevra
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": "Vous l avez en rouge ?",
"time": "14:02",
"sender": { "id": 4821, "username": "somchai_p", "name": "Somchai P." }
}
C’est tout — aucun numéro de téléphone, aucune adresse e-mail, aucun jeton de qui que ce soit dans le corps. Le bot ne voit que le salon, le message et l’identité de l’auteur, dans la stricte mesure nécessaire pour répondre.
Vérifiez la signature avant de faire confiance à la requête
X-Korat-Signature vaut HMAC-SHA256(secret, "<timestamp>." + rawBody) encodé en hexadécimal — exemple 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-rejeu : 5 minutes maximum
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(`${timestampHeader}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
Une réponse 2xx en moins de 8 secondes vaut succès — toute autre réponse (y compris une redirection 3xx, que Korat ne suit pas) ou l’absence de réponse compte comme un échec et repart dans la file.
Nouvelles tentatives quand votre serveur est tombé
En cas d’échec, le délai double à chaque fois (1, 2, 4, 8, 16, 32, 64, 128 minutes), jusqu’à 8 tentatives par message, après quoi ce message est abandonné. Si votre serveur échoue 15 fois d’affilée (tous messages confondus), le webhook est désactivé automatiquement et le propriétaire du bot est prévenu dans l’app — rien ne sera jamais envoyé indéfiniment vers un serveur mort. Appelez bot_enable_webhook pour le réactiver une fois le problème réglé chez vous.
Tableau complet des codes d’erreur
Toute réponse en échec contient toujours {"ok": false, "reason": "…"} — c’est reason qui doit guider votre décision, pas seulement le code HTTP.
| HTTP | reason | Origine | Signification / correction |
|---|---|---|---|
| 401 | bad_token | Worker / BD | Jeton absent, erroné, révoqué, ou bot désactivé — la même réponse pour tous ces cas est volontaire, afin d’empêcher de deviner par élimination |
| 403 | not_invited | BD | Le bot n’a jamais été invité dans ce salon, ou en a été exclu — appelez d’abord bot_invite |
| 403 | blocked | BD | Un utilisateur du salon a bloqué ce bot (hors boîte de réception d’un commerce) |
| 403 | account_suspended | BD | Un des deux côtés du salon a son compte suspendu — c’est une question de droits, pas une requête malformée |
| 404 | no_such_chat | BD | Ce chat_id n’existe pas |
| 404 | no_such_endpoint | Worker | Mauvaise route : il n’existe que POST /api/bot/send |
| 405 | method_not_allowed | Worker | Une méthode autre que POST a été utilisée |
| 409 | conversation_not_open | BD | Ce salon est la boîte de réception d’un commerce et le client n’a jamais écrit — le commerce (bot compris) ne peut pas engager la conversation |
| 409 | chat_closed | BD | Cette conversation de rencontre (dating) n’accepte plus de messages |
| 422 | empty_text | BD | text est vide une fois les espaces retirés |
| 422 | too_long | BD | Message au-delà du plafond — voyez la valeur max jointe |
| 422 | bad_target | BD | kind n’est pas l’un de "chat" / "community_room" / "business_room" |
| 404 | no_such_room | BD | Aucun salon de groupe ou d’équipe ne correspond à ce chat_id (le salon a été supprimé) |
| 409 | room_archived | BD | Ce salon est archivé — ni les humains ni les bots n’y écrivent plus |
| 403 | invite_stale | BD | Les droits de l’invitant ont été recalculés et ne passent plus — départ du groupe, rétrogradation, retrait de teamroom.manage, salon archivé. Voyez detail pour le motif précis (not_a_member / room_role / not_teamroom_manager) — la correction consiste à faire réinviter le bot par quelqu’un qui a encore les droits (bot_invite), pas à attendre |
| 422 | bad_json | Worker | Le corps de la requête n’est pas du JSON valide |
| 422 | bad_chat_id | Worker | chat_id / target_id doit être un entier positif |
| 422 | bad_text | Worker | text doit être une chaîne |
| 422 | idempotency_key_conflict | Worker | Idempotency-Key et client_key ont été envoyés ensemble avec des valeurs différentes — n’en envoyez qu’un |
| 422 | bad_client_key | Worker | La clé anti-doublon ne respecte pas le format (8–128 caractères, A–Z a–z 0–9 _ . : -) |
| 429 | rate_limited | Worker / BD | Plafond dépassé — regardez l’en-tête Retry-After et les champs window / retry_after_sec |
| 502 | database_error | Worker | La base a répondu autre chose qu’un 2xx — voyez pg_code et detail joints : c’est le message réel de Postgres |
| 503 | service_key_not_configured | Worker | La clé de service n’est pas configurée côté serveur — c’est un problème chez Korat, pas chez l’appelant |
| 504 | database_unreachable | Worker | Appel à la base impossible ou expiré — regardez safe_to_retry : rejouer n’est sûr que si vous avez transmis client_key ou Idempotency-Key |
La réponse en cas de succès
{ "ok": true, "duplicate": false, "message_id": 88213 }
duplicate: true signifie que cette requête avait déjà été envoyée (anti-doublon par client_key ou Idempotency-Key) — aucun nouveau message n’a été créé, mais c’est bien un succès (200), puisque le résultat attendu par l’appelant existe réellement.