Retention, export & erasure
Your users’ data is your obligation, and it is sitting on our disks. This page is the three things you need to be able to do about that.
| You need to | Where |
|---|---|
| Know how long we keep things | Retention — Usage in the console |
| Answer “send me my data” | Export — a button, or nb.users.export() |
| Answer “delete my data” | Erasure — a button, or nb.users.erase() |
| Take your whole audience elsewhere | Audience export — Subscriptions → Export everyone |
How long we keep the delivery log
Every send writes one row per recipient per channel: who, when, on what, and whether it landed. That is the log behind the report screens, and it is the largest thing we store about your users. It is kept for a window that comes with your plan:
| Plan | Delivery history |
|---|---|
| Free | 30 days |
| Pro | 90 days |
| Scale | 180 days |
| Business | 365 days |
Older entries are removed. Campaign totals are not part of that window and are never pruned — a report from two years ago still says how many people it reached, how many failed and what the click rate was. What it loses is the row-by-row list underneath. Your monthly usage figures are stored separately too, so nothing that decides an invoice depends on a log that expires.
Two floors are applied under whatever your plan says, because a shorter window would break something quietly:
- 62 days minimum. Your usage page compares this month against last month. A window that ended mid-way through that comparison would shrink your own record of what you were billed for, on the first of the month, with nothing to explain it.
- Never inside a frequency-cap window. If you cap marketing at three messages per 90 days, we have to be able to see 90 days to enforce it. Pruning sooner would not make a report wrong — it would turn the cap off, for exactly the people who were messaged longest ago.
Custom events you send with track() are not pruned: they feed
attribution revenue, which is a lifetime number for your
business rather than a delivery record.
Everything about one person
Someone asks your support team for a copy of their data. This is the whole of it — profile, devices, addresses, inbox, events, delivery history and opt-outs — as one JSON file, from the person’s page in Subscriptions, or from your own backend:
const data = await nb.users.export("user-42");
// { user, devices, channel_identities, inbox, events,
// deliveries, suppressions, preferences, exported_at, note }
GET /v1/users/user-42/export // server key
GET /v1/orgs/:orgId/apps/:appId/users/:userId/export // console session
Delivery history and events are capped at the most recent 1,000 of each and are subject to
the retention window above — the file says so in its own note field,
because a file like this usually gets forwarded to the person it describes and
“is this all of it?” is the first question they ask.
Forgetting somebody
Different from an unsubscribe, and worth being precise about: unsubscribing is somebody who said stop, and they stay in your audience so you can prove they opted out. Erasure is somebody who said forget.
await nb.users.erase("user-42");
// { erased: true, deleted: {...}, anonymised: {...}, tombstoned: 2 }
DELETE /v1/users/user-42 // server key
DELETE /v1/orgs/:orgId/apps/:appId/users/:userId // console, with a typed confirm
Three things happen, and the difference between them is the whole design:
| What | Happens to it | Why |
|---|---|---|
| Profile, addresses, inbox, preferences, queued automations | Deleted | These exist only to describe one person. |
| Delivery events, custom events, attributions | Kept, anonymised | Each of these is also a count. A campaign that reached 4,000 people must still say 4,000 tomorrow, whoever asked to be forgotten in between. The address, the raw provider response and the link to the person come off; the row stays and names nobody. |
| Devices | Kept, unlinked | A push token belongs to a browser or a handset, not to a person, and the same phone can be handed on. The device stops being theirs. |
The part that looks wrong and is not
We record that the addresses must never come back — as a salted one-way hash, never the address itself. A later import of an older CSV hashes each row and refuses any that match, with the reason on the rejected row.
Without it, an erasure lasts exactly as long as it takes to import the export sitting on somebody’s laptop from last week — which is the single most likely thing to happen next. Keeping a hash nobody can reverse is how the request is honoured, not how it is defied.
Erasure is idempotent: erasing somebody already erased returns
erased: false rather than an error, so a “delete my account” flow in
your own product can retry safely.
Taking your whole audience with you
Subscriptions → Export everyone (CSV), or:
GET /v1/orgs/:orgId/apps/:appId/audience/export
One row per person, not per device — somebody with three phones is one row. It carries every reserved attribute, your own properties as JSON, device counts, and whether they have opted out. There is no lock-in here and it is not a favour: it is your audience, and the same file shape our import reads.
What we do not offer, and what to do instead
There is no self-serve “delete my whole organization” button. Deleting an app from Settings removes its audience, messages, delivery events, keys and everything else belonging to it. For an entire account, email [email protected] from the owner’s address — it is a rarely-used, entirely irreversible action and it is worth a human on both ends.