Messages

Lisez les messages entrants que reçoivent vos numéros, et envoyez les vôtres. Répondez à une conversation lancée par un contact, ou écrivez directement à un numéro de téléphone : Sendveo ouvre la conversation pour vous.

L'objet message

message
{
  "id": "msg_7Qk2",
  "channelId": "chn_sales01",
  "direction": "INBOUND",
  "chatId": "chat_aa11",
  "from": "+212670111222",
  "to": null,
  "senderName": "Khadija Alaoui",
  "text": "Here is the model we agreed on.",
  "type": "image",
  "status": "RECEIVED",
  "statusAt": null,
  "failureReason": null,
  "timestamp": "2026-09-09T14:02:11Z",
  "attachments": [
    {
      "id": "att_5Hq1",
      "kind": "IMAGE",
      "mimeType": "image/jpeg",
      "fileName": "modele.jpg",
      "size": 184320,
      "durationSeconds": null,
      "width": 1080,
      "height": 1440,
      "voiceNote": false,
      "url": "https://api.sendveo.com/v1/messages/msg_7Qk2/attachments/att_5Hq1"
    }
  ],
  "replyTo": { "id": "msg_7Qk1", "providerMessageId": "wamid.HB1" },
  "location": null,
  "contacts": null,
  "linkPreview": null,
  "reactions": [{ "emoji": "👍", "by": "+212670111222", "at": "2026-09-09T14:03:00Z" }],
  "deletedAt": null,
  "editedAt": null
}

direction vaut INBOUND ou OUTBOUND. from porte l'expéditeur sur les messages entrants et vaut null sur les sortants ; to en est le miroir : il porte le numéro du destinataire sur un message que vous avez envoyé à un numéro de téléphone, et vaut null sinon. status vaut SENT ou FAILED sur les messages sortants ; les messages entrants sont toujours RECEIVED.

Envoyer un message

Chaque envoi indique le numéro expéditeur dans channelId (numberId est accepté comme le même champ), le message dans text, et exactement un destinataire : chatId pour répondre à une conversation existante, ou to pour un numéro de téléphone. Le numéro doit être CONNECTED, sinon la requête renvoie 409.

POST/v1/messages
cURL
curl -X POST https://api.sendveo.com/v1/messages \
  -H "X-API-KEY: sv_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "channelId": "chn_sales01", "chatId": "chat_aa11", "text": "Yes - shipping tomorrow." }'
201 Created
{
  "message": {
    "id": "msg_8Rm5",
    "channelId": "chn_sales01",
    "direction": "OUTBOUND",
    "chatId": "chat_aa11",
    "to": null,
    "text": "Yes - shipping tomorrow.",
    "status": "SENT",
    "timestamp": "2026-09-09T14:05:40Z"
  }
}

Envoyer à un numéro de téléphone

Utilisez to avec un numéro de téléphone au format E.164 pour écrire à quelqu'un qui ne vous a jamais écrit : Sendveo ouvre la conversation et renvoie son chatId, si bien que les réponses arrivent dans le même fil. +212661234567, 212661234567 et 00212661234567 sont tous acceptés, avec ou sans espaces, tirets ou parenthèses.

cURL
curl -X POST https://api.sendveo.com/v1/messages \
  -H "X-API-KEY: sv_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "channelId": "chn_sales01", "to": "+212661234567", "text": "Your order is ready." }'
201 Created
{
  "message": {
    "id": "msg_9Tn7",
    "channelId": "chn_sales01",
    "direction": "OUTBOUND",
    "chatId": "chat_9Fp2",
    "to": "+212661234567",
    "text": "Your order is ready.",
    "status": "SENT",
    "timestamp": "2026-09-15T09:12:03Z"
  }
}

Un envoi vers un numéro qui a déjà une conversation poursuit cette conversation au lieu d'en ouvrir une seconde, que le contact ait écrit en premier ou vous. Une même personne ne se retrouve jamais répartie sur deux fils.

Un to qui n'est pas un numéro de téléphone exploitable, chatId et to ensemble, ou aucun des deux : dans les trois cas la requête renvoie 400 VALIDATION_FAILED en nommant le champ dans details, et rien n'est envoyé. Une forme nationale commençant par un zéro comme 0661234567 est refusée : sans indicatif pays, ce n'est qu'une supposition.

