API TEERALContrat v1

Envoyez des messages WhatsApp depuis votre application

Un appel HTTP authentifié, et le message part sur le numéro WhatsApp Business de votre client — avec son opt-in vérifié, sa fenêtre de 24 h respectée, son palier d'envoi tenu, et le message visible dans l'inbox de son équipe. Vous n'avez ni jeton Meta à manipuler, ni règle de conformité à réimplémenter.

01
Votre application
Une confirmation, un suivi, une relance
02
POST /api/v1/messages
Authentifié par clé d'API
03
Garde-fous
Opt-in, fenêtre, palier, qualité du numéro
04
WhatsApp client
Envoyé, tracé, facturé, visible dans l'inbox

Contrat public et stable : les évolutions se font par ajout, jamais par rupture. Cette page couvre les deux surfaces publiques de TEERAL — envoyer un message que vous décidez, puis signaler un fait métier dont le tenant décide la réponse. Commencez par la première, c'est presque toujours celle que vous cherchez.

01Envoyer un message depuis votre application

Deux surfaces publiques cohabitent, et elles ne servent pas le même besoin. Choisir la mauvaise se paie en heures perdues, donc la distinction est ici, avant tout le reste.

Qui décide du message ?

POST /api/v1/messages quand c'est VOUS qui décidez, dans votre code : vous choisissez le texte ou le template, le moment, les variables. POST /api/events (sections suivantes) quand c'est le TENANT qui décide, dans son écran Automatisations : vous signalez un fait métier, il a configuré ce qui part en réponse.

Ce que vous pouvez envoyer, et à qui. Un template utilitaire ou d'authentification — OTP, confirmation, suivi, rappel — part vers n'importe quel numéro, à tout moment, sans condition. Un texte libre exige que le client vous ait écrit dans les 24 h. Un template marketing exige son consentement. Ces trois règles sont celles de Meta, pas celles de TEERAL, et elles sont appliquées à chaque appel.

Ce que l'API d'envoi vous épargne, et que l'API Cloud de Meta en direct ne vous épargne pas : le jeton WhatsApp reste chez TEERAL, l'opt-in du contact est vérifié, la fenêtre de 24 h est appliquée, le palier d'envoi et la qualité du numéro sont respectés, le message apparaît dans l'inbox de l'équipe, et le compteur de facturation Meta reste juste.

Aucun de ces garde-fous n'est contournable.

Vos envois passent par le même chemin que ceux des agents. Un message marketing vers un contact qui a envoyé STOP est refusé, quel que soit l'appelant. C'est ce qui protège le numéro WhatsApp de votre client — et donc votre intégration.

02Clé d'API

L'administrateur de l'espace crée votre clé dans Paramètres → Développeurs, bloc « Envoyer des messages ». Elle s'affiche une seule fois : TEERAL n'en conserve que l'empreinte, personne ne peut la relire, pas même le support.

POSThttps://votre-espace.teeral.app/api/v1/messages
Requête HTTP
POST /api/v1/messages HTTP/1.1
Authorization: Bearer sk_a3f9…
Content-Type: application/json
Idempotency-Key: commande-812-confirmation
Pourquoi Bearer ici et HMAC pour l'ingestion ?

Ce ne sont pas les mêmes contraintes. L'ingestion doit prouver l'intégrité d'un corps que vous émettez, d'où la signature. L'envoi doit seulement prouver une identité, d'où le jeton. Le stockage suit : le secret d'ingestion reste en clair chez nous (le serveur recalcule le HMAC avec), la clé d'envoi non — rien n'oblige à la conserver, donc elle ne l'est pas.

Une clé par application. La révoquer coupe cette intégration à la seconde, sans toucher aux autres.

03Corps de la requête

Un seul endpoint, discriminé par type. Le destinataire est toujours un numéro au format international.

JSON · texte libre
{
  "to": "+221771112233",
  "type": "text",
  "text": "Votre colis part demain matin."
}
Le texte libre n'est possible que dans la fenêtre de 24 h.

