ChinqIT VerifyChinqIT Verify

Webhooks

Recevez des notifications en temps réel pour les événements de messagerie en enregistrant des endpoints HTTPS. Base : https://sms.chinqit.com/api/v2.

Vue d'ensemble

Les webhooks vous permettent d'enregistrer des endpoints HTTPS publics sur lesquels ChinqIT pousse des notifications dès qu'un événement se produit. Chaque endpoint dispose de son propre secret de signature et peut s'abonner à n'importe quelle combinaison d'événements.

L'authentification et l'URL de base sont documentées dans la Référence API.


Catalogue des événements

Toutes les notifications utilisent l'enveloppe suivante :

{
  "id": "evt_01j9z...",
  "type": "message.sent",
  "createdAt": "2026-06-27T10:00:00.000Z",
  "data": { ... }
}
ÉvénementDéclencheur
message.sentSMS ou OTP-SMS accepté par l'opérateur (soumission réussie)
message.rejectedSMS ou OTP-SMS reçu par l'opérateur mais refusé — facturé
message.failedSMS ou OTP-SMS que notre plateforme n'a pas pu soumettre à l'opérateur — non facturé
bulk.completedTâche d'envoi en masse terminée, avec compteurs de succès/échec
otp.verifiedL'utilisateur final a saisi le code OTP correct

Remarque : La confirmation de livraison finale n'est pas disponible (pas d'accusés de réception opérateur). Il n'existe donc pas d'événement delivered. Les événements message.sent, message.rejected et message.failed reflètent uniquement le résultat de la soumission à l'opérateur.

Remarque — rupture de compatibilité : message.sent signifie que l'opérateur a reçu le message et que vous avez été facturé. message.rejected est émis lorsque l'opérateur le refuse après l'avoir reçu — généralement facturé, donc vérifiez data.billed (il vaut false dans le cas rare où ChinqIT bloque un numéro connu comme invalide avant soumission, qui n'atteint jamais l'opérateur). message.failed signifie que le message n'a jamais atteint l'opérateur et qu'il n'a pas été facturé (ou a été remboursé s'il l'avait déjà été). Si votre intégration s'appuyait auparavant sur message.failed pour détecter les refus, vous devez désormais également vous abonner à message.rejected — depuis ce changement, un refus n'émet plus message.failed.

Remarque : Les destinataires d'un envoi en masse n'émettent pas d'événements par message. Seul bulk.completed est émis à la fin du traitement.

message.sent

{
  "id": "evt_01j9z...",
  "type": "message.sent",
  "createdAt": "2026-06-27T10:00:00.000Z",
  "data": {
    "messageId": "550e8400-e29b-41d4-a716-446655440000",
    "phoneNumber": "+22238089336",
    "senderId": "CHINQIT",
    "channel": "sms",
    "type": "message",
    "submittedAt": "2026-06-27T10:00:00.000Z"
  }
}

message.rejected

{
  "id": "evt_01j9z...",
  "type": "message.rejected",
  "createdAt": "2026-06-27T10:00:03.000Z",
  "data": {
    "messageId": "550e8400-e29b-41d4-a716-446655440000",
    "phoneNumber": "+22238089336",
    "senderId": "CHINQIT",
    "channel": "sms",
    "type": "message",
    "billed": true
  }
}

data.billed fait foi : il vaut true lorsque l'opérateur a reçu et refusé le message (vous avez été facturé), et false dans le cas rare où ChinqIT bloque un numéro connu comme invalide avant soumission (jamais atteint l'opérateur, non facturé).

message.failed

{
  "id": "evt_01j9z...",
  "type": "message.failed",
  "createdAt": "2026-06-27T10:00:05.000Z",
  "data": {
    "messageId": "550e8400-e29b-41d4-a716-446655440000",
    "phoneNumber": "+22238089336",
    "senderId": "CHINQIT",
    "channel": "sms",
    "type": "message",
    "failureReason": "Insufficient balance"
  }
}

bulk.completed

{
  "id": "evt_01j9z...",
  "type": "bulk.completed",
  "createdAt": "2026-06-27T10:05:00.000Z",
  "data": {
    "batchId": "batch_01j9z...",
    "status": "completed",
    "totalMessages": 1000,
    "successfulCount": 985,
    "failedCount": 12,
    "invalidCount": 3,
    "totalCost": 9.85
  }
}

otp.verified

{
  "id": "evt_01j9z...",
  "type": "otp.verified",
  "createdAt": "2026-06-27T10:02:00.000Z",
  "data": {
    "messageId": "550e8400-e29b-41d4-a716-446655440000",
    "phoneNumber": "+22238089336",
    "verifiedAt": "2026-06-27T10:02:00.000Z"
  }
}

Sécurité

HTTPS obligatoire

Tous les endpoints webhook doivent utiliser HTTPS. Les URLs en HTTP sont rejetées.

Vérification de la signature X-Chinqit-Signature

Chaque livraison inclut les en-têtes suivants :

