Aller au contenu

Documentation API

WhatsApp et SMS sur une seule API : vous choisissez le canal, le template et le numéro, NG Messaging s'occupe du reste.

Démarrage rapide

Toute requête part de la même URL de base https://api.ng-messaging.com. Créez une clé API dans Développeurs → Clés API, puis envoyez un premier message. L'exemple utilise une clé de test : le message part réellement mais n'est jamais facturé.

cURL
curl https://api.ng-messaging.com/v1/messages \
  -H "X-NG-Api-Key: ngmsg_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "whatsapp",
    "to": "+221771234567",
    "body": "Bonjour depuis NG Messaging"
  }'

WhatsApp : texte libre ou template

Un texte libre WhatsApp n'est accepté que si le destinataire vous a écrit dans les 24 dernières heures (voir Fenêtre de 24 h). Pour écrire en premier, envoyez un template approuvé (voir Templates WhatsApp). Le SMS n'a pas cette contrainte.

La réponse est un 202 Accepted : le message est validé, débité (sauf clé de test) et mis en file avec le statut queued. L'envoi réel se fait ensuite ; suivez le statut par webhook ou avec GET /v1/messages/:id. Chaque réponse porte un en-tête X-Request-Id à communiquer au support.

Réponse à un SMS (clé de test, extrait : le champ unit_price_fcfa est omis) :

json
{
  "id": "8f6c1d9e-3a52-4b7f-9d21-5c0e7b44a1f0",
  "channel": "sms",
  "type": "simple",
  "destination": "+221771234567",
  "body": "Votre code est 482913",
  "status": "queued",
  "cost_fcfa": 0,
  "is_test": true,
  "gateway_message_id": null,
  "error_message": null,
  "error_code": null,
  "action_required": false,
  "correlation_id": null,
  "sender": "NG TECH",
  "country": "Senegal",
  "segments": 1,
  "encoding": "GSM-7",
  "scheduled_at": null,
  "sender_id": null,
  "created_at": "2026-10-23T10:00:00"
}

Réponse à un message WhatsApp (clé de test) :

json
{
  "id": "5d2b7c40-91e3-4a6f-8c1d-0b7e3f9a2c11",
  "channel": "whatsapp",
  "type": "simple",
  "destination": "+221771234567",
  "body": "Bonjour depuis NG Messaging",
  "status": "queued",
  "cost_fcfa": 0,
  "is_test": true,
  "gateway_message_id": null,
  "error_message": null,
  "error_code": null,
  "action_required": false,
  "correlation_id": null,
  "sender": null,
  "country": null,
  "segments": null,
  "encoding": null,
  "scheduled_at": null,
  "sender_id": null,
  "created_at": "2026-10-23T10:00:00"
}

cost_fcfa est le montant réellement débité : 0 avec une clé de test. Les champs sender, country, segments, encoding et unit_price_fcfa concernent le SMS (null en WhatsApp) ; sender_id indique le numéro WhatsApp utilisé. gateway_message_id vaut toujours null : l'identifiant à conserver et à communiquer au support est id.

Authentification et clés API

Chaque requête porte votre clé dans l'en-tête X-NG-Api-Key. Une clé absente, mal formée, invalide ou révoquée reçoit 401.

header
X-NG-Api-Key: ngmsg_live_xxxxxxxxxxxxxxxx
PréfixeEnvironnementComportement
ngmsg_test_testLe message part réellement mais n'est jamais facturé (cost_fcfa vaut 0, is_test vaut true). Le volume quotidien est limité : 20 messages par jour par défaut, SMS et WhatsApp confondus, au-delà 429. NG Messaging peut relever cette limite pour votre organisation.
ngmsg_live_productionChaque envoi débite votre solde. L'adresse email du propriétaire du compte doit être confirmée, sinon 403.

Il n'y a pas d'URL distincte pour les tests : le préfixe de la clé suffit. La valeur complète d'une clé n'est affichée qu'une seule fois, à sa création. Ne la placez jamais dans un navigateur ou une application mobile : elle appartient à votre serveur.

Permissions

Une clé est créée avec une liste de permissions. Une requête sans la permission requise reçoit 403, avec le nom de la permission manquante.

PermissionTypeOuvre
sms:sendpermissionPOST /v1/messages avec channel « sms », POST /v1/sms/estimate, POST /v1/messages/:id/cancel
whatsapp:sendpermissionPOST /v1/messages avec channel « whatsapp », les templates (/v1/templates) et GET /v1/senders
otp:sendpermissionPOST /v1/otp et POST /v1/otp/verify
reportspermissionGET /v1/messages, GET /v1/messages/:id, GET /v1/webhooks/deliveries et son rejeu
campaignspermissionAcceptée à la création d'une clé, mais aucune route publique ne l'exige pour le moment.
contactspermissionAcceptée à la création d'une clé, mais aucune route publique ne l'exige pour le moment.

Clé liée à une application

