Webhooks

Définissez une URL de webhook et Sendveo y relaie chaque message entrant réel en temps réel, pour que vos systèmes réagissent sans interrogation continue.

Configurer votre webhook

Définissez ou remplacez votre point de terminaison. La réponse inclut un secret partagé, affiché à la création du webhook ou lorsque vous le renouvelez. L'URL doit être joignable publiquement via HTTPS.

PUT/v1/webhook
cURL
curl -X PUT https://api.sendveo.com/v1/webhook \
  -H "X-API-KEY: sv_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hooks.yourapp.com/sendveo/inbound" }'
200 OK
{
  "webhook": {
    "url": "https://hooks.yourapp.com/sendveo/inbound",
    "active": true,
    "secret": "whsec_2f8b41c9a7e04d6fb0c3e1a95d72"
  }
}

La charge utile du relais

Chaque message entrant est livré sous forme de POST JSON vers votre URL :

POST vers votre point de terminaison
{
  "type": "message.received",
  "message": {
    "id": "msg_7Qk2",
    "channelId": "chn_sales01",
    "chatId": "chat_aa11",
    "from": "+212670111222",
    "senderName": "Khadija Alaoui",
    "text": "Hi, is the order still available?",
    "timestamp": "2026-09-09T14:02:11Z"
  }
}

Répondez à un message en l'envoyant vers son chatId : POST /v1/messages avec { channelId, to: chatId, text }.

Catalogue des événements

Seize types d'événements sont envoyés en POST à votre URL, tous avec le secret partagé dans l'en-tête x-sendveo-secret. Chaque événement porte occurredAt (le moment où il s'est produit, pas celui où nous l'avons relayé). Les huit événements de message transportent le message entier sous message, dans la forme même qu'utilise message.received, si bien qu'un seul parseur les lit tous les huit ; les trois événements de groupe portent sur une conversation et transportent chat à la place ; les cinq événements de numéro portent sur une ligne et transportent number.

ÉvénementDéclenché quandChamps supplémentaires
message.receivedUn contact envoie un message.aucun
message.sentVotre numéro a envoyé un message, depuis le téléphone lui-même ou via Sendveo.message.origin
message.deliveredUn message sortant est arrivé sur l'appareil du destinataire.aucun
message.readUn message sortant a été lu.aucun
message.failedUn message sortant a été rejeté.reason
message.reactionQuelqu'un a réagi, ou a retiré sa réaction.emoji, by
message.editedL'expéditeur a modifié le message.by
message.deletedLe message a été supprimé pour tout le monde.by
group.member_joinedQuelqu'un a été ajouté à un groupe.chat, member, by
group.member_leftQuelqu'un a quitté un groupe, ou en a été retiré.chat, member, by
group.updatedUn groupe a été créé, ou son sujet a changé.chat, subject, by
number.disconnectedle lien avec le téléphone a cessé de fonctionnernumber, reason
number.reconnectedle lien fonctionne à nouveaunumber
number.phone_offlinele lien est intact mais le téléphone ne répond pasnumber
number.phone_onlinele téléphone répond à nouveaunumber
number.history_syncedl'historique récent d'un numéro qui vient d'être relié a été importénumber, counts
message.read
{
  "type": "message.read",
  "occurredAt": "2026-09-16T11:01:00Z",
  "by": "+212670111222",
  "message": {
    "id": "msg_8Rm5",
    "channelId": "chn_sales01",
    "chatId": "chat_aa11",
    "from": null,
    "senderName": null,
    "text": "Votre commande est prete.",
    "timestamp": "2026-09-16T10:59:00Z",
    "type": "text",
    "status": "READ",
    "attachments": [],
    "replyTo": null,
    "location": null,
    "contacts": null,
    "linkPreview": null
  }
}
message.reaction
{
  "type": "message.reaction",
  "occurredAt": "2026-09-16T11:03:00Z",
  "emoji": "❤️",
  "by": "+212670111222",
  "message": { "id": "msg_8Rm5", "chatId": "chat_aa11", "type": "text", "status": "READ" }
}

Le retrait d'une réaction est le même événement message.reaction avec "emoji": null, et non un huitième type : un seul gestionnaire couvre les deux moitiés.

Les redistributions ne sont pas relayées deux fois : le service de messagerie redistribue les événements comme il redistribue les messages, et un doublon est dédupliqué, si bien que votre webhook se déclenche une seule fois. Un événement portant sur un message que Sendveo ne détient pas (une conversation antérieure à la connexion) est acquitté puis abandonné : il n'est pas relayé, et ce n'est pas une erreur.

Événements de groupe

Les trois événements de groupe portent chat là où les événements de message portent message, plus by (qui a agi). chat.members est la liste réduite des participants : qui, leur rôle, et s'il s'agit de votre propre numéro.

