Facturino / Documentation / Webhooks

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
HeaderDescription
Facturino-SignatureSignature HMAC-SHA256 (format t={ts},v1={sig})
Facturino-Event-IdIdentifiant unique de l'événement (préfixe evt_) — utilisez-le pour la déduplication côté consommateur
Facturino-Event-TypeType de l'événement (ex: invoice.paid) — pratique pour router sans parser le body
User-AgentFacturino-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.

TentativeDélai depuis l'échec précédent
10 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énementDé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.depositedinvoice.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énementDé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énementDé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énementDéclencheur
invoice.incoming.receivedRéception d'une facture entrante via PA

Paiements

ÉvénementDéclencheur
payment.createdCréation d'un paiement
payment.receivedEncaissement (manuel ou Stripe)

Autres

ÉvénementDé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

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

ChampTypeNullableDescription
idstringnonChamp en lecture seule ; peut être absent des documents historiques.
objectstringnonChamp en lecture seule ; peut être absent des documents historiques.
statusstringnonRésumé au moment de l’événement.
previous_statusstringnonRésumé précédent lors d’une transition.
livemodebooleannonChamp en lecture seule ; peut être absent des documents historiques.
numberstringouiNuméro du document ; null avant numérotation.
documentStatusstringnonValeurs : draft, finalized, cancelled.
transmissionStatusstringnonValeurs : not_applicable, pending, sending, deposited, transmitted, approved, rejected.
transmissionDetailstringouiValeurs : available, received, suspended, refused.
paymentStatusstringnonValeurs : unpaid, partially_paid, paid, partially_refunded, refunded.
paErrorCodestringouiCode du dernier échec de dépôt : contrôle local ou réponse de la plateforme.
rejectionReasonstringouiMotif 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.
rejectionCategorystringouiLecture 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.
rejectionCodestringouiCode du verdict enregistré ; null hors rejet, refus ou suspension, ou si aucun code n’a été enregistré.
rejectionSourcestringouiAuteur du verdict enregistré ; null hors rejet, refus ou suspension, ou si la source manque sur un document ancien. Valeurs : platform, buyer, facturino.
relatedInvoiceIdstringouiIdentifiant de la facture liée, sur les événements d’avoir.
relatedInvoiceNumberstringouiNuméro de la facture liée, figé à la création de l’avoir ; null si absent sur un avoir ancien.
metadataobjectnonChamp en lecture seule ; peut être absent des documents historiques.
amountstringnonpayment.received : amount, en euros décimaux (chaîne).
total_paidstringnonpayment.received : total_paid, en euros décimaux (chaîne).
paymentIdstringouiVersement corrélé à la révision du grand livre ; null faute de preuve, absent sur un ancien événement payment.received.
fr212objectouiÉtat du versement à la publication : state, sentAt, lastErrorCode, lastErrorReason éventuel, updatedAt. Null sans suivi applicable ; absent sur un ancien événement.
total_duestringnonpayment.received : total de la facture (nom historique), en euros décimaux.
totalstringnonpayment.received : total, en euros décimaux (chaîne).
amountDuestringnonpayment.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.

TypesChampsRequis
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"
    }
  }
}