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.
| Data | On erasure (billing:erase) | Retention window |
|---|---|---|
| Subscriptions, usage counters, credit balances | deleted | — |
| Stored provider API keys owned by the owner | deleted | — |
| Personal data inside webhook payloads, and the error recorded with a delivery | scrubbed | payloads and their errors aged out after billing.retention.webhook_payload_days (default 90) |
| Owed credit balance | banked to the audit ledger, then purged | audit window below |
Invoices (InvoiceRecord) | retained, unlinked from the owner | billing.retention.erased_financial_days (default eight years, § 147 AO / § 14b UStG) |
| Audit ledger | rows about the person deleted; rows in which they only acted kept, with the actor cleared | billing.retention.audit_days (default ten years), counted from each row's creation, whoever it names |
| Produced tax returns and seller-reporting records | not reached: they name a period, not a person | billing.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 records | not reached: they name a period, not a person | billing.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.