WhatsApp n'autorise un message libre que dans les 24 h qui suivent le dernier message du client. Il est alors gratuit. Au-delà, il faut un template approuvé, qui est facturé par Meta. Hors fenêtre, l'API répond 409 out_of_window : basculez sur un template plutôt que de réessayer.

Un template utilitaire ou d'authentification atteint TOUT LE MONDE, à tout moment.

C'est le point qui compte pour la plupart des intégrations. Un code de connexion, une confirmation de commande, un suivi de livraison, un rappel de rendez-vous : ces messages n'ont besoin ni de la fenêtre de 24 h, ni d'un opt-in marketing. Ils partent vers n'importe quel numéro, y compris un client qui n'a jamais écrit, y compris un client qui a répondu STOP — le STOP ne coupe que le marketing, jamais le suivi d'une transaction en cours.

Seul le marketing exige un opt-in. Si vous recevez no_marketing_opt_in ou contact_opted_out, c'est que vous envoyez un template de catégorie marketing : vérifiez la catégorie de votre template avant de conclure que le client est injoignable.

JSON · template
{
  "to": "+221771112233",
  "type": "template",
  "template": {
    "name": "confirmation_commande",
    "language": "fr",
    "variables": ["Awa", "12 500 XOF"],
    "buttons": [{ "index": 0, "value": "CMD-812" }]
  }
}

Le template est désigné par son nom et sa langue, tels qu'ils existent chez Meta. Les variables remplissent le corps dans l'ordre ; buttons ne sert qu'aux boutons dont le lien contient une variable, et attend le fragment qui la remplace, pas l'adresse complète.

JSON · média
{
  "to": "+221771112233",
  "type": "image",          // image | video | audio | document
  "media": {
    "url": "https://cdn.exemple.sn/colis-812.jpg",
    "caption": "Votre colis est prêt"
  }
}

TEERAL va chercher le fichier à cette adresse, en garde une copie pour que l'agent voie ce que son client a reçu, puis l'envoie. L'adresse doit être en HTTPS public (port 443 ou 8443) ; jusqu'à trois redirections sont suivies, chacune revérifiée. Les types et les tailles sont ceux que WhatsApp accepte : 5 Mo pour une image, 16 Mo pour une vidéo ou un audio, 100 Mo pour un document. Une légende sur un audio est refusée — WhatsApp ne l'affiche pas, et la perdre en silence serait pire.

Champ from : à ignorer, sauf si vous avez plusieurs numéros.

Omis, TEERAL choisit : le numéro sur lequel ce contact vous parle déjà, sinon l'unique numéro de l'espace. Si l'espace en a plusieurs et que rien ne permet de trancher, la réponse est 400 from_required — jamais un choix arbitraire, chaque numéro ayant sa propre santé chez Meta.

04Mettre au point sans écrire à personne

Ajoutez "dry_run": true et tout se déroule pour de vrai : authentification, validation du corps, résolution du template, verdict complet des règles d'envoi. La réponse dit ce qui serait parti, avec le même code de refus le cas échéant. Rien n'est créé, rien ne part chez Meta, rien n'apparaît dans l'inbox, rien n'est facturé.

Réponse · dry-run accepté
200 {
  "dry_run": true,
  "would_send": true,
  "to": "221771112233"
}
Utilisez-le pendant tout votre développement.

Sans lui, chaque itération envoie de vrais messages à de vrais clients. Ce n'est pas qu'une question de coût : du bruit non sollicité fait chuter la note de qualité du numéro de votre client chez Meta, et cette note conditionne son volume d'envoi quotidien.

05Codes de refus

Chaque refus porte un error stable dans le corps de la réponse. Branchez votre logique sur ce code, jamais sur la phrase qui l'accompagne : le code est un engagement, la phrase peut changer.