Une clé peut être rattachée à une application de votre organisation. Elle n'utilise alors que les numéros WhatsApp de cette application pour choisir l'expéditeur (voir Expéditeurs WhatsApp). Une clé sans rattachement peut employer tous les numéros de l'organisation.

Envoyer un message

Un seul endpoint envoie les messages WhatsApp et SMS. Le champ channel décide du reste, et la permission exigée sur la clé suit le canal (whatsapp:send ou sms:send).

POST/v1/messages202 Accepted
ChampTypeDescription
channelstring?« whatsapp » (défaut) ou « sms ».
tostringNuméro au format international, par exemple +221771234567 ou 221771234567.
bodystring?Texte du message. Requis pour un message libre, inutile pour un template.
typestring?« text » (défaut) ou « template ». La présence de template suffit à envoyer un template.
templateobject?WhatsApp : { name, language, variables }. language vaut « fr » par défaut ; variables est une liste ou un objet, dont les valeurs sont prises dans l'ordre {{1}}, {{2}}….
senderstring?SMS : signature d'envoi. « NG TECH » ou la signature approuvée de votre compte, sinon 400. Ignoré en WhatsApp.
sender_idstring?WhatsApp : numéro expéditeur (snd_… ou snd_ngtech), voir Expéditeurs WhatsApp. Ignoré en SMS.
scheduled_atdatetime?SMS seulement : heure d'envoi programmée, en ISO 8601 (sans fuseau, l'heure est lue en UTC). Entre 1 minute et 90 jours dans le futur. En WhatsApp, 400.
idempotency_keystring?Équivalent du corps de l'en-tête Idempotency-Key, voir la section Idempotence.
metadataobject?Objet libre. Il n'est pas restitué dans la réponse.
image_url / document_urlstring?L'envoi de médias n'est pas disponible : une requête qui en contient reçoit 400.
cURL
curl https://api.ng-messaging.com/v1/messages \
  -H "X-NG-Api-Key: ngmsg_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "whatsapp",
    "to": "+221771234567",
    "sender_id": "snd_ngtech",
    "body": "Bonjour depuis NG Messaging"
  }'

Envoyer un template WhatsApp

cURL
curl https://api.ng-messaging.com/v1/messages \
  -H "X-NG-Api-Key: ngmsg_live_xxx" \
  -H "Idempotency-Key: order-1042-confirmation" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "whatsapp",
    "to": "+221771234567",
    "sender_id": "snd_ngtech",
    "type": "template",
    "template": { "name": "order_confirmed", "language": "fr", "variables": ["Aïda", "ORD-1042"] }
  }'

Programmer un SMS

Le SMS est débité tout de suite et reste au statut scheduled jusqu'à l'heure prévue. Tant qu'il n'est pas parti, vous pouvez l'annuler avec POST /v1/messages/:id/cancel (permission sms:send) : il passe à cancelled et son débit est remboursé. Si le message est déjà transmis ou en cours d'envoi, la réponse est 409.

cURL
curl https://api.ng-messaging.com/v1/messages \
  -H "X-NG-Api-Key: ngmsg_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "to": "+221771234567",
    "body": "Rappel : votre rendez-vous est demain à 10 h.",
    "scheduled_at": "2026-11-02T08:00:00Z"
  }'

Longueur et segments SMS

Un SMS en alphabet GSM-7 tient dans un segment jusqu'à 160 caractères, puis 153 caractères par segment. Un seul caractère hors GSM-7 (emoji, certains accents) bascule tout le message en UCS-2 : 70 caractères pour un segment, puis 67. Le coût suit le nombre de segments. Avec la signature NG TECH, le nom de votre entreprise est ajouté en tête du message et compte dans la longueur. POST /v1/sms/estimate (permission sms:send, champs to, message, sender) renvoie le nombre de segments, l'encodage et le coût sans rien débiter ni envoyer.

Lire un message

GET/v1/messages/:idpermission reports

Renvoie le message et sa chronologie (timeline), horodatée. En cas d'échec, error_code et error_message portent le code et le message publics ; action_required vaut true quand rien ne se résoudra sans une action de votre part. Un identifiant inconnu reçoit 404.

json
{
  "id": "8f6c1d9e-3a52-4b7f-9d21-5c0e7b44a1f0",
  "channel": "sms",
  "status": "delivered",
  "...": "(les champs de la réponse d'envoi)",
  "timeline": [
    { "status": "queued", "at": "2026-10-23T10:00:00" },
    { "status": "sent", "at": "2026-10-23T10:00:02" },
    { "status": "delivered", "at": "2026-10-23T10:00:05" }
  ]
}
GET/v1/messagespermission reports

Liste paginée : paramètres channel, status, page (1 par défaut) et limit (50 par défaut, 200 au maximum). La réponse contient items et total.

Expéditeurs WhatsApp

Une organisation peut avoir plusieurs numéros WhatsApp. Chacun est identifié publiquement par un sender_id (snd_…). Le numéro mutualisé de NG TECH, quand votre organisation y a droit, a toujours l'identifiant snd_ngtech.