Restez mesuré sur les premiers contacts : WhatsApp bloque les numéros qui écrivent à beaucoup de gens sans avoir été sollicités, et c'est votre propre ligne professionnelle qui se retrouve bloquée. Sendveo autorise 20 nouvelles conversations par numéro et par heure, puis répond 429 OUTBOUND_LIMIT avec un Retry-After au-delà. Les réponses ne sont jamais comptées.

Pièces jointes

Un message peut porter jusqu'à 10 fichiers. Envoyez les octets en multipart/form-data, ou passez une url publique en JSON et Sendveo récupère le fichier pour vous. Fichiers et URL ne peuvent pas être mélangés dans un même message. text est facultatif dès lors que le message porte un fichier : une photo sans légende est un vrai message.

Dans une requête multipart, les fichiers passent par attachments (répétez le champ pour en envoyer plusieurs), et un fichier audio que vous voulez afficher sous forme d'onde va plutôt sur voice_note. Tout autre nom de champ de fichier renvoie 400.

envoi multipart
curl -X POST https://api.sendveo.com/v1/messages \
  -H "X-API-KEY: sv_live_your_key_here" \
  -F channelId=chn_sales01 \
  -F to=+212661234567 \
  -F "text=Voici le devis" \
  -F "attachments=@devis.pdf;type=application/pdf"
pièce jointe par URL
{
  "channelId": "chn_sales01",
  "chatId": "chat_aa11",
  "attachments": [
    {
      "url": "https://cdn.example.com/devis.pdf",
      "mimeType": "application/pdf",
      "filename": "devis.pdf"
    }
  ]
}

Limites

Ce sont les limites de WhatsApp, pas les nôtres : un fichier qui les dépasse est refusé par WhatsApp après l'envoi, donc Sendveo le refuse en amont et nomme la limite. Un type absent de la liste est refusé par son nom. Une erreur de pièce jointe est un 400 VALIDATION_FAILED avec details[0].path = "attachments.N.…" : un appelant qui envoie cinq fichiers sait lequel a été refusé, et rien n'atteint le service de messagerie.

TypeTypes de médiaPlafond
imageimage/jpeg image/png image/gif image/webp5 MB
videovideo/mp4 video/3gpp video/quicktime16 MB
audioaudio/mpeg audio/mp4 audio/aac audio/amr audio/ogg audio/wav16 MB
voice noteas audio, plus audio/opus audio/webm16 MB
documentapplication/pdf, the Office types, application/zip, application/json, text/plain, text/csv, text/vcard100 MB
stickerimage/webp500 KB

Télécharger une pièce jointe

GET/v1/messages/:id/attachments/:attId

Le point de terminaison renvoie les octets, avec la même authentification que le reste de l'API. C'est un proxy, jamais une redirection. Sendveo ne stocke aucun octet : le fichier est récupéré à la demande, transmis en flux puis oublié, donc chaque téléchargement est un nouvel appel et une pièce jointe n'est téléchargeable que tant que le service de messagerie la conserve. Un fichier que vous avez envoyé ne peut pas être retéléchargé (409) : ses métadonnées figurent sur le message, pour que votre propre interface affiche ce que vous avez envoyé.

Réponses et réactions

Citez un message avec replyTo : un identifiant Sendveo msg_…, ou l'identifiant fournisseur d'un message antérieur à la connexion, qui est accepté et transmis tel quel. Dans ce second cas, le message renvoyé porte replyTo avec id: null.

POST/v1/messages/:id/reactions
DELETE/v1/messages/:id/reactions

Une réaction est un seul emoji natif. Du texte donne 400. Réagir à nouveau remplace votre réaction, cela n'en ajoute jamais une deuxième, et { "emoji": null } (ou DELETE …/reactions, la même opération) la retire. Le champ reactions d'un message porte les réactions en vigueur à l'instant, pas un journal ; la réaction du numéro connecté apparaît avec by: null.

ajouter une réaction
curl -X POST https://api.sendveo.com/v1/messages/msg_8Rm5/reactions \
  -H "X-API-KEY: sv_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "emoji": "👍" }'

Transfert et suppression

POST/v1/messages/:id/forward
DELETE/v1/messages/:id