Réponse · refus
409 {
  "error": "contact_opted_out",
  "message": "This contact opted out of marketing messages."
}
CodeCe que ça veut dire, ce qu'il faut faire
invalid_payloadLe corps n'est pas exploitable. La phrase dit quel champ. Corrigez, ne réessayez pas tel quel.
invalid_phoneto n'est pas un numéro international valide (indicatif pays compris, sans le 0 national).
from_requiredL'espace a plusieurs numéros : précisez from.
no_connected_numberAucun numéro WhatsApp connecté sur cet espace. C'est à l'administrateur de le brancher.
template_not_foundAucun template de ce nom dans cette langue. Vérifiez les deux.
template_not_approvedLe template existe mais Meta ne l'a pas approuvé, ou il porte un en-tête que l'API ne sait pas encore envoyer.
template_variables_mismatchLe nombre de variables ne correspond pas au corps du template, ou un bouton dynamique attend sa valeur.
media_url_rejectedL'adresse du média a été refusée : non HTTPS, port interdit, adresse non publique, ou fichier inaccessible.
media_too_largeLe fichier dépasse le plafond WhatsApp pour ce type.
media_type_unsupportedLe type réel du fichier n'est pas accepté par WhatsApp pour ce type.
out_of_windowLa fenêtre de 24 h est fermée : seul le texte libre est concerné. Un template utilitaire ou d'authentification part quand même.
contact_opted_outLe contact s'est désinscrit du marketing. Retirez-le de vos relances. Un template utilitaire ou d'authentification reste possible.
no_marketing_opt_inLe contact n'a jamais donné son consentement marketing — ce n'est pas un désabonnement. Un template utilitaire ou d'authentification part quand même.
quality_pausedLa note de qualité du numéro est dégradée et l'envoi est freiné. Prévenez votre client, ne bouclez pas.
tier_exhaustedLe palier d'envoi quotidien est atteint. L'en-tête Retry-After dit dans combien de secondes réessayer.
recipient_unreachableCe destinataire n'est plus joignable dans l'espace.
tenant_suspendedL'envoi est suspendu pour cet espace ; son administrateur a été prévenu. Ne réessayez pas, contactez votre client.
rate_limitedVous appelez trop vite. Retry-After dit combien attendre.
unauthorizedClé absente, mal formée, inconnue ou révoquée.
provider_errorWhatsApp a refusé le message. 502 : réessayer a du sens. 422 : non, ce message ne partira jamais ainsi.

06Suivre le sort d'un message

Un envoi accepté rend 201 avec l'identifiant TEERAL du message et son wamid Meta.

Réponse · envoyé
201 {
  "id": "7b3e…",
  "status": "sent",
  "wamid": "wamid.HBgABCD…"
}

Deux façons de connaître la suite, au choix. À la demande : GET /api/v1/messages/{id} rend le statut et les horodatages d'envoi, de livraison et de lecture. En temps réel : déclarez une URL de retour sur votre clé, et TEERAL y postera chaque changement.

Retour d'envoi · vers votre URL
POST /votre-endpoint HTTP/1.1
x-teeral-event: message.delivered
x-teeral-signature: sha256=

{
  "version": "1",
  "event_id": "7b3e…:delivered",
  "type": "message.delivered",
  "occurred_at": "2026-08-26T09:59:30.000Z",
  "data": { "message_id": "7b3e…", "wamid": "wamid.HBgABCD…" }
}

Quatre types : message.sent, message.delivered, message.read, message.failed — un par transition, pour que vous branchiez un switch et non une cascade de conditions. La signature suit exactement le même schéma que les Événements TEERAL décrits plus bas : si vous avez déjà écrit ce code, il fonctionne ici sans une ligne de plus.

Chaque clé ne reçoit que ses propres messages.

L'URL de retour appartient à la clé, pas à l'espace. Deux prestataires travaillant pour le même client ne voient jamais le trafic l'un de l'autre. C'est aussi pourquoi ces retours ne passent pas par la Destination de l'espace, qui est unique et sert à autre chose.

07Rythme, doublons et garanties

Posez une clé d'idempotence sur chaque envoi.

