Retour au blog
Développeurs 7 min de lecture

Webhooks : recevoir les événements en temps réel

Le polling, c'est du passé. Plutôt que d'interroger l'API toutes les 30 secondes pour savoir si le statut d'une facture a changé, Facturino vous notifie en temps réel via des webhooks. Dès qu'un événement se produit — facture approuvée, paiement reçu, devis accepté — votre serveur reçoit un callback HTTP avec toutes les données nécessaires.

Comment fonctionnent les webhooks

Un webhook est une requête HTTP POST envoyée par Facturino vers une URL que vous avez configurée. Le principe est simple :

  1. Vous enregistrez une ou plusieurs URLs de destination dans votre dashboard Facturino (ou via l'API)
  2. Vous sélectionnez les types d'événements qui vous intéressent
  3. Quand un événement se produit, Facturino envoie un POST à votre URL avec le payload JSON
  4. Votre serveur traite l'événement et répond avec un code HTTP 2xx
// Enregistrer un webhook via l'API
const webhook = await fetch('https://facturino.com/api/v1/webhook-endpoints', {
  method: 'POST',
  headers,
  body: JSON.stringify({
    url: 'https://votre-app.com/webhooks/facturino',
    events: [
      'invoice.finalized',
      'invoice.available',
      'invoice.paid',
      'payment.received'
    ]
  })
}).then(r => r.json());

// webhook.secret → "whsec_xxxxxxxxxxxxxxxx"
// Conservez ce secret pour vérifier les signatures

Sécurité : signature HMAC-SHA256

Chaque requête webhook est signée avec votre webhook secret via HMAC-SHA256. Cela garantit que le payload provient bien de Facturino et qu'il n'a pas été altéré en transit. Vous devez vérifier cette signature avant de traiter l'événement.

La signature est envoyée dans le header Facturino-Signature sous la forme t=timestamp,v1=signature.

const crypto = require('crypto');

function verifyWebhookSignature(payload, header, secret) {
  const parts = header.split(',');
  const timestamp = parts.find(p => p.startsWith('t=')).slice(2);
  const signature = parts.find(p => p.startsWith('v1=')).slice(3);

  // Construire le message signé : timestamp + '.' + body
  const signedPayload = `${timestamp}.${payload}`;

  // Calculer la signature attendue
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Comparaison timing-safe (protection contre timing attacks)
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );

  // Vérifier que le timestamp n'est pas trop ancien (5 min max)
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
  if (age > 300) {
    throw new Error('Webhook timestamp trop ancien');
  }

  return isValid;
}

La comparaison timing-safe est essentielle : elle empêche un attaquant de deviner la signature octet par octet en mesurant les temps de réponse.

Structure du payload

Tous les webhooks suivent le même format, quel que soit le type d'événement :

{
  "id": "evt_a1b2c3d4e5f6g7h8",
  "object": "event",
  "type": "invoice.approved",
  "apiVersion": "2026-03-01",
  "created": "2026-04-14T10:30:00Z",
  "livemode": true,
  "data": {
    "id": "inv_x9y8z7w6v5u4",
    "object": "invoice",
    "status": "approved",
    "previous_status": "available",
    "livemode": true
  },
  "request": null
}

Le payload est volontairement léger : data ne contient qu'une référence à la ressource concernée (id, object, status et, pour un changement de statut, previous_status) — jamais l'objet complet. Récupérez les détails à jour (montants, client, lignes) via un GET /v1/invoices/{id}. Vous traitez ainsi toujours l'état courant de la ressource, et non une copie figée au moment de l'émission.

Les 43 types d'événements

Facturino émet 43 types d'événements organisés par catégorie. Le suivi du cycle de vie DGFiP se fait par un événement dédié par statut (il n'y a pas d'événement générique status_changed). Voici les principaux :

Factures

  • invoice.created — une facture brouillon est créée
  • invoice.finalized — la facture est finalisée (numéro attribué, Factur-X généré)
  • invoice.sent — la facture est marquée envoyée (flux de dépôt manuel)
  • invoice.deposited / transmitted / available / received / approved / refused / suspended / rejected — un événement par statut DGFiP (flux PA)
  • invoice.paid / invoice.partially_paid — la facture est intégralement ou partiellement payée
  • invoice.overdue — la facture est en retard de paiement