GET/v1/senderspermission whatsapp:send
ChampTypeDescription
sender_idstringÀ passer dans sender_id à l'envoi.
typestring« own » (votre numéro) ou « ngtech » (numéro NG TECH).
phone_numberstring?Numéro affiché ; null pour le numéro NG TECH.
display_namestring?Nom d'affichage.
statusstringSeuls les numéros actifs sont listés.
is_defaultbooleanExpéditeur par défaut de l'organisation.
json
{
  "senders": [
    { "sender_id": "snd_8c41f0a2", "type": "own", "phone_number": "+221770000000",
      "display_name": "Ma Boutique", "status": "active", "is_default": true },
    { "sender_id": "snd_ngtech", "type": "ngtech", "phone_number": null,
      "display_name": "NG TECH", "status": "active", "is_default": false }
  ]
}

Règle de sélection

En WhatsApp, l'expéditeur est choisi dans cet ordre :

  1. Le sender_id de la requête, s'il appartient à votre organisation et est actif (ou snd_ngtech si l'organisation y a droit). Sinon 404 sender_not_found ou 409 sender_unavailable.
  2. Sinon l'expéditeur par défaut de l'organisation, s'il est défini. Défini mais inactif : 409 sender_unavailable, sans repli.
  3. Sinon l'unique numéro actif de l'organisation.
  4. Sinon, plusieurs numéros actifs : 400 sender_required. Précisez sender_id ou définissez un expéditeur par défaut.
  5. Sinon, aucun numéro à vous n'est actif : le numéro NG TECH est utilisé si votre organisation y a droit.

Jamais de bascule implicite vers le numéro NG TECH

Si l'un de vos numéros existe mais est déconnecté ou suspendu, l'envoi est refusé en 409 sender_unavailable : il ne part pas depuis le numéro NG TECH à votre insu. Si l'accès de votre organisation au numéro NG TECH est fermé, vous recevez un code ngtech_* en 403 (voir Erreurs).

Le message renvoyé indique l'expéditeur réellement utilisé dans sender_id. Sur le numéro NG TECH, le destinataire voit NG TECH comme expéditeur : le texte libre est donc précédé du nom de votre entreprise. Pour le SMS, la signature se choisit avec le champ sender.

Fenêtre de 24 h

WhatsApp n'autorise un texte libre que dans les 24 heures qui suivent le dernier message reçu du destinataire sur ce numéro. Chaque message entrant rouvre une fenêtre de 24 heures ; vous êtes prévenu par l'événement message.received.

SituationTypeCe que vous pouvez envoyer
Le destinataire a écrit il y a moins de 24 hfenêtre ouverteTexte libre ou template.
Aucun message reçu, ou plus de 24 hfenêtre ferméeUn template approuvé uniquement. Un texte libre est refusé avant tout débit : 400 service_window_closed.

Sur le numéro mutualisé NG TECH, un message reçu est attribué à l'organisation qui a envoyé le dernier message sortant à ce destinataire depuis ce numéro, dans les 30 derniers jours. La fenêtre n'est ouverte que pour cette organisation.

Statuts d'un message

StatutTypeSignification
queuedstatutAccepté et débité, en attente d'envoi.
processingstatutEn cours de transmission à WhatsApp ou à l'opérateur.
sentstatutPris en charge par WhatsApp ou l'opérateur. Ce n'est pas une preuve de réception.
deliveredstatutRemis sur le téléphone du destinataire.
readstatutLu (WhatsApp, si le destinataire a activé les accusés de lecture).
failedstatutÉchec définitif ; error_code donne la cause. Le message est remboursé.
unknownajoutL'issue de l'envoi est incertaine (code outcome_unknown) : le message a peut-être été remis. Il n'est ni renvoyé ni remboursé, et aucun webhook n'est émis à ce moment.
scheduledajoutSMS programmé, débité, qui attend son heure d'envoi.
cancelledajoutSMS programmé annulé avant son envoi, remboursé.

Règles

  • La progression normale est queued, processing, sent, delivered, read. Un accusé tardif n'écrase jamais un statut plus avancé : un sent reçu après delivered est ignoré.
  • « Livré » l'emporte : un échec signalé après delivered ou read est ignoré et ne donne lieu à aucun remboursement.
  • Un failed est remboursé, une seule fois. Si un accusé delivered arrive ensuite, le statut redevient delivered et l'erreur disparaît, sans nouveau débit.
  • Un message unknown n'est pas remboursé tant que son issue n'est pas connue : un accusé failed ultérieur le rembourse, un accusé delivered ou read le laisse facturé. Un message que le réseau n'a pas pu remettre dans le délai passe à failed avec le code message_expired (remboursé).
  • Les statuts scheduled et cancelled ne sont modifiés par aucun accusé du réseau.

Le SMS ne remonte pas read. Pour suivre un message, abonnez-vous aux webhooks message.sent, message.delivered, message.read et message.failed, ou interrogez GET /v1/messages/:id.

