Webhooks
Abonnez-vous aux messages entrants et aux événements de statut de livraison via les webhooks Qyvo, vérification de signature comprise.
Les webhooks vous permettent de pousser les événements hors de Qyvo dans votre stack au fur et à mesure qu'ils se produisent, au lieu de poller des triggers. Aujourd'hui Qyvo émet deux familles d'événements : les événements message et les événements status. Configurez-les dans Paramètres → Webhooks.
Mise en place
- Ouvrez Paramètres → Webhooks
- Cliquez sur Ajouter un webhook, collez l'URL de destination (doit être en
https://) - Choisissez les événements que vous souhaitez recevoir (vous pouvez en choisir plusieurs)
- Enregistrez — Qyvo déclenche immédiatement un événement
webhook.testpour confirmer la joignabilité - Copiez le secret de signature affiché une seule fois — Qyvo signera chaque payload avec HMAC-SHA256
Le secret de signature est affiché pendant 90 secondes, puis masqué pour toujours. Si vous le perdez, faites tourner le webhook (il obtient un nouveau secret) — ne journalisez jamais le secret côté serveur.
Types d'événements
| Événement | Quand il est déclenché | Forme du corps |
|---|---|---|
webhook.test |
Juste après la création du webhook | {event, webhook_id, timestamp} |
message.received |
Un message entrant texte/média arrive d'un contact | {event, message: {...}, contact: {...}} |
message.status.sent |
Message sortant accepté par Meta | {event, message_id, whatsapp_message_id, timestamp} |
message.status.delivered |
Meta confirme la livraison | Même forme |
message.status.read |
Le destinataire a ouvert le message | Même forme |
message.status.failed |
Meta a rejeté — le message atterrit dans l'inbox du tableau de bord avec status: failed |
Même forme, plus error_code et error_message |
L'objet message sur message.received correspond à la forme de réponse du trigger new-message-received.
Forme du payload
Les en-têtes incluent toujours :
Content-Type: application/json
X-Qyvo-Signature: t=1730918400,v1=4f3a...
X-Qyvo-Event: message.received
X-Qyvo-Webhook-Id: 01J4K...
Un corps message.received :
{
"event": "message.received",
"timestamp": "2026-05-07T08:14:23+00:00",
"tenant_id": "01HZX9...",
"message": {
"id": "01J4M...",
"direction": "inbound",
"type": "text",
"content": { "body": "Hi, do you ship to Canada?" },
"status": "received",
"contact_id": "01J1Y..."
},
"contact": {
"id": "01J1Y...",
"phone": "+14155550123",
"name": "Romain"
}
}
Vérifier la signature
Qyvo signe le corps brut de la requête avec HMAC-SHA256 keyed par le secret de signature de votre webhook. L'en-tête est X-Qyvo-Signature: t=<timestamp>,v1=<hex_signature>.
Calculez HMAC_SHA256(secret, t + "." + raw_body) et comparez avec v1. Rejetez en cas de mismatch, ou si t a plus de 5 minutes (protection contre le rejeu).
import crypto from 'crypto';
function verifyQyvoSignature(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) {
throw new Error('bad signature');
}
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
throw new Error('stale timestamp');
}
}
function verifyQyvoSignature(string $secret, string $header, string $rawBody): void
{
parse_str(strtr($header, [',' => '&']), $parts);
$expected = hash_hmac('sha256', $parts['t'].'.'.$rawBody, $secret);
if (!hash_equals($expected, $parts['v1'])) {
throw new RuntimeException('bad signature');
}
if (abs(time() - (int) $parts['t']) > 300) {
throw new RuntimeException('stale timestamp');
}
}
import hmac, hashlib, time
def verify_qyvo_signature(secret: str, header: str, raw_body: bytes) -> None:
parts = dict(p.split('=') for p in header.split(','))
expected = hmac.new(
secret.encode(),
f"{parts['t']}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, parts['v1']):
raise ValueError('bad signature')
if abs(time.time() - int(parts['t'])) > 300:
raise ValueError('stale timestamp')
Réessais
Si votre endpoint renvoie autre chose que 2xx (ou expire après 10 secondes), Qyvo réessaie avec un backoff exponentiel : 1 min, 5 min, 30 min, 2 h, 12 h, 24 h — puis abandonne l'événement. Le journal de livraison du webhook dans le tableau de bord montre les tentatives, le code de réponse, et le corps reçu en retour par Qyvo.
Idempotence : chaque événement a un en-tête immuable X-Qyvo-Webhook-Id. Si vous traitez les événements dans une base de données, écrivez INSERT … ON CONFLICT (webhook_id) DO NOTHING sur cet id pour rendre les réessais sûrs.
Développement local
La manière la plus rapide de recevoir des webhooks sur un laptop est ngrok http 3000 (ou cloudflared tunnel). Collez l'URL publique dans Paramètres → Webhooks → Ajouter un webhook, déclenchez webhook.test, et c'est prêt.
