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.
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.
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.
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.
POST /api/v1/messages HTTP/1.1
Authorization: Bearer sk_a3f9…
Content-Type: application/json
Idempotency-Key: commande-812-confirmationCe 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.
{
"to": "+221771112233",
"type": "text",
"text": "Votre colis part demain matin."
}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.
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.
{
"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.
{
"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.
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é.
200 {
"dry_run": true,
"would_send": true,
"to": "221771112233"
}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.
409 {
"error": "contact_opted_out",
"message": "This contact opted out of marketing messages."
}| Code | Ce que ça veut dire, ce qu'il faut faire |
|---|---|
invalid_payload | Le corps n'est pas exploitable. La phrase dit quel champ. Corrigez, ne réessayez pas tel quel. |
invalid_phone | to n'est pas un numéro international valide (indicatif pays compris, sans le 0 national). |
from_required | L'espace a plusieurs numéros : précisez from. |
no_connected_number | Aucun numéro WhatsApp connecté sur cet espace. C'est à l'administrateur de le brancher. |
template_not_found | Aucun template de ce nom dans cette langue. Vérifiez les deux. |
template_not_approved | Le 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_mismatch | Le nombre de variables ne correspond pas au corps du template, ou un bouton dynamique attend sa valeur. |
media_url_rejected | L'adresse du média a été refusée : non HTTPS, port interdit, adresse non publique, ou fichier inaccessible. |
media_too_large | Le fichier dépasse le plafond WhatsApp pour ce type. |
media_type_unsupported | Le type réel du fichier n'est pas accepté par WhatsApp pour ce type. |
out_of_window | La 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_out | Le contact s'est désinscrit du marketing. Retirez-le de vos relances. Un template utilitaire ou d'authentification reste possible. |
no_marketing_opt_in | Le 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_paused | La note de qualité du numéro est dégradée et l'envoi est freiné. Prévenez votre client, ne bouclez pas. |
tier_exhausted | Le palier d'envoi quotidien est atteint. L'en-tête Retry-After dit dans combien de secondes réessayer. |
recipient_unreachable | Ce destinataire n'est plus joignable dans l'espace. |
tenant_suspended | L'envoi est suspendu pour cet espace ; son administrateur a été prévenu. Ne réessayez pas, contactez votre client. |
rate_limited | Vous appelez trop vite. Retry-After dit combien attendre. |
unauthorized | Clé absente, mal formée, inconnue ou révoquée. |
provider_error | WhatsApp 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.
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.
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.
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
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_limitedet unRetry-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.
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.
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.
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 lesecret: le secret n'est affiché qu'une seule fois. - Signez et postez votre premier événement sur
/api/events. Vous recevez 202 avechandled. - 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.
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.
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=.
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.
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
});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
{
"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 }
}| Champ | Requis | Description |
|---|---|---|
version | oui | Toujours "1" pour ce contrat. |
event_id | oui | Identifiant du fait. Le point le plus important : voir la section suivante. |
type | oui | Nommage objet.action en minuscules. Liste ouverte. |
object_ref | non | Référence de la chose dans votre système (le numéro de commande). |
contact_phone | oui | Numé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_name | non | Nom affiché. Crée la fiche contact si absente. |
occurred_at | non | Date ISO 8601 du fait. À défaut, TEERAL prend la date de réception et le signale. |
data | non | Objet 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.
| Champ | Désigne | Exemple |
|---|---|---|
object_ref | la chose | la commande 123 |
event_id | le 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.
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.
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.
| Type | Effet |
|---|---|
order.created | Crée la commande. Déclenche la confirmation de commande si vous avez activé ce scénario dans votre espace. |
order.status_changed | Signale 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.resolved | Déclenche le suivi de ticket support. |
cart.abandoned | Enregistre un panier abandonné. |
cart.converted | Marque 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.
{
"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 created → prepared → shipped → in_transit → delivered, 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.
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
| Code | Signification | Que faire |
|---|---|---|
| 202 | Accepté. Le corps renvoie l'identifiant TEERAL et handled (une automatisation s'en occupe, ou non). | Rien. |
| 400 | Corps mal formé. data.code précise lequel. | Corriger l'émission. Rien n'a été enregistré. |
| 401 | Source inconnue ou signature invalide. | Vérifier l'identifiant de source et le calcul de signature. |
| 403 | Source 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. |
| 413 | Corps 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_idne 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_iddifférent (sinon rejeu neutre) vers un destinataire opt-in. Un humain confirme la réception.
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.
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.
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'URL | Pourquoi |
|---|---|
https:// obligatoire | Aucune donnée client ne part en clair. |
| Joignable depuis Internet | localhost, un nom sans point et les adresses privées ou lien-local (v4 et v6) sont refusés. |
| Port 443 ou 8443 | Tout autre port sert surtout à atteindre un service d'administration. |
| Aucun identifiant dans l'URL | Il 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
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ête | Contenu |
|---|---|
x-teeral-event | Le type de l'événement, dupliqué ici pour router sans lire le corps. |
x-teeral-delivery | La 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-signature | sha256= suivi du HMAC hexadécimal minuscule du corps brut. |
Corps
{
"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
}
}
}| Champ | Toujours présent | Description |
|---|---|---|
version | oui | Toujours "1". Les évolutions se font par ajout de champs, jamais par rupture. |
event_id | oui | Identifiant du fait, opaque et stable entre les tentatives. Votre clé de déduplication. |
type | oui | Nommage objet.action. Le catalogue s'agrandira : ignorez proprement un type inconnu plutôt que d'échouer. |
object_ref | non | null 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_at | oui | Date du fait, ISO 8601 en UTC. Ce n'est pas la date de la livraison : une retentative garde la date d'origine. |
contact.phone | oui | Format E.164, + compris. |
contact.name | non | null quand le contact n'a pas de nom connu. |
data | oui | Charge propre au type. Voir ci-dessous pour form.submitted. |
data de form.submitted
| Champ | Description |
|---|---|
form_name | Nom du Formulaire dans l'espace TEERAL, ou null. |
meta_flow_id | Identifiant Meta du Flow, ou null. |
fields | Les 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. |
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.
| Champ | Description |
|---|---|
catalog_id | Identifiant Meta du catalogue d'origine, ou null. |
note | Message libre joint au panier par le client, ou null. |
items | Les 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=.
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.
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);
});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.
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éponse | Ce que fait TEERAL |
|---|---|
| 2xx | Livré. Rien de plus. |
| 408 425 429 5xx | Retentative. Ce sont des refus datés, pas des refus de fond. |
| Aucune réponse | Retentative. 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.
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.
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.