Templates WhatsApp

WhatsApp exige un template approuvé par Meta pour écrire en premier à un contact ou hors de la fenêtre de 24 h. Créez-le ici ou depuis Messagerie → Templates, attendez la décision de Meta (le délai est fixé par Meta ; vous êtes prévenu par les événements template.approved et template.rejected), puis envoyez-le avec POST /v1/messages. Tous les endpoints ci-dessous exigent la permission whatsapp:send.

POST/v1/templates201 Created, statut pending
ChampTypeDescription
namestringMinuscules, chiffres et _ ; commence par une lettre (100 caractères au plus).
languagestring?Code langue, « fr » par défaut.
categorystringUTILITY, MARKETING ou AUTHENTICATION. Elle détermine le tarif.
bodystringTexte de 1 024 caractères au plus. Variables {{1}}, {{2}}… sans trou, sans variable en première ni en dernière position.
examplestring?Message complet d'exemple, requis dès que le corps contient des variables.
header / footerstring?En-tête et pied de message.
buttonsobject[]?Boutons du template.
code_expiration_minutesnumber?AUTHENTICATION : de 1 à 90 minutes. Le corps contient alors exactement une variable, {{1}}, le code.
sender_idstring?Numéro (snd_…) auprès duquel créer le template. Sans lui, la règle de sélection de l'expéditeur s'applique.
cURL
curl https://api.ng-messaging.com/v1/templates \
  -H "X-NG-Api-Key: ngmsg_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "order_confirmed",
    "language": "fr",
    "category": "UTILITY",
    "body": "Bonjour {{1}}, votre commande {{2}} est confirmée. Merci de votre confiance.",
    "example": "Bonjour Aïda, votre commande ORD-1042 est confirmée. Merci de votre confiance."
  }'

Réponse (le champ status vaut ensuite approved, rejected, paused, disabled ou deleted) :

json
{
  "id": "…",
  "name": "order_confirmed",
  "language": "fr",
  "category": "UTILITY",
  "status": "pending",
  "body": "Bonjour {{1}}, votre commande {{2}} est confirmée. Merci de votre confiance.",
  "rejection_reason": null
}
EndpointRéponseDescription
GET /v1/templates200Vos templates approuvés, les seuls utilisables pour l'envoi. Paramètre optionnel sender_id.
GET /v1/templates/:name200 / 404Un template quel que soit son statut, avec rejection_reason en cas de refus. name peut aussi être l'id.
DELETE /v1/templates/:name204 / 404Supprime le template.
POST /v1/templates/sync200Relit les statuts auprès de Meta ; renvoie { "status": "ok" }.
POST /v1/templates/send202Variante d'envoi d'un template (to, name, language, variables, sender_id, idempotency_key). POST /v1/messages reste le point d'entrée recommandé.

Templates du numéro NG TECH

Sur le numéro NG TECH (sender_id snd_ngtech), les templates forment un catalogue commun, activé pour votre organisation par NG Messaging. Vous ne pouvez ni les créer, ni les modifier, ni les supprimer : POST /v1/templates, DELETE et sync reçoivent 403. GET /v1/templates?sender_id=snd_ngtech liste ceux qui vous sont ouverts. Envoyer un template qui ne l'est pas donne 403 ngtech_template_not_allowed, et un pays non autorisé pour ce template 400 ngtech_country_not_allowed. Pour créer vos propres templates, connectez votre propre numéro WhatsApp.

Un template appartient à un numéro : approuvé sur un numéro, il n'existe pas sur un autre. Avec une clé de test, POST /v1/messages refuse les templates (400) : utilisez une clé live.

OTP

NG Messaging génère le code, l'envoie et le vérifie : votre application ne manipule qu'un identifiant de demande, request_id. Le code n'est jamais renvoyé par l'API et n'est conservé que sous forme d'empreinte. Les deux endpoints exigent la permission otp:send.

POST/v1/otp202 Accepted
ChampTypeDescription
tostringNuméro au format international.
channelstring?« whatsapp » (défaut), « sms » ou « auto ».
template / languagestring?WhatsApp : template d'authentification (sinon celui des réglages OTP de l'organisation) et sa langue, « fr » par défaut.
code_lengthnumber?De 4 à 10 chiffres (6 par défaut, ou le réglage de l'organisation).
expires_in_minutesnumber?Durée de validité, de 1 à 60 minutes (10 par défaut).
max_attemptsnumber?Tentatives de vérification, de 1 à 10 (5 par défaut).
request_idstring?Votre identifiant de demande ; généré s'il est absent. Le rejouer ne renvoie ni ne débite deux fois.
idempotency_keystring?Aussi acceptée dans l'en-tête Idempotency-Key ; sans request_id, elle en tient lieu.
codestring?Pour les intégrations qui génèrent leur propre code : il est livré tel quel et reste vérifiable. Sinon NG Messaging en génère un.
client_ipstring?Adresse IP (IPv4 ou IPv6) de l'utilisateur final. Elle active la limite par adresse IP, sans quoi seule l'IP de votre serveur serait vue.

Une valeur hors bornes est refusée en 400 avec le nom du champ et l'intervalle : elle n'est jamais corrigée en silence. Le canal auto choisit WhatsApp quand un numéro est utilisable et qu'un template d'authentification approuvé existe, et le SMS sinon. Avec une clé de test, le canal auto choisit le SMS. Par SMS, le message est composé par NG Messaging.

cURL
curl https://api.ng-messaging.com/v1/otp \
  -H "X-NG-Api-Key: ngmsg_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+221771234567", "channel": "auto" }'

