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-09-01",
"created": "2026-04-22T13:48:10.000Z",
"data": {
"id": "inv_8f2k4m9n",
"object": "invoice",
"status": "paid",
"previous_status": "approved",
"livemode": true,
"number": "FAC2026-00042",
"documentStatus": "finalized",
"transmissionStatus": "approved",
"transmissionDetail": null,
"paymentStatus": "paid",
"paErrorCode": null,
"rejectionReason": null,
"rejectionCategory": null,
"rejectionCode": null,
"rejectionSource": null,
"metadata": { "orderId": "cmd-1842" }
},
"request": null
}
Les événements d’avoir incluent data.relatedInvoiceNumber, le numéro de la facture corrigée figé à la création (null si absent sur un avoir ancien). Le signal fr:211 ajoute uniquement une mention à l’historique de la facture et ne produit aucun événement webhook.
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 de délais planifiés, hors temps HTTP et attente en file), l'événement
est marqué delivered: false et un email est envoyé à l'administrateur de l'endpoint.
L’endpoint peut être désactivé après trois jours d’échecs continus ; les alertes sont espacées d’au moins 24 heures. 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. Sans corps, la relance ne vise que
les endpoints où la livraison a échoué ; avec { "endpointId": "we_…" }, elle rejoue l'événement vers ce
seul endpoint, même déjà livré, pour rattraper un traitement corrigé de votre côté. Votre récepteur doit rester
idempotent par identifiant d'événement.
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 ; le code et le motif de la plateforme sont dans einvoicing.paErrorCode et einvoicing.rejectionReason via GET /v1/invoices/:id) invoice.paidPaiement intégral invoice.partially_paidPaiement partiel invoice.overdueDate d'échéance dépassée sans paiement complet
Les événements de transmission (invoice.deposited … invoice.approved) suivent
transmissionStatus, pas seulement status : une facture encaissée avant son dépôt garde
status: "paid" à l'approbation et reçoit tout de même invoice.approved
(data.previous_status vaut alors "paid").
Contenu de data. status et previous_status décrivent le saut annoncé ;
les autres champs décrivent le document tel qu'il était écrit au moment de l'événement : number,
les trois axes documentStatus, transmissionStatus (avec transmissionDetail) et
paymentStatus, et vos metadata. Les avoirs ajoutent relatedInvoiceId, les devis
portent number et metadata. payment.received ajoute amount,
total_paid, total (total de la facture, identique au champ historique total_due)
et amountDue (reste dû), ainsi que paymentId et fr212 lorsque le versement est identifiable. L'ordre d'arrivée des événements n'est pas garanti : lisez l'état porté par
l'événement ou relisez le document, jamais l'ordre de réception.
Rejet ou refus. Chaque événement de facture ou d'avoir porte aussi paErrorCode,
rejectionReason (les mots de la plateforme ou de l'acheteur) et rejectionCategory
(la lecture stable qu'en fait Facturino : buyer_not_in_directory, addressing_error, other, format_invalid,
semantic_error, duplicate, platform_auth, platform_unavailable,
refused_by_buyer, suspended, unknown). Ils décrivent la tentative en cours :
renseignés sur invoice.rejected, invoice.refused et
credit_note.credit_refused, remis à null dès qu'un renvoi ouvre une nouvelle tentative.
Routez sur la catégorie, jamais sur le texte : buyer_not_in_directory signifie que la recherche n’a trouvé aucune adresse active pour cet acheteur — envoyez-la par e-mail.
Ce constat d’acheminement ne valide pas le contenu de la facture.
rejectionCode porte le code DGFiP, HTTP plateforme ou local et rejectionSource
l’auteur enregistré : platform, buyer ou facturino.
Ces deux champs sont null hors rejet, refus ou suspension, ou s’ils manquent sur un document ancien.
Un contrôle annuaire avant dépôt porte rejectionCode: buyer_not_in_directory et
rejectionSource: facturino. Le motif brut reste verbatim ; les notes se lisent dans
einvoicing.rejectionNote du document. Le renvoi archive ces champs dans
einvoicing.previousSubmissions puis les remet à null.
Sauts d'état. Une plateforme peut passer directement de sending à available sans
signaler deposited ni transmitted : Facturino n'émet que les transitions observées, jamais
des événements reconstitués. Un statut fr:211 « Paiement transmis » émis par l’acheteur ne rétrograde jamais
un encaissement complet ou un remboursement enregistré dans Facturino. Il ajoute uniquement une mention
à l’historique, sans modifier les axes, le résumé ou les montants, et sans émettre d’événement webhook.
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
Contrat de data : types et nullabilité
Les champs ci-dessous concernent les événements de facture et d’avoir. Les ajouts peuvent être absents des événements historiques. Le corps signé contient id, object, type, apiVersion, created, livemode, data et request (objet ou null). Les compteurs de livraison se lisent par GET /v1/events/:id.
Champs documentaires et payment.received
Champ Type Nullable Description idstring non Champ en lecture seule ; peut être absent des documents historiques. objectstring non Champ en lecture seule ; peut être absent des documents historiques. statusstring non Résumé au moment de l’événement. previous_statusstring non Résumé précédent lors d’une transition. livemodeboolean non Champ en lecture seule ; peut être absent des documents historiques. numberstring oui Numéro du document ; null avant numérotation. documentStatusstring non Valeurs : draft, finalized, cancelled. transmissionStatusstring non Valeurs : not_applicable, pending, sending, deposited, transmitted, approved, rejected. transmissionDetailstring oui Valeurs : available, received, suspended, refused. paymentStatusstring non Valeurs : unpaid, partially_paid, paid, partially_refunded, refunded. paErrorCodestring oui Code du dernier échec de dépôt : contrôle local ou réponse de la plateforme. rejectionReasonstring oui Motif brut de la plateforme ou de l’acheteur, verbatim ; constat local pour la source facturino. Ne contient pas de code ou de note reconstruits. Null si absent. rejectionCategorystring oui Lecture stable parmi onze catégories. Priorité : code local, code DGFiP, code HTTP ; seul le pré-contrôle annuaire Super PDP dispose d’un repli textuel. REJ_ADR = addressing_error, REJ_SEMAN = semantic_error, REJ_SYNT/REJ_XSD = format_invalid, REJ_DOUBLON = duplicate, AUTRE = other (explication dans rejectionNote) ; codes Iopole : SYNTAX_ERROR = format_invalid, SEMANTIC_ERROR = semantic_error, DUPLICATED_INVOICE = duplicate, ROUTING_FAILURE = buyer_not_in_directory (unknown si le vendeur est inconnu de la plateforme). Le statut fr:213 seul ne classe pas en sémantique. Null au renvoi. Valeurs : buyer_not_in_directory, addressing_error, other, format_invalid, semantic_error, duplicate, platform_auth, platform_unavailable, refused_by_buyer, suspended, unknown. rejectionCodestring oui Code du verdict enregistré ; null hors rejet, refus ou suspension, ou si aucun code n’a été enregistré. rejectionSourcestring oui Auteur du verdict enregistré ; null hors rejet, refus ou suspension, ou si la source manque sur un document ancien. Valeurs : platform, buyer, facturino. relatedInvoiceIdstring oui Identifiant de la facture liée, sur les événements d’avoir. relatedInvoiceNumberstring oui Numéro de la facture liée, figé à la création de l’avoir ; null si absent sur un avoir ancien. metadataobject non Champ en lecture seule ; peut être absent des documents historiques. amountstring non payment.received : amount, en euros décimaux (chaîne). total_paidstring non payment.received : total_paid, en euros décimaux (chaîne). paymentIdstring oui Versement corrélé à la révision du grand livre ; null faute de preuve, absent sur un ancien événement payment.received. fr212object oui État du versement à la publication : state, sentAt, lastErrorCode, lastErrorReason éventuel, updatedAt. Null sans suivi applicable ; absent sur un ancien événement. total_duestring non payment.received : total de la facture (nom historique), en euros décimaux. totalstring non payment.received : total, en euros décimaux (chaîne). amountDuestring non payment.received : amountDue, en euros décimaux (chaîne).
Les notes verbatim restent dans einvoicing.rejectionNote, à relire sur la facture ou l’avoir. Les montants de payment.received sont des chaînes en euros décimaux, alors que l’API REST renvoie des centimes entiers ; total_due désigne historiquement le total de la facture, et amountDue le solde restant.
invoice.rejected
{
"id": "evt_invoice_rejected",
"object": "event",
"type": "invoice.rejected",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"data": {
"id": "inv_00090",
"object": "invoice",
"number": "FAC2026-00090",
"status": "rejected",
"previous_status": "deposited",
"livemode": true,
"documentStatus": "finalized",
"transmissionStatus": "rejected",
"transmissionDetail": null,
"paymentStatus": "unpaid",
"paErrorCode": null,
"rejectionReason": "REJ_ADR — Directory line matricule_plateforme (0037) does not match expected matricule (0022)",
"rejectionCategory": "addressing_error",
"rejectionCode": "REJ_ADR",
"rejectionSource": "platform",
"metadata": {}
},
"request": null
}
invoice.refused
{
"id": "evt_invoice_refused",
"object": "event",
"type": "invoice.refused",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"data": {
"id": "inv_00090",
"object": "invoice",
"number": "FAC2026-00090",
"status": "refused",
"previous_status": "deposited",
"livemode": true,
"documentStatus": "finalized",
"transmissionStatus": "rejected",
"transmissionDetail": "refused",
"paymentStatus": "unpaid",
"paErrorCode": null,
"rejectionReason": "Montant contesté.",
"rejectionCategory": "refused_by_buyer",
"rejectionCode": null,
"rejectionSource": "buyer",
"metadata": {}
},
"request": null
}
credit_note.credit_refused
{
"id": "evt_credit_note_credit_refused",
"object": "event",
"type": "credit_note.credit_refused",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"data": {
"id": "cn_example",
"object": "credit_note",
"number": "AV2026-00001",
"status": "credit_refused",
"previous_status": "deposited",
"livemode": true,
"documentStatus": "finalized",
"transmissionStatus": "rejected",
"transmissionDetail": "refused",
"paymentStatus": "unpaid",
"paErrorCode": null,
"rejectionReason": "Montant contesté.",
"rejectionCategory": "refused_by_buyer",
"rejectionCode": null,
"rejectionSource": "buyer",
"metadata": {},
"relatedInvoiceId": "inv_00090",
"relatedInvoiceNumber": "FAC2026-00090"
},
"request": null
}
payment.received
{
"id": "evt_payment_received",
"object": "event",
"type": "payment.received",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"data": {
"paymentId": "pay_00090",
"fr212": {
"state": "awaiting_deposit",
"sentAt": null,
"lastErrorCode": "awaiting_pa_deposit",
"updatedAt": "2026-09-07T12:36:00.000Z"
},
"id": "inv_00090",
"object": "invoice",
"number": "FAC2026-00090",
"status": "paid",
"previous_status": "rejected",
"livemode": true,
"documentStatus": "finalized",
"transmissionStatus": "rejected",
"transmissionDetail": null,
"paymentStatus": "paid",
"paErrorCode": null,
"rejectionReason": "REJ_ADR — Directory line matricule_plateforme (0037) does not match expected matricule (0022)",
"rejectionCategory": "addressing_error",
"rejectionCode": "REJ_ADR",
"rejectionSource": "platform",
"metadata": {},
"amount": "11.88",
"total_paid": "11.88",
"total_due": "11.88",
"total": "11.88",
"amountDue": "0.00"
},
"request": null
}
Lire fr212 après payment.received
payment.received décrit une variation du total encaissé : data.id identifie la facture et data.paymentId le versement lié à cette révision du grand livre. data.fr212 porte l’état de collecte au moment de la publication. Les deux champs peuvent être absents sur un événement ancien ; paymentId vaut null si la variation ancienne ou agrégée ne permet pas une attribution certaine, et fr212 vaut aussi null sans suivi applicable. Relisez GET /v1/invoices/:id/payments pour l’état actuel. Exemple REST d’un paiement, en centimes entiers :
{
"id": "pay_example",
"object": "payment",
"amount": 1188,
"method": "transfer",
"reference": null,
"paidAt": "2026-09-07T12:40:00.000Z",
"recorded_by": "api",
"created": "2026-09-07T12:41:00.000Z",
"fr212": {
"state": "awaiting_deposit",
"sentAt": null,
"lastErrorCode": "awaiting_pa_deposit",
"updatedAt": "2026-09-07T12:41:00.000Z"
}
}
Données discriminées par type d’événement
L’OpenAPI décrit douze familles avec un discriminateur sur type. Les champs requis figurent dans la dernière colonne ; les autres sont facultatifs. Les montants des événements sont des chaînes décimales en euros ; count et attempt sont des entiers. Les horodatages sont des chaînes ISO. Les quatre SDK 2.7.0 exposent les données de ces familles.
Types Champs Requis invoice.created, invoice.finalized, invoice.sending, invoice.sent, invoice.deposited, invoice.transmitted, invoice.available, invoice.received, invoice.approved, invoice.refused, invoice.rejected, invoice.suspended, invoice.paid, invoice.partially_paid, invoice.overdueid, object, status, previous_status, livemode, number (nullable), documentStatus, transmissionStatus, transmissionDetail, paymentStatus, paErrorCode (nullable), rejectionReason (nullable), rejectionCategory (nullable), rejectionCode (nullable), rejectionSource (nullable), metadataid, object, status invoice.incoming.receivedpa_invoice_id, sender_siret, sender_name, total_ht, total_tva, total_ttcid, object, pa_invoice_id quote.created, quote.sent, quote.viewed, quote.accepted, quote.refused, quote.expired, quote.convertedid, object, status credit_note.created, credit_note.finalized, credit_note.credit_deposited, credit_note.credit_transmitted, credit_note.credit_approved, credit_note.credit_refused, credit_note.sentrelatedInvoiceId (nullable), relatedInvoiceNumber (nullable)id, object, status customer.created, customer.updated, customer.deletedid, object, livemode payment.createdinvoiceId, paymentId, amount, methodinvoiceId, paymentId, amount, method payment.receivedpaymentId, fr212, amount, total_paid, total_due, total, amountDueid, object, amount, total_paid, total_due ereporting.submittedtype (nullable), period (nullable), attemptNumberid, object, status recurring_invoice.generatedrecurringInvoiceIdid, object, recurringInvoiceId recurring_invoice.failederrorid, object, error export.readycountid, object, count subscription.created, subscription.cancelled, subscription.renewed, subscription.pausedplan, stripeSubscriptionId, pausedUntil (nullable), reasonstatus
payment.created porte invoiceId et paymentId, sans id ni object. payment.received conserve id pour la facture et ajoute paymentId pour le versement, avec son fr212. Une variation sans attribution certaine porte null ; les événements anciens restent lisibles sans ces champs.
ereporting.submitted est créé dans la transaction qui confirme la déclaration, par la route REST comme par le traitement planifié. Son identité dépend de la déclaration et de sa tentative attempt. Aucun nouvel événement webhook.* n’est ajouté : les livraisons restent décrites par deliveries, avec attemptCount et generation pour le cycle de rejeu courant.
Les événements sont conservés 90 jours après leur création, ou après la dernière livraison réussie lorsque celle-ci rafraîchit cette échéance. Une panne de création de tâche laisse nextRetry durable ; le passage planifié toutes les dix minutes récupère ces livraisons. Un rejeu ciblé conserve l’historique et ouvre une génération distincte.
Exemples par famille
invoice.created
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_invoice_created",
"type": "invoice.created",
"data": {
"id": "inv_x",
"object": "invoice",
"status": "draft"
}
}
credit_note.created
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_credit_note_created",
"type": "credit_note.created",
"data": {
"id": "cn_x",
"object": "credit_note",
"status": "draft"
}
}
quote.created
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_quote_created",
"type": "quote.created",
"data": {
"id": "qt_x",
"object": "quote",
"status": "draft",
"number": null,
"metadata": {}
}
}
customer.created
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_customer_created",
"type": "customer.created",
"data": {
"id": "cus_x",
"object": "customer",
"livemode": false
}
}
payment.created
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_payment_created",
"type": "payment.created",
"data": {
"invoiceId": "inv_00090",
"paymentId": "pay_fixture",
"amount": "11.88",
"method": "card"
}
}
invoice.incoming.received
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_invoice_incoming_received",
"type": "invoice.incoming.received",
"data": {
"id": "rec_fixture",
"object": "received_invoice",
"pa_invoice_id": "pa_fixture",
"sender_siret": "73282932000074",
"sender_name": "INTEK CENTER",
"number": "FAC2026-00112",
"total_ht": "9.90",
"total_tva": "1.98",
"total_ttc": "11.88"
}
}
recurring_invoice.generated
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_recurring_invoice_generated",
"type": "recurring_invoice.generated",
"data": {
"id": "inv_x",
"object": "invoice",
"recurringInvoiceId": "recurring_fixture",
"livemode": false
}
}
recurring_invoice.failed
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_recurring_invoice_failed",
"type": "recurring_invoice.failed",
"data": {
"id": "recurring_fixture",
"object": "recurring_invoice",
"error": "Operation unavailable"
}
}
ereporting.submitted
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_ereporting_submitted",
"type": "ereporting.submitted",
"data": {
"id": "rep_fixture",
"object": "ereporting",
"status": "submitted",
"type": "b2c",
"period": "2026-09",
"attempt": 2
}
}
export.ready
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_export_ready",
"type": "export.ready",
"data": {
"id": "job_fixture",
"object": "export",
"count": 10
}
}
subscription.paused
{
"object": "event",
"apiVersion": "2026-09-01",
"created": "2026-09-07T12:36:00.000Z",
"livemode": true,
"request": null,
"id": "evt_subscription_paused",
"type": "subscription.paused",
"data": {
"status": "paused",
"pausedUntil": null,
"stripeSubscriptionId": "sub_fixture"
}
}
credit_note.sent fournit aussi paStatus (string) et paInvoiceId (string nullable), distincts du statut résumé de l’avoir.
Suivi autonome des obligations
invoice.obligation_updated, credit_note.obligation_updated, payment.obligation_updated et ereporting.obligation_updated annoncent un changement durable de prise en charge. data reste un objet plat : id (string), object (invoice, credit_note, payment ou ereporting), status facultatif, obligation (objet requis) et reason (string nullable, mots originaux). Un versement ajoute invoiceId et fr212 (objet nullable). Aucun identifiant de verrou n’est exposé.
obligation contient state, reasonCode, reasonSource, owner, action, nextAttemptAt ; tous sauf state sont nullables. Les événements de facture et d’avoir peuvent aussi porter cette projection facultative. ereporting.accepted et ereporting.rejected annoncent les verdicts finaux avec id, object, status, type, period, attempt, obligation, paRejectionCode et paRejectionReason (nullables).
ereporting.submitted signifie réception de la transmission par la plateforme, pas acceptation fiscale. Une tentative conserve son identité ; le rejeu d’un résultat déjà enregistré ne recrée pas son événement. Les événements restent livrés au moins une fois et peuvent arriver dans le désordre : dédoublonnez par event.id et ne rétrogradez pas un résultat final confirmé. Ces retours dispensent de relectures permanentes et de cron de reprise chez l’intégrateur.
{
"id": "evt_example",
"object": "event",
"type": "payment.obligation_updated",
"apiVersion": "2026-09-01",
"created": "2026-09-13T10:00:00.000Z",
"livemode": false,
"data": {
"id": "pay_example",
"object": "payment",
"invoiceId": "inv_example",
"reason": "Réessayer plus tard",
"obligation": {
"state": "waiting",
"reasonCode": "pa_lifecycle_not_ready",
"reasonSource": "platform",
"owner": "facturino",
"action": "retry",
"nextAttemptAt": "2026-09-13T10:05:00.000Z"
},
"fr212": {
"state": "pending",
"sentAt": null,
"lastErrorCode": "pa_lifecycle_not_ready",
"updatedAt": "2026-09-13T10:00:00.000Z"
}
}
}