L'en-tête Idempotency-Key garantit qu'une retentative réseau n'enverra pas deux fois le même message à votre client. Prenez l'identifiant que vous avez déjà — commande-812-confirmation fait très bien l'affaire : 128 caractères au maximum, caractères imprimables sans espace. Rejouer la même clé rend la première réponse mémorisée — y compris si le corps a changé : le rôle d'une clé d'idempotence est d'empêcher un double envoi, pas d'arbitrer entre deux intentions. Pour envoyer autre chose, utilisez une autre clé.

  • Rythme. Calé sur le palier d'envoi du numéro, avec une réserve pour les salves courtes. Un dépassement rend 429 rate_limited et un Retry-After — respectez-le plutôt que de boucler.
  • Corps. 128 Ko au maximum. Un texte est plafonné à 4 096 caractères, une légende à 1 024.
  • Contact inconnu. Créé automatiquement : aucune étape préalable n'est nécessaire pour confirmer la commande d'un nouveau client.
  • Visibilité. Vos messages apparaissent dans l'inbox de l'équipe, identifiés comme venant de l'API. C'est voulu : l'agent qui répond doit savoir ce que votre application a déjà dit.
Ce que cette API ne fait pas.

Elle n'ouvre aucune lecture : ni l'inbox, ni les contacts, ni les campagnes. Elle ne reçoit pas les messages entrants de vos clients. Elle n'écrit pas dans le CRM, ne déclenche pas de campagne, n'envoie ni boutons interactifs, ni listes, ni réactions, ni messages de groupe. Si votre besoin est là, dites-le à votre client : c'est une demande produit, pas un contournement à inventer.

API d'ingestion d'événementsContrat v1

Signaler un fait métier

L'autre besoin. Votre système signale un fait : une commande passée, une livraison expédiée, un ticket résolu. TEERAL le reçoit, choisit le scénario que le tenant a activé, et envoie le message. Vous poussez ; TEERAL encaisse — et c'est lui, pas vous, qui décide du contenu.

01
Votre système
Un fait survient (commande, ticket…)
02
POST /api/events
Requête signée (HMAC)
03
Décideur
Une automatisation active écoute ce type, et ce statut ?
04
WhatsApp client
Template envoyé, tracé, facturé

L'ingestion est servie par l'application TEERAL, pas par son Directus : ce sont deux hôtes distincts.

08Démarrage rapide

Trois étapes, du premier événement au message reçu.

  • Créez une source. Dans votre espace TEERAL, écran Paramètres → Développeurs, bloc « Signaler vos événements ». Notez l'identifiant de source (src_…), et le secret : le secret n'est affiché qu'une seule fois.
  • Signez et postez votre premier événement sur /api/events. Vous recevez 202 avec handled.
  • Vérifiez le journal des exécutions dans TEERAL. Tant qu'un envoi réel n'est pas activé, l'exécuteur reste en dry-run, aucun message ne part, vous validez sans risque.
L'adaptateur vers ce contrat vous appartient.

TEERAL ne traduit pas le format de votre outil et n'interroge jamais votre système. Vous décidez quand émettre et vous mappez vos champs vers le corps ci-dessous.

09Endpoint & authentification

Une seule route reçoit tout. L'URL exacte, l'identifiant de source et le secret figurent dans votre espace TEERAL : recopiez-les de là, pas de cette page.

POSThttps://votre-espace.teeral.app/api/events
Requête HTTP
POST /api/events HTTP/1.1
Content-Type: application/json
x-teeral-source: src_b1f84be9c5c07f2300fe34ce18ae3d97
x-teeral-signature: sha256=9f86d081884c7d659a2feaa0c55ad015…

Aucun secret ne circule dans l'URL : il ne doit jamais finir dans un journal serveur ou une capture d'écran. Le secret est régénérable à tout moment, source par source, sans toucher à vos autres intégrations.

10Signature HMAC

La signature est un HMAC-SHA256 du corps brut (les octets exactement tels qu'ils partent) avec votre secret comme clé, en hexadécimal minuscule, préfixé de sha256=.

Signez le corps que vous envoyez, jamais une version re-sérialisée.

Le moindre réencodage (ordre des clés, espaces) change la signature et vous vaut un 401. Sérialisez une fois, signez cette chaîne, envoyez cette chaîne.

JavaScript · Node
import { createHmac } from "node:crypto";

// sérialisé UNE fois, c'est cette chaîne exacte qui part
const body = JSON.stringify(payload);
const signature = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");

await fetch(`${TEERAL_URL}/api/events`, {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "x-teeral-source": sourceKey,
    "x-teeral-signature": signature,
  },
  body,                // le MÊME corps
});
Pas de node:crypto ? (sandbox Directus Flows, edge…)