Réponse (avec une clé de test, cost_fcfa vaut 0) :

json
{
  "request_id": "otp_3f8a1c92b7d4e6f0",
  "message_id": "9594319d-55ab-4a38-b921-34cda240e701",
  "status": "queued",
  "expires_at": "2026-10-23T10:10:00",
  "cost_fcfa": 0
}
POST/v1/otp/verify
cURL
curl https://api.ng-messaging.com/v1/otp/verify \
  -H "X-NG-Api-Key: ngmsg_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "request_id": "otp_3f8a1c92b7d4e6f0", "code": "438210" }'

Réponse 200 :

json
{
  "id": "…",
  "request_id": "otp_3f8a1c92b7d4e6f0",
  "phone_number": "+221771234567",
  "status": "verified",
  "attempts": 1,
  "max_attempts": 5,
  "expires_at": "2026-10-23T10:10:00",
  "verified_at": "2026-10-23T10:01:12",
  "message_id": "9594319d-55ab-4a38-b921-34cda240e701",
  "created_at": "2026-10-23T10:00:00"
}
HTTPSituationDétail
200valideCode correct ; l'événement otp.verified est émis.
400incorrectCode incorrect ; le message indique le nombre de tentatives restantes. Chaque échec consomme une tentative.
404inconnuAucune demande pour ce request_id.
409déjà vérifiéCe code a déjà été vérifié.
410expiréLe code a expiré : demandez-en un nouveau.
429trop de tentativesLe maximum de tentatives est atteint : la demande est close, il faut en envoyer une nouvelle.

Antifraude

Avant tout envoi et tout débit, NG Messaging applique des limites contre le détournement des codes. Un refus est une réponse 429, avec un en-tête Retry-After (en secondes) quand une attente a un sens, et un corps contenant error, message et retry_after. Aucun message n'est créé et rien n'est débité.

errorHTTPCause
number_cooldown429Un code vient d'être envoyé à ce numéro : délai minimum entre deux codes.
number_hourly429Trop de codes envoyés à ce numéro sur l'heure par votre organisation.
number_daily_global429Ce numéro a atteint sa limite de codes sur 24 h, toutes organisations confondues.
ip_hourly429Trop de codes demandés depuis la même adresse IP d'utilisateur final (si client_ip est fourni).
account_hourly / account_daily429Limite horaire ou quotidienne de codes de votre organisation.
international_hourly429Limite horaire des codes vers l'international.
spend_daily429Plafond de dépense OTP du jour atteint (clés live).
platform_international_spend_daily429Plafond de sécurité de la plateforme vers l'international atteint.
workspace_otp_suspended429Envoi d'OTP suspendu temporairement après une activité anormale (beaucoup de codes envoyés, très peu vérifiés).
country_blocked400Les codes vers cette destination ne sont pas autorisés. Pas de Retry-After.

Les seuils exacts sont fixés par NG Messaging et peuvent être ajustés pour votre organisation sur demande au support.

Si l'envoi échoue sur un canal, NG Messaging ne retente pas sur l'autre. Si la réception du code est critique pour votre parcours, proposez un bouton « recevoir par SMS ».

Webhooks

Configurez une URL HTTPS depuis Développeurs → Webhooks et choisissez les événements auxquels vous vous abonnez : un événement non coché n'est jamais envoyé. Le secret de l'endpoint (préfixe ngwhsec_) sert à vérifier la signature ; il se renouvelle depuis la même page. L'URL ne doit pas pointer vers un réseau interne et les redirections ne sont pas suivies.

Événements

ÉvénementTypeQuand
message.senteventLe message est pris en charge par WhatsApp ou l'opérateur.
message.deliveredeventLe message est remis au destinataire.
message.readeventLe destinataire l'a lu (WhatsApp uniquement).
message.failedeventÉchec, avec error_code et error_message ; le message est remboursé.
message.receivedeventUn contact vous a écrit sur WhatsApp. Le payload porte l'expéditeur du message (from), son type et son contenu, ainsi que sender_id, le numéro qui l'a reçu.
template.approved / template.rejectedeventDécision de Meta sur un template : name, language_code, reason.
otp.senteventUn code a été envoyé : request_id, message_id, to, channel, expires_at.
otp.verifiedeventUn code a été vérifié : request_id, to, verified_at.
campaign.completedeventUne campagne a fini d'envoyer : campaign_id, recipients, sent, failed, skipped, cost_fcfa.
wallet.low_balanceeventVotre solde passe sous le seuil d'alerte : balance_fcfa, threshold_fcfa.

