Fakturowanie w sklepie headless: webhooki na wejściu, numery faktur z powrotem
cmssy zostawia fakturowanie Twojemu własnemu systemowi. Oto pętla webhooków, która wystawia fakturę po płatności, zapisuje jej numer w zamówieniu i dodaje fakturę korygującą przy zwrocie - bezpiecznie, nawet gdy dostawa się powtórzy.
Każdy sklep potrzebuje faktur i prawie każdy sklep ma już system, który je wystawia: program księgowy, krajową bramkę e-fakturowania, dostawcę, przy którym upiera się Twoja księgowa. cmssy nie próbuje go zastąpić. Robi trzy mniejsze rzeczy: mówi Twojemu systemowi fakturowemu, kiedy ma działać, przechowuje w zamówieniu to, co ten system wystawił, i pokazuje wynik kupującemu.
Ten wpis buduje taką pętlę: wystaw fakturę, gdy zamówienie zostanie opłacone, zapisz jej numer z powrotem i wystaw fakturę korygującą, gdy pieniądze zostaną zwrócone.

Model: jedna faktura, potem korekty
Każde zamówienie ma listę invoices, od najstarszego wpisu. Każdy wpis ma kind (invoice lub correction), number, opcjonalny url do PDF-a, opcjonalny provider i znacznik czasu issuedAt. Zasady są krótkie:
- Jedna faktura na zamówienie. Ponowny zapis tego samego numeru niczego nie zmienia. Zapis innego numeru jest odrzucany, bo zamówienie, które nagle ma dwie faktury, to błąd księgowy, a nie aktualizacja.
- Anulowanego zamówienia nie da się zafakturować. Gdy najpierw anulujesz, nie ma czego fakturować.
- Korekta wymaga faktury. Faktura korygująca jest dozwolona także w zamówieniach anulowanych i zwróconych, jej numer musi być w zamówieniu nowy, a jej powtórzenie niczego nie zmienia.
- Najwyżej 20 wpisów na zamówienie, wliczając korekty.

Zespół może zapisać jedno i drugie ze strony zamówienia w panelu admina, przez mutacje GraphQL order.recordInvoice i order.recordInvoiceCorrection albo narzędziami MCP record_order_invoice i record_order_invoice_correction. Wszystkie wymagają uprawnienia orders:manage.
Krok 1: zasubskrybuj właściwe zdarzenia
Utwórz endpoint webhooka dla swojego handlera i zasubskrybuj go na:
order.paid- zamówienie jest w pełni opłacone; czas wystawić fakturę.order.refundediorder.partially_refunded- pieniądze wróciły do kupującego; czas na fakturę korygującą.- opcjonalnie
order.invoicediorder.invoice_corrected- wysyłane po zapisaniu numeru, przydatne do wysłania PDF-a e-mailem albo synchronizacji księgi.
Subskrypcja zdarzeń order.* wymaga orders:view oprócz webhooks:manage. Każdy payload niesie zamówienie z jego invoices, total, tax, currency, taxTreatment (z adnotacją prawną do umieszczenia na fakturze z odwrotnym obciążeniem lub eksportowej), kupującego i jego firmę.
Krok 2: handler
Oto trasa Next.js. issueInvoice i issueCorrection zastępują API Twojego dostawcy; cała reszta to 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 });
}Token to token API workspace'u (cs_…) utworzony przez osobę, której rola ma orders:manage. Trzymaj go po stronie serwera; może zrobić wszystko, co ta osoba.
Krok 3: zadbaj, by podwójne uruchomienie było bezpieczne
Webhooki są dostarczane co najmniej raz i nie po kolei. Dostawa może zostać ponowiona po timeoucie, którego Twój handler nie zauważył, a powolna ponowna próba może dotrzeć po nowszym zdarzeniu. Trzy rzeczy w handlerze powyżej istnieją właśnie po to:
- Czytaj bieżące zamówienie, nie payload. Payload to migawka z chwili zdarzenia. Dwie dostawy tego samego
order.paidmówią „jeszcze bez faktury”; dopiero świeży odczyt powie drugiej, że pierwsza już skończyła. - Daj dostawcy klucz idempotencji. Strona cmssy jest bezpieczna - ten sam numer wysłany dwa razy niczego nie zapisze podwójnie - ale to, czy dostawca wystawi dwie faktury, jest już po Twojej stronie. Jeśli Twój dostawca obsługuje klucze, kluczuj fakturę po id zamówienia, a fakturę korygującą po zwróconej kwocie; jeśli nie, przed wystawieniem poszukaj istniejącego dokumentu po własnym identyfikatorze.
- Traktuj odmowę jako informację. Jeśli
recordInvoiceodpowie, że zamówienie ma już fakturę z innym numerem, dwa handlery się ścigały, a dostawca ma duplikat. Anuluj go u dostawcy; cmssy zachowało pierwszą.
Zwracaj 2xx dopiero po zapisaniu numeru. Nieudana próba jest ponawiana przez około 27 godzin, więc krótka awaria u dostawcy naprawia się sama.
Co widzi kupujący
Sklep czyta tę samą listę w zamówieniu kupującego, więc strona zamówienia może linkować fakturę i każdą fakturę korygującą, gdy tylko zostaną zapisane - dla zalogowanego klienta i dla gościa, który czyta swoje zamówienie tokenem z checkoutu. Nie ma czego synchronizować.
Żądania usunięcia danych
Gdy zespół anonimizuje zakończone zamówienie, cmssy usuwa dane osobowe kupującego i linki w jego fakturach, a zachowuje numery faktur, żeby księgi wciąż się zgadzały. Twój system fakturowy ma własną kopię; webhook order.anonymized to sygnał, by obsłużyć ją tam zgodnie z Twoimi zasadami retencji.
Bez kodu
Nie każdy sklep potrzebuje handlera. Jeśli faktury wystawiasz ręcznie, zapisz numer na stronie zamówienia albo poproś agenta połączonego przez MCP: „zapisz fakturę FV/2026/1042 w zamówieniu 1042 z tym linkiem do PDF-a”. Zasady są te same, podobnie jak webhook, który to wywołuje.
Pełny opis jest w dokumentacji commerce, lista zdarzeń i szczegóły podpisów w dokumentacji webhooków, a wpis o webhookach wchodzi głębiej w ponowienia i weryfikację.