Utilisez l'API Web Crypto (crypto.subtle avec HMAC) ou une implémentation HMAC-SHA256 autonome. Le résultat doit être identique octet pour octet.

11Corps de la requête

JSON
{
  "version": "1",
  "event_id": "evt_2026_07_21_88213",
  "type": "order.created",
  "object_ref": "CMD-20260721-0042",
  "contact_phone": "221771112233",
  "contact_name": "Awa Diop",
  "occurred_at": "2026-07-21T14:32:00Z",
  "data": { "amount_xof": 15000 }
}
ChampRequisDescription
versionouiToujours "1" pour ce contrat.
event_idouiIdentifiant du fait. Le point le plus important : voir la section suivante.
typeouiNommage objet.action en minuscules. Liste ouverte.
object_refnonRéférence de la chose dans votre système (le numéro de commande).
contact_phoneouiNuméro concerné au format international : indicatif pays + numéro, 8 à 15 chiffres. +, préfixe 00, espaces et tirets tolérés. Un format national (0 initial, sans indicatif) est refusé.
contact_namenonNom affiché. Crée la fiche contact si absente.
occurred_atnonDate ISO 8601 du fait. À défaut, TEERAL prend la date de réception et le signale.
datanonObjet libre. Les scénarios y puisent les variables du message.

12event_id vs object_ref

C'est l'erreur qui coûte le plus cher, et elle est silencieuse. Prenez une minute ici.

ChampDésigneExemple
object_refla chosela commande 123
event_idle fait« la commande 123 est passée en expédiée »

Une même commande produit plusieurs événements au cours de sa vie : ils partagent le même object_ref et ont chacun un event_id distinct.

Réutiliser le même event_id = notification jamais reçue.

Si vous réemployez un event_id pour tous les événements d'une commande, TEERAL prend les suivants pour des doublons et les ignore. Votre client ne reçoit jamais sa notification de livraison, et aucune erreur n'est levée.

event_id doit aussi rester stable entre deux tentatives d'envoi du même fait. C'est ce qui rend un rejeu sûr : si votre requête échoue en réseau et que vous la rejouez, le même event_id garantit qu'aucun message en double ne part chez votre client.

Une bonne valeur

L'identifiant de la ligne d'événement dans votre propre base, ou <référence objet>:<transition>:<horodatage du fait>.

13Types d'événements

Un type inconnu de TEERAL est accepté et journalisé sans rien déclencher. Vous pouvez donc émettre plus large que ce que TEERAL consomme aujourd'hui, sans rien changer quand un nouveau scénario sortira.

TypeEffet
order.createdCrée la commande. Déclenche la confirmation de commande si vous avez activé ce scénario dans votre espace.
order.status_changedSignale un changement de statut. Statut visé dans data.status : chaîne libre, votre vocabulaire tel quel. Le déclenchement se configure dans l'automatisation.
ticket.resolvedDéclenche le suivi de ticket support.
cart.abandonedEnregistre un panier abandonné.
cart.convertedMarque le panier converti.

Statuts de commande : votre vocabulaire

data.status accepte n'importe quelle chaîne non vide : confirmed, preparing, livré… TEERAL ne traduit rien et n'impose aucune liste. C'est dans l'automatisation, côté espace TEERAL, que se déclarent les statuts qui déclenchent un message : match exact, insensible à la casse, espaces ignorés. Un statut qui ne matche aucune automatisation active est journalisé et répond 202 avec handled: false : aucun message ne part.

Exemple
{
  "version": "1",
  "event_id": "CMD-0042:confirmed:2026-07-24T10:05:00Z",
  "type": "order.status_changed",
  "object_ref": "CMD-0042",
  "contact_phone": "221771112233",
  "data": { "status": "confirmed" }
}
// Une automatisation configurée sur le statut "confirmed" envoie
// son template ; un statut sans automatisation → 202, handled: false.

