Facturer une boutique headless : webhooks à l'aller, numéros de facture au retour
cmssy laisse la facturation à votre propre système. Voici la boucle de webhooks qui émet la facture au paiement, enregistre son numéro sur la commande et ajoute un avoir au remboursement - en toute sécurité, même quand une livraison se répète.
Toute boutique a besoin de factures, et presque toute boutique a déjà un système qui les émet : un logiciel de comptabilité, une plateforme nationale de facturation électronique, un prestataire auquel tient votre comptable. cmssy n'essaie pas de le remplacer. Il fait trois choses plus modestes : il indique à votre système de facturation quand agir, il conserve sur la commande ce que ce système a émis, et il montre le résultat à l'acheteur.
Cet article construit cette boucle : émettre la facture quand une commande est payée, renvoyer le numéro, et émettre une facture d'avoir quand de l'argent est remboursé.

Le modèle : une facture, puis des avoirs
Chaque commande contient une liste invoices, de la plus ancienne à la plus récente. Chaque entrée a un kind (invoice ou correction), un number, une url facultative vers le PDF, un provider facultatif et un horodatage issuedAt. Les règles sont courtes :
- Une facture par commande. Enregistrer à nouveau le même numéro ne change rien. Un numéro différent est refusé, car une commande qui a soudain deux factures est une erreur comptable, pas une mise à jour.
- Une commande annulée ne peut pas être facturée. Si vous annulez d'abord, il n'y a plus rien à facturer.
- Un avoir nécessite une facture. Un avoir (facture d'avoir) est aussi autorisé sur les commandes annulées et remboursées, son numéro doit être nouveau pour la commande, et le répéter ne change rien.
- 20 entrées au maximum par commande, avoirs compris.

L'équipe peut enregistrer l'une ou l'autre depuis la page de la commande dans l'admin, via les mutations GraphQL order.recordInvoice et order.recordInvoiceCorrection, ou avec les outils MCP record_order_invoice et record_order_invoice_correction. Tous exigent la permission orders:manage.
Étape 1 : s'abonner aux bons événements
Créez un endpoint de webhook pour votre handler et abonnez-le à :
order.paid- la commande est entièrement payée ; il est temps d'émettre la facture.order.refundedetorder.partially_refunded- de l'argent a été rendu ; il est temps d'émettre un avoir.- en option
order.invoicedetorder.invoice_corrected- déclenchés après l'enregistrement d'un numéro, utiles pour envoyer le PDF par e-mail ou synchroniser un grand livre.
S'abonner aux événements order.* exige orders:view en plus de webhooks:manage. Chaque payload contient la commande avec ses invoices, total, tax, currency, taxTreatment (avec la mention légale à imprimer sur une facture en autoliquidation ou à l'export), l'acheteur et son entreprise.
Étape 2 : le handler
Voici une route Next.js. issueInvoice et issueCorrection représentent l'API de votre prestataire ; tout le reste, c'est cmssy.
// app/api/invoicing/route.ts
import { verifyCmssyWebhook, CmssyWebhookError } from "@cmssy/next";
import { issueInvoice, issueCorrection } from "@/lib/invoicing";
const RECORD = `mutation ($input: RecordOrderInvoiceInput!) {
order { recordInvoice(input: $input) { id } }
}`;
const CORRECT = `mutation ($input: RecordOrderInvoiceInput!) {
order { recordInvoiceCorrection(input: $input) { id } }
}`;
const CURRENT = `query ($id: ID!) {
order { get(id: $id) { invoices { kind number } } }
}`;
async function cmssy(query: string, variables: object) {
const res = await fetch("https://api.cmssy.io/graphql", {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${process.env.CMSSY_API_TOKEN}`,
"x-workspace-id": process.env.CMSSY_WORKSPACE_ID!,
},
body: JSON.stringify({ query, variables }),
});
const json = await res.json();
if (json.errors?.length) throw new Error(json.errors[0].message);
return json.data;
}
export async function POST(request: Request) {
const body = await request.text();
let event;
try {
event = await verifyCmssyWebhook({
body,
signatureHeader: request.headers.get("x-cmssy-signature"),
secret: process.env.CMSSY_WEBHOOK_SECRET!,
});
} catch (error) {
if (error instanceof CmssyWebhookError) {
return new Response(null, { status: 400 });
}
throw error;
}
const order = event.data.order;
const { order: { get: current } } = await cmssy(CURRENT, { id: order.id });
const invoice = current.invoices.find((i) => i.kind === "invoice");
if (event.event === "order.paid" && !invoice) {
const issued = await issueInvoice(order, { idempotencyKey: order.id });
await cmssy(RECORD, {
input: { orderId: order.id, number: issued.number, url: issued.pdfUrl, provider: "my-invoicing" },
});
}
if (event.event.endsWith("refunded") && invoice) {
const key = `${order.id}:${order.refundedAmount}`;
const issued = await issueCorrection(order, invoice, {
refundedTotal: order.refundedAmount,
idempotencyKey: key,
});
await cmssy(CORRECT, {
input: { orderId: order.id, number: issued.number, url: issued.pdfUrl, provider: "my-invoicing" },
});
}
return new Response(null, { status: 204 });
}Le jeton est un jeton d'API de workspace (cs_…) créé par une personne dont le rôle a orders:manage. Gardez-le côté serveur ; il peut faire tout ce que cette personne peut faire.
Étape 3 : supporter une double exécution
Les webhooks sont livrés au moins une fois et pas dans l'ordre. Une livraison peut être retentée après un délai dépassé que votre handler n'a pas remarqué, et une nouvelle tentative lente peut arriver après un événement plus récent. Trois éléments du handler ci-dessus existent pour cela :
- Lisez la commande actuelle, pas le payload. Le payload est un instantané du moment de l'événement. Deux livraisons du même
order.paiddisent toutes deux « pas encore de facture » ; seule une lecture fraîche indique à la seconde que la première a déjà terminé. - Donnez une clé d'idempotence à votre prestataire. Côté cmssy, tout est sûr - le même numéro envoyé deux fois n'enregistre rien en double - mais éviter que le prestataire émette deux factures, c'est votre côté. Utilisez l'id de la commande comme clé de la facture et le montant remboursé comme clé de l'avoir, si votre prestataire gère les clés ; sinon, cherchez un document existant par votre propre référence avant d'en émettre un.
- Traitez un refus comme une information. Si
recordInvoicerépond que la commande est déjà facturée avec un autre numéro, deux handlers se sont fait la course et le prestataire détient un doublon. Annulez-le chez lui ; cmssy a conservé la première.
Ne renvoyez un 2xx qu'après l'enregistrement du numéro. Une tentative échouée est retentée pendant environ 27 heures, donc une courte panne chez votre prestataire se résorbe d'elle-même.
Ce que voit l'acheteur
La boutique lit la même liste sur la commande de l'acheteur, de sorte qu'une page de commande peut afficher le lien vers la facture et chaque avoir dès qu'ils sont enregistrés - pour un client connecté comme pour un invité qui consulte sa commande avec le jeton reçu au checkout. Il n'y a rien à synchroniser.
Demandes d'effacement
Quand l'équipe anonymise une commande terminée, cmssy efface les données personnelles de l'acheteur et les liens de ses factures, et conserve les numéros de facture pour que les comptes restent justes. Votre système de facturation détient sa propre copie ; le webhook order.anonymized est le signal pour la traiter là-bas selon vos règles de conservation.
Sans code
Toutes les boutiques n'ont pas besoin d'un handler. Si les factures sont émises à la main, enregistrez le numéro sur la page de la commande, ou demandez à un agent connecté via MCP : « enregistre la facture FV/2026/1042 sur la commande 1042 avec ce lien PDF ». Les règles sont les mêmes, tout comme le webhook déclenché.
La référence se trouve dans la documentation commerce, la liste des événements et le détail des signatures dans la documentation des webhooks, et l'article sur les webhooks approfondit les nouvelles tentatives et la vérification.
