Skip to main content

Retention and erasure

The package keeps exactly what a subject-access request and an erasure request need, and no more. The two commands are covered operationally in Data protection; this page is the matrix.

DataOn erasure (billing:erase)Retention window
Subscriptions, usage counters, credit balancesdeleted—
Stored provider API keys owned by the ownerdeleted—
Personal data inside webhook payloads, and the error recorded with a deliveryscrubbedpayloads and their errors aged out after billing.retention.webhook_payload_days (default 90)
Owed credit balancebanked to the audit ledger, then purgedaudit window below
Invoices (InvoiceRecord)retained, unlinked from the ownerbilling.retention.erased_financial_days (default eight years, § 147 AO / § 14b UStG)
Audit ledgerrows about the person deleted; rows in which they only acted kept, with the actor clearedbilling.retention.audit_days (default ten years), counted from each row's creation, whoever it names
Produced tax returns and seller-reporting recordsnot reached: they name a period, not a personbilling.retention.audit_days, counted from the end of the year a record was produced in; a reporting record that was filed stays until the filings of its period go
Filings of seller-reporting recordsnot reached: they name a period, not a personbilling.retention.audit_days, counted from the end of the year a filing was recorded in; the filings of a period go together, once the youngest of them has had the window

A scrubbed payload stays scrubbed. A provider retries, a duplicate arrives, somebody resends an event from the dashboard: each of them reaches the delivery row the erasure kept. The scrub and the prune stamp that row, so a later delivery of the same event is recognized and its payload is not stored again.

A client_secret is never kept. Stripe asks for the field not to be stored, logged, or exposed to anyone but the customer. The ledger removes it before it stores a payload, and the data export removes it from a payload stored before that, so the copy a person receives holds the payment and not the secret.

Why invoices stay. A valid invoice has to carry the buyer's name and address (§ 14 UStG), and invoices have to be kept for years — so the right to erasure yields to a legal retention obligation (Art. 17(3)(b)). The rows are unlinked from the owner and kept; billing:prune removes them once the retention window closes.

A window counts in whole years from the end of a year. A value in days is read as its full years plus the days left over: 2920 days are eight years, so an invoice of 2021 is kept through 31 December 2029 and goes on the first run of 2030. Counted as plain days the window would close two days early, because eight years hold two leap days.

A reporting period leaves as a whole. A correction names the filing it corrects, so billing:prune keeps every filing of a period until the youngest of them has had its window. Then it removes the period's filings, newest first, and the records they filed go in the same run. A period with a filing the host holds stays whole.

A record's file goes with it. An invoice's kept PDF (pdf_path on billing.invoices.pdf_disk) and the file a tax return, a recapitulative statement or a seller-reporting record was written to (written_to, which names its disk and path) carry the same data as their rows. billing:prune removes such a file after its row, and only when no remaining row names it: a return produced again for the same period writes to the same path, and the newer row keeps the file. A file that is already gone is counted. One the disk does not remove is logged with its disk and path, and the run fails.

The retention floors cannot be set below the statutory minimum without an explicit jurisdiction opt-in — a fail-closed guard, see Invariants. Weigh the default windows against GDPR storage limitation for your own case and jurisdiction.


← Back to the documentation index