Pour le suivi interne de la commande (fiche contact, historique), TEERAL reconnaît en plus le cycle createdpreparedshippedin_transitdelivered, linéaire et sans retour. Un statut hors de ce cycle est conservé au journal sans faire avancer la fiche, cela n'empêche aucun envoi.

Montants

amount_xof en francs CFA, entier. Le XOF n'a pas de décimales : une valeur à virgule est refusée plutôt qu'arrondie en silence.

14Codes de réponse

CodeSignificationQue faire
202Accepté. Le corps renvoie l'identifiant TEERAL et handled (une automatisation s'en occupe, ou non).Rien.
400Corps mal formé. data.code précise lequel.Corriger l'émission. Rien n'a été enregistré.
401Source inconnue ou signature invalide.Vérifier l'identifiant de source et le calcul de signature.
403Source désactivée.La réactiver dans l'espace TEERAL.
409Événement déjà reçu (même event_id).Aucune action. Comportement normal d'un rejeu : votre client n'a pas été servi deux fois.
413Corps trop volumineux.Alléger le payload.

Codes de refus en 400

version_unsupported event_id_missing type_invalid contact_phone_missing contact_phone_invalid occurred_at_invalid data_not_object invalid_json

15Garanties & limites

Ce que TEERAL garantit

  • Réponse immédiate. L'ingestion authentifie, valide, enregistre, répond. Aucun appel à WhatsApp pendant votre requête : votre système n'attend jamais Meta.
  • Rejeu sans danger. Le même event_id ne produit ni second événement ni second message.
  • Isolation. Le tenant est déduit de votre source authentifiée, jamais d'un champ du corps.

Ce que TEERAL ne fait pas

  • Aucun retour de statut vers votre système : TEERAL ne vous notifie pas de la délivrance du message.
  • Aucune traduction depuis le format de votre outil : l'adaptateur vers ce contrat vous appartient.
  • Un événement sans numéro exploitable est refusé. Un fait qui ne désigne personne de joignable ne peut rien déclencher.

16Tester l'intégration

Validez en deux temps, sans jamais risquer un envoi accidentel au client.

  • Dry-run d'abord. Tant que l'envoi réel n'est pas activé côté TEERAL, l'exécuteur journalise sans envoyer. Émettez, vérifiez 202 + handled: true, et lisez le journal : un run dry-run y apparaît.
  • Puis réel. Une fois l'envoi activé, réémettez avec un event_id différent (sinon rejeu neutre) vers un destinataire opt-in. Un humain confirme la réception.
Conformité Meta

Le template du scénario doit être approuvé par Meta, et le destinataire opt-in. Rien qui puisse dégrader la qualité du numéro ne doit sortir.

Événements TEERAL sortantsContrat v1

17Recevoir les événements TEERAL

Le sens inverse. Quand un fait se produit dans TEERAL (aujourd'hui la soumission d'un Formulaire par un client sur WhatsApp) TEERAL le POSTe vers l'URL que vous déclarez. Même enveloppe versionnée, même schéma de signature : le code que vous avez écrit pour signer vos envois vous sert à vérifier ceux de TEERAL.

01
WhatsApp client
Un client soumet un Formulaire
02
TEERAL
L'Événement est enregistré puis mis en file
03
POST signé
Vers votre URL (HMAC)
04
Votre système
Répond 2xx en moins de 10 s

Vous déclarez cette URL (appelée la Destination) dans votre espace TEERAL, écran Paramètres → Développeurs, bloc « Recevoir les Événements TEERAL ». Une seule par espace. Le secret de signature y est affiché une seule fois, à la déclaration et à chaque régénération ; aucun écran ni aucune réponse d'API ne le redonne ensuite.

Contrainte sur l'URLPourquoi
https:// obligatoireAucune donnée client ne part en clair.
Joignable depuis Internetlocalhost, un nom sans point et les adresses privées ou lien-local (v4 et v6) sont refusés.
Port 443 ou 8443Tout autre port sert surtout à atteindre un service d'administration.
Aucun identifiant dans l'URLIl finirait dans nos journaux comme dans les vôtres. C'est la signature qui authentifie TEERAL.

