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

  1. Ouvrez Paramètres → Webhooks
  2. Cliquez sur Ajouter un webhook, collez l'URL de destination (doit être en https://)
  3. Choisissez les événements que vous souhaitez recevoir (vous pouvez en choisir plusieurs)
  4. Enregistrez — Qyvo déclenche immédiatement un événement webhook.test pour confirmer la joignabilité
  5. 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.