group.member_joined
{
  "type": "group.member_joined",
  "occurredAt": "2026-09-16T11:40:00Z",
  "by": "+212661234567",
  "member": { "phone": "+212612345678", "name": "Hind Zerouali", "removed": false },
  "chat": {
    "id": "cht_m8k3x7q2a1b4",
    "chatId": "120363001122334455@g.us",
    "channelId": "chn_sales01",
    "kind": "group",
    "name": "Atlas team - deliveries",
    "members": [
      { "phone": "+212661234567", "name": null, "role": "super_admin", "self": true },
      { "phone": "+212612345678", "name": "Hind Zerouali", "role": "member", "self": false }
    ]
  }
}
group.updated
{
  "type": "group.updated",
  "occurredAt": "2026-09-16T11:45:00Z",
  "by": "+212661234567",
  "subject": "Atlas team - deliveries 2026",
  "chat": { "id": "cht_m8k3x7q2a1b4", "chatId": "120363001122334455@g.us", "kind": "group" }
}

Être retiré et partir de soi-même forment UN SEUL événement, group.member_left, que member.removed permet de distinguer : exactement le choix de forme que fait message.reaction pour une réaction et son retrait. La création d'un groupe et son renommage sont eux aussi un seul événement, group.updated, parce que pour un gestionnaire les deux disent « les détails de ce groupe ont changé » ; subject est le sujet après le changement.

Un message envoyé dans un groupe arrive comme un message.received ordinaire portant message.group (quelle conversation) et message.author (qui a parlé) : il n'existe pas d'événement distinct pour les messages de groupe. Les deux champs valent null sur un message en tête-à-tête, ce qui est exactement ce que continue de voir un gestionnaire écrit avant les groupes.

Un événement portant sur une conversation d'un numéro que Sendveo ne connaît pas est acquitté puis abandonné, comme tout autre événement de compte inconnu. Il n'est pas relayé, et ce n'est pas une erreur.

Événements de numéro

Cinq événements portent sur un numéro plutôt que sur un message ou une conversation : ils transportent donc number là où les autres transportent message ou chat. number a exactement la forme que renvoie GET /v1/numbers/:id, statut et santé réunis, si bien qu'un seul parseur lit les deux.

number.disconnected
{
  "type": "number.disconnected",
  "occurredAt": "2026-09-16T10:05:00Z",
  "reason": "SESSION_EXPIRED",
  "number": {
    "id": "chn_sales01",
    "displayName": "Sales line",
    "declaredNumber": "+212661234567",
    "pairedNumber": "+212661234567",
    "status": "CREDENTIALS",
    "health": {
      "phoneOnline": false,
      "lastSeenAt": "2026-09-16T09:58:00Z",
      "lastEventAt": "2026-09-16T10:05:00Z"
    }
  }
}

reason n'accompagne que number.disconnected : une forme par nom d'événement, la règle que suivent déjà les événements de message et de groupe. Traitez number.disconnected comme une raison de déranger quelqu'un, et number.phone_offline comme une chose qu'on laisse passer : confondre les deux fait refaire une liaison complète à un client parce que son téléphone était en charge dans la pièce d'à côté.

Un navigateur qui tient une session de tableau de bord peut lire en direct les mêmes seize événements sur GET /app/events. Il n'existe volontairement pas d'équivalent par clé API : un EventSource de navigateur ne peut pas envoyer d'en-tête Authorization, donc un flux sur l'API signifierait une clé API dans une chaîne de requête, dans un historique de navigateur et dans chaque journal de proxy. Pour un serveur, le webhook est le canal de push.

Versionnage de la charge utile

Des champs sont ajoutés, jamais renommés. message.received conserve exactement les sept champs qu'il avait en V1 (id, channelId, chatId, from, senderName, text, timestamp), avec les mêmes noms et les mêmes sens ; type, status, attachments, replyTo, location, contacts et linkPreview viennent s'y ajouter. Un gestionnaire écrit pour la V1 continue de fonctionner, et c'est pourquoi la légende d'une photo arrive toujours dans text.

type apparaît deux fois et désigne deux choses : en tête de l'enveloppe, c'est l'événement (message.read) ; à l'intérieur de message, c'est le genre du message (image). Le second est nommé comme dans la ressource message, de sorte qu'un seul analyseur lit les deux.

Vérifier un relais

Chaque relais transmet votre secret partagé dans l'en-tête x-sendveo-secret. Comparez-le à la valeur de votre configuration de webhook et rejetez tout ce qui ne correspond pas.

node / express
app.post("/sendveo/inbound", (req, res) => {
  const secret = req.header("x-sendveo-secret")
  if (secret !== process.env.SENDVEO_WEBHOOK_SECRET) {
    return res.status(401).end()
  }
  const { message } = req.body
  handleInbound(message)
  res.status(200).end()
})

Ce qui est relayé

Tous les événements des tableaux ci-dessus sont relayés. Vos propres messages sortants ne vous sont jamais renvoyés comme message.received : cet événement est donc toujours un vrai message venu de quelqu'un d'autre. Avant la V1.1, seul message.received existait ; un gestionnaire qui ignore un type inconnu continue de fonctionner sans changement.

GET/v1/webhook
DELETE/v1/webhook
Webhooks WhatsApp : événements entrants · Sendveo