Forme de la requête

Chaque livraison est un POST JSON dont data dépend de l'événement. Pour les événements message.*, data.message_id est l'identifiant renvoyé par l'envoi, et error_code / error_message ne sont renseignés qu'en cas d'échec (avec en plus error et errors).

json
{
  "id": "evt_6b79e8b422f9432e",
  "event_type": "message.delivered",
  "source": "ng_messaging",
  "version": "1.0",
  "company_id": "0e5ad9db-a30e-45e7-a69d-6daa476961a0",
  "occurred_at": "2026-10-23T10:00:05.103787Z",
  "received_at": "2026-10-23T10:00:05.103787Z",
  "correlation_id": "corr_61d340fcddac42ca",
  "data": {
    "message_id": "9594319d-55ab-4a38-b921-34cda240e701",
    "channel": "whatsapp",
    "to": "221771234567",
    "status": "delivered",
    "correlation_id": "7c9eb28e-8055-40a0-bd01-c9615cfa1d85",
    "error_code": null,
    "error_message": null,
    "timestamp": "2026-10-23T10:00:05.103787Z"
  }
}

En-têtes et signature

En-têteTypeContenu
X-NG-Signaturestring« sha256= » suivi du HMAC-SHA256 en hexadécimal.
X-NG-TimestampstringHorodatage Unix, en secondes.
X-NG-Event-IdstringIdentifiant de l'événement (evt_…), identique à id dans le corps.
X-NG-Delivery-AttemptnumberNuméro de la tentative de livraison, à partir de 1.

La signature est le HMAC-SHA256, avec le secret de l'endpoint comme clé, de {timestamp}.{corps brut}. Calculez-la sur les octets reçus, avant tout décodage JSON, et refusez les horodatages trop anciens.

Node.js
import crypto from "node:crypto"
import express from "express"

const app = express()

// Le corps BRUT est indispensable : ne le passez pas par express.json() avant la vérification.
app.post("/webhooks/ng-messaging", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.header("X-NG-Timestamp") ?? ""
  const signature = req.header("X-NG-Signature") ?? ""

  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400)

  const attendu =
    "sha256=" +
    crypto
      .createHmac("sha256", process.env.NG_WEBHOOK_SECRET)
      .update(Buffer.concat([Buffer.from(timestamp + "."), req.body]))
      .digest("hex")

  const a = Buffer.from(attendu)
  const b = Buffer.from(signature)
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401)

  res.sendStatus(200) // accusez réception d'abord, traitez ensuite
  const event = JSON.parse(req.body.toString("utf8"))
  // dédoublonnez sur req.header("X-NG-Event-Id") puis traitez event.event_type
})
Python
import hashlib
import hmac
import time

def verifier(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool:
    # Refuser un horodatage trop ancien : protège du rejeu d'un appel intercepté.
    if abs(time.time() - int(timestamp)) > 300:
        return False
    message = timestamp.encode() + b"." + raw_body
    attendu = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(attendu, signature)

Réponses, reprises et rejeu

Répondez par un code 2xx en moins de 8 secondes puis traitez l'événement. Toute autre réponse, ou une erreur réseau, est un échec : NG Messaging retente après 30 secondes, 2 minutes, 10 minutes, 1 heure puis 6 heures, soit 6 tentatives au total. Ensuite la livraison passe à l'état dead et reste consultable. Un même événement peut arriver plusieurs fois : dédoublonnez sur X-NG-Event-Id. Un échec de webhook n'affecte jamais l'envoi du message qui l'a déclenché.

GET/v1/webhooks/deliveriespermission reports

Journal des livraisons, les plus récentes d'abord. Paramètres : status (pending, delivered ou dead) et limit (50 par défaut, 200 au maximum). Chaque entrée porte id, event_id, event_type, status, attempts, last_http_status, last_error, next_attempt_at et delivered_at.

POST/v1/webhooks/deliveries/:id/retrypermission reports

Rejoue une livraison avec un compteur de tentatives remis à zéro. Une livraison inconnue reçoit 404, et une livraison déjà réussie 409.

Idempotence

Une clé d'idempotence garantit qu'une même opération métier crée un seul message et un seul débit, même si votre code réessaie après un délai d'attente. Elle s'applique à tous les envois : messages, templates et OTP. Transmettez-la dans l'en-tête Idempotency-Key ou dans le champ idempotency_key du corps.

  • Rejouer la même clé ne renvoie pas d'erreur : l'API retourne le message déjà créé, sans rien renvoyer ni débiter de nouveau.
  • En-tête et corps fournis ensemble avec deux valeurs différentes : 400 idempotency_key_mismatch.
  • Un message refusé faute de solde (402) ne réserve pas sa clé : après avoir rechargé, vous pouvez réessayer avec la même.
  • Pour un OTP, la clé détermine le request_id quand vous n'en fournissez pas : un rejeu ne renvoie pas de nouveau code.

Choisissez un identifiant stable et propre à l'opération (un numéro de commande), pas un horodatage.

Codes d'erreur

Toute erreur de l'API a le même corps JSON : detail (chaîne, objet ou liste, selon l'erreur), error (code stable sur lequel brancher votre code), message (texte lisible, en français) et request_id. Ce dernier est aussi renvoyé dans l'en-tête X-Request-Id : communiquez-le au support.