Ces règles sont revérifiées à chaque tentative, pas seulement à l'enregistrement : une URL modifiée après coup ne contourne rien.

18Enveloppe & en-têtes

Requête HTTP
POST /votre-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: TEERAL-Webhooks/1 (+https://teeral.app/developpeurs)
x-teeral-event: form.submitted
x-teeral-delivery: 4c1f2a7e-3b90-4d55-8e21-1f6c0a93bd77.3
x-teeral-signature: sha256=9f86d081884c7d659a2feaa0c55ad015…
En-têteContenu
x-teeral-eventLe type de l'événement, dupliqué ici pour router sans lire le corps.
x-teeral-deliveryLa tentative : <identifiant de livraison>.<rang>. Il change à chaque retentative, là où event_id ne bouge pas. Journalisez les deux et une retentative ne se confondra jamais avec un fait neuf.
x-teeral-signaturesha256= suivi du HMAC hexadécimal minuscule du corps brut.

Corps

JSON
{
  "version": "1",
  "event_id": "9f1c0b2e-7d44-4a1f-9b3e-2c8a5f0d61aa",
  "type": "form.submitted",
  "object_ref": "CMD-20260731-0042",
  "occurred_at": "2026-07-31T09:14:22.000Z",
  "contact": {
    "phone": "+221771112233",
    "name": "Awa Diop"
  },
  "data": {
    "form_name": "Demande de rappel",
    "meta_flow_id": "1234567890123456",
    "fields": {
      "ville": "Dakar",
      "rappel_accepte": true
    }
  }
}
ChampToujours présentDescription
versionouiToujours "1". Les évolutions se font par ajout de champs, jamais par rupture.
event_idouiIdentifiant du fait, opaque et stable entre les tentatives. Votre clé de déduplication.
typeouiNommage objet.action. Le catalogue s'agrandira : ignorez proprement un type inconnu plutôt que d'échouer.
object_refnonnull quand rien ne rattache le fait à un objet de votre système. Pour form.submitted, c'est l'object_ref de l'événement que vous aviez poussé et qui a déclenché l'envoi du Formulaire ; null si un agent l'a envoyé à la main.
occurred_atouiDate du fait, ISO 8601 en UTC. Ce n'est pas la date de la livraison : une retentative garde la date d'origine.
contact.phoneouiFormat E.164, + compris.
contact.namenonnull quand le contact n'a pas de nom connu.
dataouiCharge propre au type. Voir ci-dessous pour form.submitted.

data de form.submitted

ChampDescription
form_nameNom du Formulaire dans l'espace TEERAL, ou null.
meta_flow_idIdentifiant Meta du Flow, ou null.
fieldsLes réponses du client, sous leurs noms techniques bruts et avec leurs valeurs brutes : telles que votre Formulaire les déclare. Les libellés embellis pour l'affichage sont une convention d'écran : ils ne sortent jamais.
Mappez par nom, pas par position.

Un Formulaire modifié dans TEERAL change le contenu de fields sans changer la version du contrat. Lisez les clés dont vous avez besoin et ignorez les autres.

data de order.received

Émis quand un client envoie un panier depuis le catalogue WhatsApp. C'est une intention d'achat ouverte dans la conversation, pas une commande payée : la confirmation, si elle a lieu, viendra de votre système. object_ref est toujours null : le panier arrive à l'initiative du client, rien ne le précède.

ChampDescription
catalog_idIdentifiant Meta du catalogue d'origine, ou null.
noteMessage libre joint au panier par le client, ou null.
itemsLes lignes du panier : product_retailer_id (votre référence produit : la clé de rapprochement avec votre système), quantity, item_price (prix unitaire), currency (explicite par ligne, jamais supposée), name (libellé résolu depuis le catalogue, ou null). Aucune URL d'image : celles de Meta sont signées et expirent.

19Vérifier la signature

Strictement le même schéma qu'à l'entrée : HMAC-SHA256 du corps brut, votre secret de Destination comme clé, hexadécimal minuscule, préfixé de sha256=.

Vérifiez sur le corps brut, avant tout JSON.parse.

Un corps reparsé puis re-sérialisé n'a plus les mêmes octets (ordre des clés, espaces), et aucune signature valide ne passera. Dans Express, montez la route en express.raw ; dans un framework qui parse d'office, demandez explicitement le corps brut.

JavaScript · Node
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody : un Buffer ou la chaine EXACTE recue, jamais un objet reserialise
function verifyTeeralSignature(rawBody, header, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const received = Buffer.from(header ?? "", "utf8");
  const awaited = Buffer.from(expected, "utf8");
  // longueurs comparees d'abord : timingSafeEqual leve si elles different
  return received.length === awaited.length && timingSafeEqual(received, awaited);
}

app.post("/teeral", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyTeeralSignature(req.body, req.get("x-teeral-signature"), process.env.TEERAL_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body.toString("utf8"));
  enqueue(event);           // le travail se fait APRES la reponse
  res.sendStatus(204);
});
Comparez en temps constant.

