Webhooks
Les webhooks permettent à Facturino de notifier votre serveur en temps réel quand un événement se produit (facture payée, devis accepté, dépôt rejeté par la PA…). C'est l'alternative event-driven au polling, sans consommation de quota d'API.
Configurer un endpoint
Créez un endpoint depuis votre tableau de bord (Paramètres → Webhooks) ou via l'API :
POST /v1/webhook-endpoints
{
"url": "https://api.your-app.com/facturino-webhook",
"events": ["invoice.paid", "invoice.rejected", "quote.accepted"],
"description": "Synchronisation finance"
}
À la création, Facturino retourne le champ secret à conserver en lieu sûr — il sert à signer toutes les notifications.
Le secret n'est renvoyé en clair qu'à la création (masqué sur les lectures suivantes) ; en cas de perte, faites tourner l'endpoint.
L'URL doit être publique et résolvable. L'endpoint est vérifié à la création : HTTPS obligatoire, hôte résolvable en DNS vers une adresse publique. Les hôtes internes (localhost, IP privées) ou non résolvables sont refusés avec un 400 invalid_field_value. En développement, exposez votre serveur local via un tunnel (ngrok, cloudflared).
Format de la requête
Chaque notification est une requête POST en JSON avec quatre headers Facturino :
POST /your-webhook-endpoint HTTP/1.1
Host: api.your-app.com
Content-Type: application/json
Facturino-Signature: t=1709891260,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Facturino-Event-Id: evt_a1b2c3d4e5f6
Facturino-Event-Type: invoice.paid
User-Agent: Facturino-Webhooks/1.0 | Header | Description |
|---|---|
Facturino-Signature | Signature HMAC-SHA256 (format t={ts},v1={sig}) |
Facturino-Event-Id | Identifiant unique de l'événement (préfixe evt_) — utilisez-le pour la déduplication côté consommateur |
Facturino-Event-Type | Type de l'événement (ex: invoice.paid) — pratique pour router sans parser le body |
User-Agent | Facturino-Webhooks/1.0 |
Payload
Tous les événements partagent la même structure :
{
"id": "evt_a1b2c3d4e5f6",
"object": "event",
"type": "invoice.paid",
"livemode": true,
"apiVersion": "2026-03-01",
"created": "2026-04-22T13:48:10.000Z",
"data": {
"id": "inv_8f2k4m9n",
"object": "invoice",
"status": "paid",
"previous_status": "approved"
},
"request": null
}
Vérifier la signature
La signature suit le format Stripe : t={timestamp_unix},v1={hmac_sha256_hex} avec un payload signé "{ts}.{raw_body}".
Critique : vérifiez la signature à partir du corps brut de la requête HTTP, pas du JSON re-sérialisé. Une re-sérialisation modifie l'ordre des clés ou les espaces et casse la vérification. Configurez votre framework pour préserver l'octet-stream original (ex: express.raw()).
Node.js + Express :
import crypto from 'crypto'
import express from 'express'
const WEBHOOK_SECRET = process.env.FACTURINO_WEBHOOK_SECRET
function verifySignature(rawBody, header, tolerance = 300) {
if (!header) return false
const parts = Object.fromEntries(
header.split(',').map(p => p.split('=')),
)
const ts = Number(parts.t)
const sig = parts.v1
if (!ts || !sig) return false
// Anti-replay : rejette les requêtes trop anciennes
if (Math.abs(Date.now() / 1000 - ts) > tolerance) return false
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${ts}.${rawBody}`)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(sig, 'hex'),
Buffer.from(expected, 'hex'),
)
}
// IMPORTANT : utiliser express.raw() pour préserver l'octet-stream original
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifySignature(req.body.toString(), req.headers['facturino-signature'])) {
return res.status(401).end()
}
const event = JSON.parse(req.body.toString())
switch (event.type) {
case 'invoice.paid':
markOrderPaid(event.data.id)
break
case 'invoice.rejected':
alertFinanceTeam(event.data.id)
break
}
// Toujours répondre 200 rapidement — traitement async si besoin
res.status(200).end()
})
Python + Flask :
import hmac, hashlib, time
from flask import Flask, request, abort
WEBHOOK_SECRET = os.environ["FACTURINO_WEBHOOK_SECRET"]
def verify_signature(raw_body: bytes, header: str, tolerance: int = 300) -> bool:
if not header:
return False
parts = dict(p.split("=", 1) for p in header.split(","))
ts = int(parts.get("t", 0))
sig = parts.get("v1", "")
if not ts or not sig:
return False
if abs(time.time() - ts) > tolerance:
return False
expected = hmac.new(
WEBHOOK_SECRET.encode(),
f"{ts}.{raw_body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(sig, expected)
@app.post("/webhook")
def webhook():
if not verify_signature(request.get_data(), request.headers.get("Facturino-Signature")):
abort(401)
event = request.get_json()
if event["type"] == "invoice.paid":
mark_order_paid(event["data"]["id"])
return "", 200
PHP :
function verifySignature(string $payload, ?string $header, int $tolerance = 300): bool {
if (!$header) return false;
parse_str(str_replace(',', '&', $header), $parts);
$ts = (int) ($parts['t'] ?? 0);
$sig = $parts['v1'] ?? '';
if (!$ts || !$sig) return false;
if (abs(time() - $ts) > $tolerance) return false;
$expected = hash_hmac('sha256', "$ts.$payload", getenv('FACTURINO_WEBHOOK_SECRET'));
return hash_equals($expected, $sig);
}
Go :
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
"time"
)
func VerifySignature(payload []byte, header, secret string, tolerance time.Duration) bool {
if header == "" { return false }
var ts int64; var sig string
for _, p := range strings.Split(header, ",") {
kv := strings.SplitN(p, "=", 2)
switch kv[0] {
case "t": fmt.Sscan(kv[1], &ts)
case "v1": sig = kv[1]
}
}
if time.Since(time.Unix(ts, 0)) > tolerance { return false }
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.%s", ts, payload)
expected := hex.EncodeToString(mac.Sum(nil))
expectedBytes, _ := hex.DecodeString(expected)
sigBytes, _ := hex.DecodeString(sig)
return hmac.Equal(expectedBytes, sigBytes)
}
Retry et livraison
Facturino considère la livraison réussie quand votre endpoint retourne 2xx en moins de 30 secondes.
Toute autre réponse (timeout, 5xx, erreur réseau) déclenche un retry avec backoff exponentiel.
Tentative Délai depuis l'échec précédent 1 0 s (immédiat) 2 +1 min 3 +5 min 4 +30 min 5 +2 h
Après 5 tentatives échouées (≈ 2 h 36 min cumulés au plus), l'événement
est marqué delivered: false et un email est envoyé à l'administrateur de l'endpoint.
L'endpoint reste actif — vous pouvez rejouer l'événement à la main depuis
Paramètres → Webhooks
ou via POST /v1/events/:id/retry après avoir corrigé votre serveur.
Idempotence côté consommateur
Comme tout système distribué avec retry, Facturino garantit une livraison at-least-once — votre endpoint peut recevoir
le même événement plusieurs fois. Stockez les Facturino-Event-Id traités dans une table d'idempotence avec un TTL de quelques jours :
-- Schéma SQL simple
CREATE TABLE webhook_events_processed (
event_id VARCHAR(64) PRIMARY KEY,
processed_at TIMESTAMP DEFAULT NOW()
);
-- À la réception
INSERT INTO webhook_events_processed (event_id) VALUES ('evt_a1b2c3')
ON CONFLICT (event_id) DO NOTHING RETURNING event_id;
-- Si rien n'est retourné, l'événement a déjà été traité → ignorer
Types d'événements
Factures
Événement Déclencheur invoice.createdCréation d'un brouillon invoice.finalizedFinalisation (numéro attribué) invoice.sentEnvoi via PA ou téléchargement manuel invoice.sendingDépôt en cours auprès de la PA invoice.depositedDépôt confirmé par la PA invoice.transmittedTransmission à la DGFiP invoice.availableMise à disposition de l'acheteur invoice.receivedPrise en charge par l'acheteur invoice.approvedApprouvée par l'acheteur invoice.refusedRefusée par l'acheteur invoice.suspendedSuspendue par l'acheteur invoice.rejectedRejetée par la PA (le référent est dans data ; récupérez le motif via GET /v1/invoices/:id) invoice.paidPaiement intégral invoice.partially_paidPaiement partiel invoice.overdueDate d'échéance dépassée sans paiement complet
Devis
Événement Déclencheur quote.createdCréation d'un brouillon quote.sentEnvoi au client quote.viewedConsultation du devis par le client quote.acceptedAcceptation quote.refusedRefus quote.expiredDate de validité dépassée quote.convertedConversion en facture
Avoirs
Événement Déclencheur credit_note.createdCréation d'un brouillon credit_note.finalizedFinalisation credit_note.sentEnvoi credit_note.credit_depositedDépôt PA credit_note.credit_transmittedTransmission à la DGFiP credit_note.credit_approvedApprobation acheteur credit_note.credit_refusedRefus acheteur
Factures reçues (achat)
Événement Déclencheur invoice.incoming.receivedRéception d'une facture entrante via PA
Paiements
Événement Déclencheur payment.createdCréation d'un paiement payment.receivedEncaissement (manuel ou Stripe)
Autres
Événement Déclencheur customer.created / updated / deletedCycle de vie client ereporting.submittedDéclaration e-reporting transmise recurring_invoice.generated / failedGénération d'une facture récurrente export.readyExport FEC / RGPD disponible au téléchargement subscription.created / cancelled / paused / renewedCycle de vie de votre abonnement Facturino
Tester un webhook
Depuis le tableau de bord, vous pouvez :
- Envoyer un événement de test (
ping) à votre endpoint - Rejouer un événement passé via
POST /v1/events/:id/retry - Inspecter les tentatives de livraison via
GET /v1/events/:id
Pour le développement local, utilisez un tunnel HTTPS comme ngrok ou cloudflared :
# Exposer localhost:3000 sur une URL publique HTTPS
ngrok http 3000
# Coller l'URL https://xxxx.ngrok.io/webhook dans le dashboard Facturino
Restriction par IP
Les webhooks Facturino sont émis depuis des plages d'IPs stables si vous avez besoin de les ajouter à un allowlist.
Demandez la liste à jour au support — la signature reste la méthode d'authentification primaire.
Étapes suivantes
- Idempotence — déduplication côté consommateur
- Gestion des erreurs — interpréter les payloads d'événements d'erreur
- Référence interactive — API
/v1/webhook-endpoints