json
{
  "detail": "Hors fenêtre de 24 h : ce destinataire ne vous a pas écrit récemment. Utilisez un template approuvé.",
  "error": "service_window_closed",
  "message": "Hors fenêtre de 24 h : ce destinataire ne vous a pas écrit récemment. Utilisez un template approuvé.",
  "request_id": "3f9a0c7e41b84d2e9a1c5f60"
}

Solde insuffisant (402)

Un solde insuffisant est une réponse 402 avec le code insufficient_balance. Selon le circuit, detail est un objet qui donne le montant requis et le solde disponible, ou une chaîne.

json
{
  "detail": { "error": "insufficient_balance", "required": 15, "available": 4 },
  "error": "insufficient_balance",
  "message": "Solde insuffisant.",
  "request_id": "3f9a0c7e41b84d2e9a1c5f60"
}

Codes par statut HTTP

Quand une erreur n'a pas de code public propre, error reprend le code de son statut HTTP :

HTTPerrorSignification
400bad_requestRequête invalide : le message nomme la cause (pays fermé, signature inconnue, valeur hors bornes, média non disponible…).
401unauthorizedClé API absente, de format invalide, invalide ou révoquée.
402insufficient_balanceSolde insuffisant. Le message n'est pas envoyé.
403forbiddenPermission manquante sur la clé, compte suspendu, adresse email non confirmée, ou accès au numéro NG TECH fermé (codes ngtech_*).
404not_foundRessource introuvable : message, template, livraison ou expéditeur.
409conflictConflit avec l'état de la ressource : aucun canal WhatsApp actif, expéditeur indisponible, SMS déjà parti, OTP déjà vérifié.
410goneCode OTP expiré.
422validation_errorCorps mal formé ou champ manquant : le message nomme le champ en cause.
429rate_limitedLimite atteinte : clé de test, numéro NG TECH, ou antifraude OTP. Respectez Retry-After quand il est présent.
500internal_errorErreur interne. Communiquez le request_id au support.
502 / 503messaging_temporarily_unavailableService d'envoi momentanément indisponible : réessayez avec la même clé d'idempotence.

Codes publics

Ces codes décrivent un refus de votre requête (colonne HTTP renseignée) ou l'échec d'un message après la réponse 202. Dans ce second cas, vous les lisez dans error_code et error_message sur GET /v1/messages/:id et dans l'événement message.failed ; le statut HTTP ne s'applique pas. Le message est toujours celui du tableau, jamais un texte brut d'un tiers. Un code absent du tableau doit être traité comme messaging_temporarily_unavailable.

CodeHTTPMessage
invalid_recipient-Le numéro du destinataire est invalide.
recipient_unreachable-Le destinataire est injoignable avec ce canal.
country_not_supported-L'envoi vers ce pays n'est pas disponible.
sender_not_approved-L'expéditeur (signature) n'est pas approuvé pour cet envoi.
template_not_found-Le template demandé est introuvable.
template_rejected-Le template a été refusé.
template_invalid-Le template ou ses variables sont invalides.
content_rejected-Le contenu du message a été refusé.
insufficient_balance402Solde insuffisant pour envoyer ce message.
rate_limited-Cadence d'envoi trop élevée. Réessayez dans un instant.
outcome_unknown-Statut en cours de vérification : le message a peut-être été remis.
message_expired-Le message n'a pas pu être remis dans le délai imparti.
messaging_temporarily_unavailable-Service d'envoi momentanément indisponible. Réessayez plus tard.
internal_error-Erreur interne. Le message n'a pas pu être envoyé.
sender_required400Plusieurs numéros WhatsApp sont actifs : précisez sender_id ou définissez un expéditeur par défaut.
sender_not_found404Expéditeur introuvable : ce sender_id n'existe pas pour votre organisation.
sender_unavailable409Cet expéditeur n'est pas disponible (numéro déconnecté ou suspendu). Reconnectez-le ou choisissez un autre expéditeur.
service_window_closed400Hors fenêtre de 24 h : ce destinataire ne vous a pas écrit récemment. Utilisez un template approuvé.
ngtech_not_authorized403Votre organisation n'est pas autorisée à utiliser le numéro NG TECH.
ngtech_disabled403Le numéro NG TECH n'est pas disponible pour le moment.
ngtech_suspended403L'accès de votre organisation au numéro NG TECH est suspendu. Contactez le support.
ngtech_template_not_allowed403Ce template n'est pas disponible pour votre organisation.
ngtech_country_not_allowed400Ce template n'est pas autorisé vers ce pays.
ngtech_rate_limited429Trop d'envois par minute sur le numéro NG TECH, réessayez dans un instant.