Devis

  • quote.sent — le devis est envoyé au client
  • quote.accepted — le client a accepté le devis
  • quote.refused — le client a refusé le devis
  • quote.expired — le devis a expiré

Avoirs

  • credit_note.created — un avoir est créé
  • credit_note.finalized — l'avoir est finalisé

Clients

  • customer.created — un nouveau client est ajouté
  • customer.updated — les informations d'un client sont modifiées

Paiements

  • payment.created — un paiement est enregistré sur une facture
  • payment.received — le paiement est confirmé reçu

Factures entrantes

  • invoice.incoming.received — une facture fournisseur est reçue via la PA

Politique de retry

Si votre serveur ne répond pas avec un code 2xx (ou est injoignable), Facturino retente automatiquement avec un backoff exponentiel :

Tentative Délai après l'échec
2e tentative 1 minute
3e tentative 5 minutes
4e tentative 30 minutes
5e (dernière) tentative 2 heures

Après 5 échecs consécutifs, l'événement est marqué comme échoué. Vous pouvez le relancer manuellement depuis le dashboard ou via l'API (POST /v1/events/:id/retry).

Gérer les doublons

Dans de rares cas (timeout réseau, retry), votre serveur peut recevoir le même événement plusieurs fois. Utilisez le champ id de l'événement pour garantir un traitement idempotent :

app.post('/webhooks/facturino', async (req, res) => {
  const event = req.body;

  // Vérifier la signature d'abord
  const isValid = verifyWebhookSignature(
    JSON.stringify(req.body),
    req.headers['facturino-signature'],
    process.env.WEBHOOK_SECRET
  );
  if (!isValid) return res.status(401).send('Signature invalide');

  // Vérifier si l'événement a déjà été traité
  const alreadyProcessed = await db.collection('processed_events')
    .doc(event.id).get();
  if (alreadyProcessed.exists) {
    return res.status(200).send('Déjà traité');
  }

  // Traiter l'événement
  switch (event.type) {
    case 'invoice.paid':
      await handleInvoicePaid(event.data.id);
      break;
    case 'invoice.approved':
      await handleStatusChange(event.data.id, event.data.status);
      break;
    // ... un case par statut : invoice.deposited, invoice.available, etc.
  }

  // Marquer comme traité
  await db.collection('processed_events')
    .doc(event.id).set({ processed_at: new Date().toISOString() });

  res.status(200).send('OK');
});

Console de débogage

Le dashboard Facturino inclut une console de débogage pour vos webhooks. Vous y trouvez :

  • L'historique complet des événements envoyés (payload, headers)
  • La réponse de votre serveur (code HTTP, body, durée)
  • Le statut de chaque tentative (succès, échec, en attente de retry)
  • Un bouton Renvoyer pour retester un événement

Tester en sandbox

En mode test (fac_test_), les webhooks fonctionnent exactement comme en production : même format de payload, même signature HMAC-SHA256, même politique de retry. Vous pouvez configurer une URL de destination différente pour le sandbox et la production.

Utilisez l'endpoint de simulation pour déclencher des événements à la demande :

// Simuler un changement de statut en sandbox
await fetch(
  `https://facturino.com/api/v1/sandbox/simulate-status/${invoiceId}`,
  {
    method: 'POST',
    headers,
    body: JSON.stringify({ status: 'approved' })
  }
);
// → Déclenche un webhook invoice.approved vers votre URL

Bonnes pratiques

  • Répondez vite : retournez un 200 immédiatement, puis traitez l'événement de manière asynchrone (queue, worker)
  • Vérifiez toujours la signature : ne traitez jamais un webhook sans valider le HMAC-SHA256
  • Gérez l'idempotence : stockez les IDs d'événements traités pour éviter les doublons
  • Protégez le timestamp : rejetez les webhooks dont le timestamp dépasse 5 minutes
  • Utilisez HTTPS : les URLs en HTTP ne sont pas acceptées en production
  • Loguez tout : en cas de problème, les logs sont votre meilleur allié pour comprendre ce qui s'est passé

Les webhooks sont disponibles sur les plans Essential (14 EUR/mois) et Pro (29 EUR/mois), en production comme en sandbox. Le plan gratuit n'inclut pas la création d'endpoints webhook.

Prêt à passer à la facturation électronique ?

Créez votre compte Facturino gratuitement et commencez à émettre des factures conformes en quelques minutes.