Aller au contenu

Webhooks

vitef appelle votre serveur à chaque étape de vos envois : plus besoin d’interroger l’API. Déclarez jusqu’à 5 adresses HTTPS dans l’onglet « Webhooks » de votre espace développeur, chacune pour l’environnement test ou live, avec les événements voulus (aucun coché : tous). Le secret de signature (whsec_…) s’affiche une seule fois, à la création.

Événements

  • mailing.created : envoi créé.
  • mailing.printed : imprimé.
  • mailing.posted : remis à la poste.
  • mailing.in_transit : en cours d’acheminement.
  • mailing.delivered : distribué.
  • mailing.ar_signed : accusé de réception signé.
  • mailing.returned : retourné à l’expéditeur.
  • mailing.error : incident.
  • mailing.canceled : annulé.
  • mailing.proof_available : preuve disponible (data.proof.kind : filing, delivery ou return).
  • ping : envoyé par le bouton « Envoyer un ping » de l’espace développeur, pour tester votre point de terminaison.

mailing.created part vers les webhooks déclarés au moment de la création de l’envoi.

Requête reçue

Un POST en JSON, avec ces en-têtes :

En-têtes
Content-Type: application/json
User-Agent: vitef-webhooks/1.0 (+https://vitef.com/developpeurs)
Vitef-Event: mailing.posted
Vitef-Delivery: d27ef1ba-566a-4445-92c6-a934dd9bcc84
Vitef-Signature: t=1790157600,v1=56ba1d22c65c8167bffbbface1341df8765851df35dbb3066f105e5cc61d72c0
  • Vitef-Event : type de l’événement.
  • Vitef-Delivery : identifiant de la livraison, le même à chaque nouvelle tentative (visible dans l’historique de l’espace développeur).
  • Vitef-Signature : t= horodatage Unix de l’envoi, v1= signature.
Corps
{
  "id": "d67c8dd3-a1da-40de-87b9-0275b5dc2aa2",
  "type": "mailing.posted",
  "created": "2026-09-23T10:00:00.000Z",
  "livemode": false,
  "data": {
    "mailing": {
      "title": "Lettre de résiliation SFR",
      "mode": "ar",
      "modeLabel": "Recommandé avec AR",
      "status": "posted",
      "statusLabel": "Remise à La Poste",
      "step": 2,
      "lastStep": 5,
      "trackingNumber": "1A15760000001",
      "createdAt": "2026-09-23T10:00:00.000Z",
      "events": [
        {
          "step": 0,
          "status": "paid",
          "label": "Payée",
          "at": "2026-09-23T10:00:00.000Z"
        },
        {
          "step": 1,
          "status": "printed",
          "label": "Imprimée",
          "at": "2026-09-23T10:00:00.000Z"
        },
        {
          "step": 2,
          "status": "posted",
          "label": "Remise à La Poste",
          "at": "2026-09-23T10:00:00.000Z"
        }
      ],
      "id": "e1209b63-3764-49e3-bf9c-154e9be830da",
      "orderId": null,
      "recipient": [
        "SFR Résiliation",
        "TSA 30103",
        "69947 Lyon Cedex 20"
      ],
      "amountCents": 0,
      "reference": "BAIL-2026-042",
      "test": true,
      "proofs": [
        {
          "kind": "filing",
          "label": "Preuve de dépôt"
        }
      ]
    }
  }
}

data.mailing est l’envoi tel que renvoyé par GET /v1/mailings/{id}, sans le contenu de la lettre, dans son état au moment de l’événement. livemode vaut false en bac à sable.

Vérifier la signature

v1 est le HMAC-SHA256, en hexadécimal, de `${t}.${corps brut}` avec votre secret (chaîne whsec_… entière) comme clé. Calculez-le sur le corps exact reçu (octets bruts, avant tout décodage JSON), comparez en temps constant et refusez un t éloigné de plus de 5 minutes de votre horloge (protection contre le rejeu).

Vérification
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.VITEF_WEBHOOK_SECRET; // whsec_…
const seen = new Set(); // en production : une table avec index unique sur event.id

// Corps brut : la signature porte sur les octets reçus, pas sur un JSON reformaté.
app.post('/webhooks/vitef', express.raw({ type: 'application/json' }), (req, res) => {
  const parts = Object.fromEntries((req.get('Vitef-Signature') ?? '').split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  const expected = crypto.createHmac('sha256', SECRET).update(`${parts.t}.${req.body}`).digest('hex');
  const valid =
    typeof parts.v1 === 'string' &&
    parts.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
    Math.abs(Date.now() / 1000 - t) <= 300; // 5 minutes de tolérance
  if (!valid) return res.status(400).send('Signature invalide');

  const event = JSON.parse(req.body);
  if (!seen.has(event.id)) {
    seen.add(event.id);
    // traitez event.type et event.data.mailing (de préférence en file d'attente)
  }
  res.sendStatus(200);
});

app.listen(3000);

Répondre, et nouvelles tentatives

  • Répondez 2xx en moins de 10 secondes, puis traitez l’événement en tâche de fond. Les redirections ne sont pas suivies.
  • Sinon (erreur, délai dépassé, autre statut), vitef réessaie après 1 min, 5 min, 30 min, 2 h, 6 h, 12 h puis 24 h : 8 tentatives au total, puis la livraison est abandonnée. Chaque tentative est signée avec un nouvel horodatage.
  • Un événement peut arriver deux fois ou dans le désordre : dédoublonnez sur id (l’événement) et fiez-vous à step et status de data.mailing plutôt qu’à l’ordre d’arrivée.
  • L’historique des livraisons (statut, tentatives, dernier code HTTP) est dans l’espace développeur, 30 jours.
  • Seules les adresses HTTPS publiques sont appelées ; une adresse qui résout vers un réseau privé est refusée.

Tester en bac à sable

Déclarez un webhook « test », créez un envoi avec votre clé test puis faites-le avancer avec POST /v1/test/mailings/{id}/advance : vous recevez les mêmes événements qu’en production, par exemple la preuve de dépôt :

mailing.proof_available
{
  "id": "979e9c80-00a3-47e9-8989-9692b0729c64",
  "type": "mailing.proof_available",
  "created": "2026-09-23T10:00:00.000Z",
  "livemode": false,
  "data": {
    "mailing": "…",
    "proof": {
      "kind": "filing"
    }
  }
}