Codes Meta normalisés

Ces codes en majuscules concernent votre compte WhatsApp Business. action_required vaut true sur le message quand aucun réessai ne servira tant que vous n'avez rien changé.

CodeHTTPMessageAction requise
TEMPLATE_OUTSIDE_WINDOW-Texte libre impossible hors de la fenêtre de 24 h : utilisez un template approuvé.non
RECIPIENT_UNREACHABLE-Numéro invalide, ou sans compte WhatsApp.non
PAYMENT_METHOD_ISSUE-Le moyen de paiement du compte WhatsApp Business est en défaut.oui
BUSINESS_VERIFICATION_REQUIRED-L'entreprise doit être vérifiée par Meta.oui
ACCOUNT_RESTRICTED-Le compte WhatsApp Business est restreint.oui
RATE_LIMIT_HIT-Cadence d'envoi trop élevée. Ralentissez puis réessayez.non
INVALID_PARAMETER-Un paramètre du message a été refusé (variables ou format du template).non
INVALID_ACCESS_TOKEN-La connexion WhatsApp doit être renouvelée : reconnectez le numéro.oui
META_API_ERROR-Le message a été refusé par WhatsApp.non

Autres codes

errorHTTPCas
idempotency_key_mismatch400L'en-tête Idempotency-Key et le champ idempotency_key du corps ont des valeurs différentes.
validation_error422Corps mal formé ou champ manquant.
codes antifraude OTP429 / 400Voir la section OTP.

Réessayer proprement

Seuls 429, 502 et 503 justifient un nouvel essai, avec un délai croissant et la même clé d'idempotence pour ne pas créer de doublon. Un 400, 401, 402, 403, 404 ou 422 se corrige dans la requête ou dans le compte.

Limites et quotas

Voici ce que l'API applique. Les valeurs par défaut peuvent être ajustées pour votre organisation par NG Messaging.

LimiteRéponseDétail
Clé de test429Volume quotidien limité : 20 messages par jour par défaut, SMS et WhatsApp confondus. Les messages de test ne sont jamais facturés.
Clé live, email non confirmé403L'adresse email du propriétaire du compte doit être confirmée pour tout envoi réel.
Numéro NG TECH, quota quotidien429Plafond de messages par jour et par organisation sur le numéro mutualisé (50 par défaut). Il se lève en connectant votre propre numéro WhatsApp. Un échec remboursé avant tout envoi ne consomme pas le quota.
Numéro NG TECH, débit429 ngtech_rate_limitedNombre maximal de messages par minute et par organisation (60 par défaut). Réessayez dans un instant.
Template du numéro NG TECH429Certains templates du catalogue ont leur propre plafond quotidien par organisation.
OTP429 / 400Délai entre deux codes vers un même numéro, plafonds par numéro, par adresse IP, par organisation et vers l'international, plafond de dépense, suspension en cas d'activité anormale. Voir la section OTP.
SMS programmé400Entre 1 minute et 90 jours dans le futur.
Pays400Un pays fermé est refusé avant tout débit, avec la liste des pays ouverts pour le SMS.

Indépendamment de NG Messaging, Meta applique ses propres limites à chaque numéro WhatsApp : palier d'envoi, note de qualité, cadence. Un envoi trop rapide donne le code RATE_LIMIT_HIT.

Tarifs

Les tarifs dépendent du canal, de la destination et de la catégorie. La grille en vigueur est publiée sur la page Tarifs ; elle est aussi lisible sans clé avec GET /v1/billing/message-pricing. Aucun prix n'est donné ici pour ne jamais diverger de la facturation.

  • Montant débité. Le coût de chaque message est fixé à l'acceptation et renvoyé dans cost_fcfa ; il vaut 0 avec une clé de test. Un message en échec est remboursé.
  • SMS. Le prix suit le pays et le nombre de segments (voir Longueur et segments SMS). La signature utilisée compte aussi : NG TECH ou la signature approuvée de votre compte. POST /v1/sms/estimate donne le devis exact sans rien débiter.
  • WhatsApp. Le prix suit la catégorie du message :
CatégorieTypeUsage
marketingtemplatePromotions, relances, annonces.
utilitytemplateSuite d'une action du client : confirmation de commande, statut de livraison.
authenticationtemplateCodes à usage unique (OTP).
servicetexte libreRéponse en texte libre dans la fenêtre de 24 h qui suit un message du destinataire.

La catégorie d'un template est celle que Meta lui attribue, pas celle que vous espérez : déclarer un message promotionnel comme utility conduit Meta à le reclasser. Le tarif appliqué suit la catégorie réelle.