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 :
- Vous enregistrez une ou plusieurs URLs de destination dans votre dashboard Facturino (ou via l'API)
- Vous sélectionnez les types d'événements qui vous intéressent
- Quand un événement se produit, Facturino envoie un POST à votre URL avec le payload JSON
- 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ééeinvoice.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éeinvoice.overdue— la facture est en retard de paiement
Devis
quote.sent— le devis est envoyé au clientquote.accepted— le client a accepté le devisquote.refused— le client a refusé le devisquote.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 facturepayment.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.