Privacy and retention
See what decision history keeps, what it never stores, and how long it is retained.
SignupScore keeps a limited decision history so you can understand recent signups and investigate a result. Full secret keys and full email addresses are not stored in that history.
Decision events
A decision in the dashboard can include:
| Stored item | Example |
|---|---|
| Domain | example.com |
| Input type | email or domain, never the address itself |
| Decision | allow, review, or block |
| Main reason | DISPOSABLE |
| Request ID | 01J... |
| Secret key name | Production |
| Time | When the check happened |
No part of the address is kept, not even a masked form of it. A row records the domain and that the input was an email, which is enough to recognize a decision.
Retention windows
| Plan | Decision history | Email memory |
|---|---|---|
| Free | 7 days | 30 days |
| Paid plans | 90 days | 365 days |
Records past these windows are deleted automatically. The dashboard may also show summary counts for recent decisions; those totals contain counts only, not email addresses or domain inputs.
We also read this history in aggregate to improve detection: a domain that several unrelated accounts check, and that we hold no intelligence about, is a candidate for review. That analysis looks at domains and counts only, and no customer is identified in the result.
Email memory (mailbox_alias)
The mailbox_alias signal spots when one mailbox reaches you under more than one spelling. It is off until you turn it on.
Turning it on never changes a decision. It adds one field to the response and nothing else, so no signup that gets in today is refused tomorrow because you enabled it.
How it works
- Your signup form sends an address, as usual.
- We simplify it to one standard spelling.
f.o.o+trial2@gmail.comandfoo@googlemail.comboth becomefoo@gmail.com. - We save two codes that cannot be read back: one for the simplified address, one for the spelling you sent. The address is not saved.
- Next time, any spelling of that inbox finds the same record. A spelling different from the first one we saw returns
mailbox_alias: true. The identical address returnsfalse, because that is a retry your own records already cover. - The decision never changes because of it.
Per-provider alias rules are in Signals and reasons.
What it is good for
Your users table sees two different strings. We see one inbox.
| They checked in as | They come back as | You get | If that mailbox already signed up |
|---|---|---|---|
sarah.chen@gmail.com | sarahchen+new@gmail.com | true | Block, a second trial |
johnsmith@gmail.com | john.smith@gmail.com | true | Block, or offer account recovery |
foo@gmail.com | foo@googlemail.com | true | Block, a second trial |
dana@fastmail.com | dana+promo@fastmail.com | true | Block, a second trial |
mia@outlook.com | mia+trial@outlook.com | true | Block, a second trial |
jane.smith@gmail.com | j.smith@gmail.com | false | Different inbox, let them in |
mia@outlook.com | m.ia@outlook.com | false | Dots count on Outlook |
foo@gmail.com | foo@gmail.com | false | Same address, just a retry |
The bottom three rows matter as much as the rest. We collapse only spellings a provider documents as one mailbox, never two addresses it treats as different, and never the identical address twice.
The deciding fact is one only you hold: whether that mailbox already has an account. Block when it does. When it does not, leave the decision to your other rules, because the earlier check may never have become a signup.
That lookup needs one column on your side. Save normalized_email as a canonical_email column when a signup completes, put a unique index on it, and query that column rather than the address the person typed. Your table stores what each person entered, so johnsmith@gmail.com will not find a row saved as john.smith@gmail.com unless you saved the canonical form too. The unique index is also what stops two alias signups racing each other through at the same moment.
{
"action": "allow",
"signals": {
"normalized_email": "johnsmith@gmail.com",
"mailbox_alias": true
}
}How the fingerprint is built
you send f.o.o+trial2@gmail.com
we simplify foo@gmail.com
your key HMAC-SHA-256(our secret, your account id)
we hash it HMAC-SHA-256(your key, foo@gmail.com)
we save v1:4c9be0f2a1d83b57e6c2091f7ad4e8b3Only the last line is saved. The v1 says which version of the rules made it.
The address you sent, f.o.o+trial2@gmail.com, goes through the same step to produce a second code. That one is saved on the first sighting and is what later checks compare against. Equally unreadable.
Nobody can read an address back out of it. A plain hash would not be safe: there are few enough real addresses that anyone could hash a long list and match the rows. The key stops that, and the key is not in the database.
Every account gets a different key. The same address under two customers gives two codes that look nothing alike, so records can never be matched between accounts.
What is stored
A separate data category from decision history, storing strictly less:
| Stored item | Detail |
|---|---|
| Keyed fingerprint | The v1:… value above. Non-reversible, unique to your account. |
| Spelling fingerprint | The same kind of code for the first spelling seen. Tells a retry from a new alias. |
| Related derived codes | Further codes of the same kind, used only to measure our own accuracy. Never returned by the API. |
| First seen, last seen, count | Timestamps and a counter. |
What is not stored: the email address, the local part, or any reversible form of either.
Records are kept 30 days on Free and 365 on paid plans, counted from the last check of that address; checking it again extends that. Controlled from Dashboard → Protection → Email memory:
- It starts off. New accounts record nothing until you turn it on.
- Turn it off at any time. Checks then return
mailbox_alias: null, nothing new is recorded, and stored records expire on their own. - Forget a specific email. Enter the address; we fingerprint it transiently, delete the match, and discard the address.
- Account deletion removes all memory records with the account.
API keys
SignupScore shows a new secret key once. After that, the dashboard keeps only what it needs to recognize and display the key. Revoking the key stops it from authenticating later requests.
Account deletion
For a privacy or account-deletion request, contact hi@hreflabs.com. We will confirm the account, remove the associated product data, and explain any records that cannot be removed immediately.