Korat

Créer un bot qui répond aux clients

Cette page commence par ce qu’un commerçant qui n’écrit pas de code peut suivre pas à pas, puis descend vers le détail technique destiné aux développeurs en fin de page — pour aller droit au but, servez-vous du sommaire à droite.

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

  1. 1Créez le bot : donnez-lui un nom et un nom d’utilisateur (celui-ci doit se terminer par bot, par exemple orderbot) — pour que le bot appartienne au commerce et non à vous seul, choisissez le commerce dès la création
  2. 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)
  3. 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
  4. 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 signifieComment le corriger
not_invitedLe 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_staleLa 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 excluFaites 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_openCe 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 conversationAttendez que le client écrive, puis laissez le bot répondre
rate_limitedLe 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
blockedUn utilisateur de ce salon a bloqué ce botRien à corriger côté utilisateur — utilisez un autre salon ou un autre bot si nécessaire
Le bot parlait, puis s’est tu sans raison apparenteLa cause la plus fréquente est invite_stale ci-dessus — le bot n’est pas en panneVé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’invitationbot_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

ChampTypeObligatoireSignification
kindstringnon (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_idinteger > 0oui (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)
textstringouiLe 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_keystringnonClé 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.

HTTPreasonOrigineSignification / correction
401bad_tokenWorker / BDJeton 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
403not_invitedBDLe bot n’a jamais été invité dans ce salon, ou en a été exclu — appelez d’abord bot_invite
403blockedBDUn utilisateur du salon a bloqué ce bot (hors boîte de réception d’un commerce)
403account_suspendedBDUn des deux côtés du salon a son compte suspendu — c’est une question de droits, pas une requête malformée
404no_such_chatBDCe chat_id n’existe pas
404no_such_endpointWorkerMauvaise route : il n’existe que POST /api/bot/send
405method_not_allowedWorkerUne méthode autre que POST a été utilisée
409conversation_not_openBDCe 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
409chat_closedBDCette conversation de rencontre (dating) n’accepte plus de messages
422empty_textBDtext est vide une fois les espaces retirés
422too_longBDMessage au-delà du plafond — voyez la valeur max jointe
422bad_targetBDkind n’est pas l’un de "chat" / "community_room" / "business_room"
404no_such_roomBDAucun salon de groupe ou d’équipe ne correspond à ce chat_id (le salon a été supprimé)
409room_archivedBDCe salon est archivé — ni les humains ni les bots n’y écrivent plus
403invite_staleBDLes 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
422bad_jsonWorkerLe corps de la requête n’est pas du JSON valide
422bad_chat_idWorkerchat_id / target_id doit être un entier positif
422bad_textWorkertext doit être une chaîne
422idempotency_key_conflictWorkerIdempotency-Key et client_key ont été envoyés ensemble avec des valeurs différentes — n’en envoyez qu’un
422bad_client_keyWorkerLa clé anti-doublon ne respecte pas le format (8–128 caractères, A–Z a–z 0–9 _ . : -)
429rate_limitedWorker / BDPlafond dépassé — regardez l’en-tête Retry-After et les champs window / retry_after_sec
502database_errorWorkerLa base a répondu autre chose qu’un 2xx — voyez pg_code et detail joints : c’est le message réel de Postgres
503service_key_not_configuredWorkerLa clé de service n’est pas configurée côté serveur — c’est un problème chez Korat, pas chez l’appelant
504database_unreachableWorkerAppel à 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.