En-têteDescription
X-Chinqit-Signaturet=<unix>,v1=<hex> — horodatage et signature HMAC-SHA256
X-Chinqit-TimestampHorodatage Unix en secondes (même valeur que t= dans la signature)
X-Chinqit-DeliveryIdentifiant unique de la tentative de livraison (clé d'idempotence)

Recette de vérification :

signature = HMAC_SHA256(secret, "${t}.${rawBody}")
  1. Extrayez t et toutes les valeurs v1 depuis l'en-tête X-Chinqit-Signature.
  2. Calculez HMAC_SHA256(secret, t + "." + rawBody) avec le corps brut (non parsé).
  3. Comparez le résultat (en hex) à chaque valeur v1. Acceptez si au moins une correspond.
  4. Rejetez la requête si X-Chinqit-Timestamp est en dehors de votre fenêtre de tolérance (par exemple ±5 minutes) pour vous protéger contre les attaques par rejeu.

Exemple (Node.js) :

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifySignature(secret, rawBody, sigHeader, tsHeader, toleranceSec = 300) {
  const now = Math.floor(Date.now() / 1000);
  const ts = parseInt(tsHeader, 10);
  if (Math.abs(now - ts) > toleranceSec) return false;

  const parts = sigHeader.split(',');
  const t = parts.find(p => p.startsWith('t=')).slice(2);
  const v1s = parts.filter(p => p.startsWith('v1=')).map(p => p.slice(3));

  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return v1s.some(v => {
    try { return timingSafeEqual(Buffer.from(v, 'hex'), Buffer.from(expected, 'hex')); }
    catch { return false; }
  });
}

Idempotence

L'en-tête X-Chinqit-Delivery contient un identifiant stable de la livraison — la même valeur est réutilisée à chaque nouvelle tentative. Utilisez-le comme clé d'idempotence pour dédupliquer les événements reçus plusieurs fois.


Fiabilité

Nouvelles tentatives et backoff exponentiel

En cas d'échec (réponse non-2xx ou timeout), ChinqIT retente automatiquement la livraison jusqu'à 8 fois avec backoff exponentiel — les intervalles s'échelonnent progressivement (1 min, 5 min, 30 min, 2 h, 5 h, 10 h, jusqu'à 24 h entre deux tentatives), couvrant au total environ 1,5 à 2 jours.

Désactivation automatique

Un endpoint est automatiquement désactivé après 20 échecs consécutifs. Une alerte est envoyée à votre administrateur. Vous pouvez réactiver l'endpoint via l'API de gestion.

Rotation du secret avec fenêtre de chevauchement

Lors d'une rotation de secret (POST .../rotate-secret), l'ancien secret continue de signer les livraisons pendant une fenêtre de chevauchement de 24 heures. Pendant cette période, les livraisons incluent deux valeurs v1= dans l'en-tête de signature — l'une signée avec l'ancien secret, l'autre avec le nouveau. Acceptez si l'une ou l'autre est valide.


API de gestion

Toutes les routes nécessitent l'en-tête X-API-Key.

MéthodeEndpointDescription
POST/api/v2/webhooksCréer un endpoint webhook
GET/api/v2/webhooksLister vos endpoints
GET/api/v2/webhooks/{id}Obtenir un endpoint
PATCH/api/v2/webhooks/{id}Mettre à jour un endpoint
DELETE/api/v2/webhooks/{id}Supprimer un endpoint
POST/api/v2/webhooks/{id}/rotate-secretFaire tourner le secret de signature
POST/api/v2/webhooks/{id}/reveal-secretRévéler le secret de signature actuel (réservé au propriétaire, à tout moment)
POST/api/v2/webhooks/{id}/testEnvoyer une livraison de test
GET/api/v2/webhooks/{id}/deliveriesConsulter l'historique des livraisons

Créer un endpoint (exemple) :

curl -X POST https://sms.chinqit.com/api/v2/webhooks \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/chinqit",
    "events": ["message.sent", "message.failed", "otp.verified"]
  }'

Champs du corps de la requête :

  • url (requis) — l'endpoint HTTPS public vers lequel envoyer les événements.
  • events (requis) — tableau des types d'événements auxquels s'abonner.
  • apiKeyIds (optionnel) — restreint cet endpoint aux événements déclenchés par des clés API spécifiques (« applications »). Omettez ce champ ou passez [] pour recevoir les événements de toutes vos clés API. Lorsqu'il est renseigné, seuls les événements provenant des clés listées sont transmis à cet endpoint.

Réponse :

{
  "success": true,
  "webhook": {
    "_id": "665f1a2b3c4d5e6f7a8b9c0d",
    "userId": "6953a1b2c3d4e5f6a7b8c9d0",
    "url": "https://example.com/webhooks/chinqit",
    "events": ["message.sent", "message.failed", "otp.verified"],
    "status": "enabled",
    "secretPrefix": "whsec_ab12c…",
    "consecutiveFailures": 0,
    "createdAt": "2026-06-27T10:00:00.000Z",
    "updatedAt": "2026-06-27T10:00:00.000Z"
  },
  "secret": "whsec_4f3c8e21a9b0c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2"
}

Important : Le secret complet (préfixe whsec_) n'est renvoyé qu'ici à la création, ainsi que par /rotate-secret et /reveal-secret. Toutes les autres réponses n'exposent que secretPrefix. Conservez le secret complet immédiatement en lieu sûr.


Voir aussi

On this page