Un transfert répond 201 avec la copie enregistrée dans la conversation de destination, si bien que votre propre boîte de réception montre ce que vous avez envoyé. { to: "+212661234567" } transfère dans la conversation que vous avez déjà avec ce numéro ; un transfert vers un numéro avec lequel il n'existe aucune conversation renvoie 409, et non une nouvelle conversation : faire d'un transfert un premier contact à froid dépenserait la réputation du numéro sans que vous l'ayez demandé.

transférer un message
curl -X POST https://api.sendveo.com/v1/messages/msg_8Rm5/forward \
  -H "X-API-KEY: sv_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "chatId": "chat_aa11" }'

La suppression retire le message chez WhatsApp. Côté Sendveo, la ligne est conservée : text est vidé, attachments devient [] et deletedAt est renseigné, de sorte qu'un historique montre encore que quelque chose a été dit puis retiré. Supprimer un message déjà supprimé renvoie 409.

Réagir, transférer, supprimer pour tout le monde et marquer comme lu exigent la portée messages:manage, qui ne fait délibérément PAS partie de messages:send : agir sur un message qui existe déjà est une autorité différente que d'en écrire un nouveau. Les clés émises avant l'existence de cette portée ne la portent pas et répondent 403 : émettez une nouvelle clé plutôt que de modifier l'ancienne. POST /v1/chats/:chatId/typing ne demande que messages:send.

Positions et contacts

Une position est stockée de façon structurée et renvoyée sous forme de nombres dans les deux sens. Une valeur hors plage donne 400 en nommant location.latitude ou location.longitude. Votre text est conservé au-dessus.

Chaque téléphone d'une fiche de contact est normalisé selon la même règle qu'un destinataire, et une forme nationale commençant par un zéro est refusée. Les fiches de contact entrantes sont réanalysées en noms et en numéros E.164 pour vous. Une position et des contacts ne peuvent pas voyager sur le même message.

position et contacts
{
  "channelId": "chn_sales01",
  "chatId": "chat_aa11",
  "text": "On est ici",
  "location": {
    "latitude": 33.5731,
    "longitude": -7.5898,
    "name": "Casablanca",
    "address": "Boulevard Zerktouni"
  }
}

{
  "channelId": "chn_sales01",
  "chatId": "chat_aa11",
  "contacts": [
    { "name": "Nadia Benali", "phones": ["+212661778899"], "organisation": "Atlas" }
  ]
}

linkPreview vaut true par défaut : le premier lien http(s) du texte est renvoyé sous la forme linkPreview: { url }, ce qui vous évite de réanalyser le corps du message. Sendveo ne récupère jamais le lien : pas de titre, pas d'image, aucune requête sortante par envoi.

Ce que l'option ne fait pas : la carte d'aperçu que voit un destinataire est dessinée par son client WhatsApp à partir de l'URL présente dans le texte, et aucune primitive ne permet de la supprimer. linkPreview: false empêche seulement Sendveo d'extraire le lien ; cela ne supprime pas la carte de WhatsApp, et le texte n'est jamais réécrit pour essayer.

Marquer comme lu et saisie

POST/v1/chats/:chatId/read
POST/v1/chats/:chatId/typing

Marquer une conversation comme lue et afficher un indicateur de saisie tiennent chacun en un seul appel. L'indicateur de saisie est « au mieux » par contrat : il répond 200 { "sent": false } lorsque le service de messagerie le refuse, plutôt que de faire échouer votre requête ; une politesse ne doit pas faire échouer un appelant. Ce qui reste une erreur, c'est votre propre faute : une conversation inconnue (404), un numéro non connecté (409), une durée hors de 1-25000 ms (400).

Statut de remise

Sur un message sortant, status parcourt SENT → DELIVERED → READ, et il ne progresse que vers l'avant : les accusés arrivent dans le désordre, et un DELIVERED tardif ne rend jamais un message non lu. FAILED l'emporte sur tout et rien ne l'emporte sur lui. statusAt indique le dernier changement, et failureReason n'est renseigné qu'avec FAILED.

Lister les messages

Filtrez par numéro, direction et date. Les résultats sont classés du plus récent au plus ancien et limités à 200 par page.

GET/v1/messages?channelId=chn_sales01&direction=INBOUND&limit=50
cURL
curl "https://api.sendveo.com/v1/messages?channelId=chn_sales01&direction=INBOUND&limit=50" \
  -H "X-API-KEY: sv_live_your_key_here"
Envoyer et recevoir des messages WhatsApp · Sendveo