Suivi

Webhooks de livraison avec signatures

Définissez un endpoint HTTPS dans la page Webhooks de la console. Quand un message atteint un statut final, nous envoyons une requête POST avec un événement JSON à cet endpoint.

Événements

typeQuand
message.deliveredLe message a été livré.
message.failedLe message a échoué ; data.error contient le code. Son coût a été remboursé.
POST to your endpointJSON
{
  "id": "evt_8F3K2Q9TZ1MXD",
  "type": "message.delivered",
  "created_at": "2026-11-07T09:30:17.902Z",
  "data": {
    "id": "msg_8F3K2Q9TZ1MX",
    "object": "message",
    "batch_id": "bat_4M7Q1X0PK2RS",
    "to": "+14155550123",
    "country": "US",
    "text": "Your Northwind code is 482913. It expires in 10 minutes.",
    "segments": 1,
    "price": "0.00507",
    "currency": "USD",
    "status": "delivered",
    "error": null,
    "reference": "login-7781",
    "created_at": "2026-11-07T09:30:12.418Z",
    "sent_at": "2026-11-07T09:30:13.020Z",
    "done_at": "2026-11-07T09:30:17.902Z"
  }
}

En-têtes et signature

Chaque appel comporte trois en-têtes, au format Standard Webhooks :

En-têteValeur
webhook-idL'identifiant de l'événement (evt_...). Le même identifiant est utilisé pour chaque nouvelle tentative d'un même événement.
webhook-timestampHorodatage Unix en secondes lorsque cette tentative a été signée.
webhook-signaturev1, suivi de la signature HMAC-SHA256 encodée en base64. Plusieurs signatures peuvent être listées, séparées par des espaces : acceptez l'appel si l'une d'elles correspond.

Pour vérifier un appel : prenez le secret depuis la console (il commence par whsec_), décodez en base64url la partie après whsec_ pour obtenir la clé, calculez HMAC-SHA256 de webhook-id + "." + webhook-timestamp + "." + raw body, encodez-le en base64 et comparez-le avec la signature en temps constant. Rejetez les horodatages éloignés de plus de cinq minutes de votre horloge. Utilisez toujours le corps brut : un corps JSON parsé et resérialisé ne correspondra pas.

// Express: keep the raw body, check the signature, then answer 2xx quickly
import crypto from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.BULKSMSAPI_WEBHOOK_SECRET; // whsec_...

app.post("/hooks/sms", express.raw({ type: "application/json" }), (req, res) => {
  const id = req.header("webhook-id");
  const ts = req.header("webhook-timestamp");
  const key = Buffer.from(secret.slice("whsec_".length), "base64url");
  const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${req.body}`).digest("base64");
  const sent = (req.header("webhook-signature") || "").split(" ").map((s) => s.replace(/^v1,/, ""));
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  if (!fresh || !sent.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)))) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body);
  console.log(event.type, event.data.id, event.data.status);
  res.status(204).end();
});
app.listen(3000);

Réponse et nouvelles tentatives

  • Répondez avec un statut 2xx dans les 10 secondes. Effectuez le travail lent après avoir répondu, depuis une file d'attente.
  • Les redirections ne sont pas suivies : pointez le paramètre vers l'adresse finale.
  • Un appel qui expire ou répond avec autre chose qu'un statut 2xx est réessayé après 30 secondes, 2 minutes, 10 minutes, 30 minutes, 1 heure, 2 heures, 4 heures, 8 heures, 8 heures : 10 tentatives au total, sur environ 24 heures à partir de la première.
  • Chaque tentative est listée sur la page Webhooks avec le statut HTTP et le temps qu'elle a pris.

Doublons et ordre

Un événement peut arriver deux fois, par exemple lorsque votre endpoint l'a enregistré mais que la réponse a été perdue. Stockez webhook-id et ignorez les identifiants que vous avez déjà traités. Les événements pour différents messages peuvent arriver dans n'importe quel ordre ; lisez data.status et data.done_at plutôt que de vous fier à l'ordre d'arrivée.

Règles des endpoints

  • HTTPS uniquement, sur un nom d'hôte public (pas d'adresses IP littérales, localhost ou réseaux privés).
  • Le secret peut être remplacé sur la page Webhooks à tout moment ; les appels signés après cela utilisent le nouveau secret.
  • Supprimer l'endpoint arrête les nouveaux événements ; les messages déjà terminés conservent leur statut pour le polling.
Webhooks de livraison avec signatures | Documentation BulkSMSAPI