Une comparaison de chaînes ordinaire s'arrête au premier octet différent et laisse fuiter, à la milliseconde, où elle s'est arrêtée. timingSafeEqual (ou son équivalent) ferme cette porte.

Une régénération de clé invalide l'ancienne immédiatement.

Rien n'est gardé en réserve : posez la nouvelle clé dans votre système avant la prochaine livraison, sinon vous répondrez 401 aux événements suivants, et un 4xx est un échec définitif : chaque fait sera perdu dès sa première livraison, sans retentative.

20Retentatives & déduplication

Ce qui compte comme une livraison réussie

Une réponse 2xx en moins de 10 secondes. Le corps de votre réponse est lu de façon plafonnée et n'entre dans aucune décision : répondez 204 et faites le travail après. Une redirection 3xx n'est pas suivie : déclarez l'URL finale.

Votre réponseCe que fait TEERAL
2xxLivré. Rien de plus.
408 425 429 5xxRetentative. Ce sont des refus datés, pas des refus de fond.
Aucune réponseRetentative. DNS, coupure, ou dépassement des 10 s.
3xx et autres 4xxÉchec définitif, sans retentative. Votre endpoint a répondu et refusé : rejouer la même requête donnerait le même refus.

La cadence

Huit retentatives, de plus en plus espacées, après la livraison immédiate :

1 min 5 min 15 min 1 h 3 h 6 h 12 h 24 h

La dernière tombe environ 47 h après le fait. La promesse est donc chiffrable : votre endpoint peut être indisponible 24 h d'affilée sans qu'un seul fait soit perdu. Passé la huitième, l'événement est abandonné et l'administrateur de l'espace reçoit une notification.

Une intégration ne se coupe jamais toute seule.

Quel que soit le nombre d'échecs, TEERAL ne désactive pas votre Destination. Seul un administrateur la coupe, depuis l'écran Paramètres, et les faits survenus pendant une coupure volontaire sont perdus, sans mise en attente ni rejeu.

Déduplication et ordre

  • Dédupliquez par event_id. Il ne change pas d'une tentative à l'autre. Enregistrez-le et ignorez un fait déjà vu : c'est ce qui rend le rejeu inoffensif.
  • Le rejeu est possible, et se neutralise par event_id. Une réponse perdue en chemin fait recommencer TEERAL alors que vous aviez déjà traité le fait. La signature seule ne l'empêche pas : elle prouve l'origine, pas la nouveauté.
  • Aucune garantie d'ordre. Deux faits proches peuvent vous arriver dans le désordre, et un fait retenté arrive forcément après des faits plus récents. Fiez-vous à occurred_at, jamais à l'ordre de réception.
  • Traitez de façon idempotente. Répondre 2xx vite puis travailler en tâche de fond est le bon patron : les 10 secondes ne sont pas un budget de traitement.
Pas d'accusé de réception dans l'autre sens.

TEERAL ne vous propose aucune API pour rejouer un événement perdu ni pour lire l'historique des livraisons. L'état de votre intégration (dernière livraison réussie, dernier échec) se lit en tête de l'écran Paramètres → Développeurs, avec celui de vos clés et de vos sources.

TEERAL · Contrat d'événements v1 : engagement public, évolutif par ajout.Créer un espace TEERAL