What happens to an address you send us
Sortance is a router, so the honest answer involves other companies. This page says which ones, what they see, what is kept, and what is never written down at all.
This describes how the system behaves, verified against the source it runs on. It is not a privacy policy, a DPA or legal advice, and it has not been through legal review. Those documents are still to be written.
What is never persisted
The raw email address. It exists in the request you made and in the outbound call to a provider, or to the screening model. Before it reaches storage, a log line or a metrics blob it is replaced by a keyed hash under a deployment secret. There is no table anywhere in Sortance containing readable customer addresses.
The one exception is a list you upload. A CSV sent to bulk verification, and the result files made from it, are kept as files for 7 days so you can download them, then deleted (see the table below). They are never copied into a table, a log line or a metrics blob, and their storage keys contain a job id, never an address.
The response echoes your address back to you because you sent it thirty milliseconds earlier. Nothing else does.
A name you send for screening. The optional name on /v1/screen is used in that request and then dropped. No column holds the name, and nothing that could recover it (no hash, length or script). Only the labels describing how it compared to the address and whether it looked real are kept.
Your API key. Only its SHA-256 is stored. Sortance can verify a key you present and cannot look one up, recover one, or show you one again.
Sub-processors
An address you send may be forwarded to one or more verification providers. Which ones depends on the routing decision, and every response tells you exactly who was asked in its providers map — this is not something you have to take on trust after the fact.
When enrichment runs, the address may also go to an enrichment provider, and the response then carries personal data about the person behind it. That is a real change to the boundary above, so it has its own section.
Verification providers
| Provider | Receives |
|---|---|
| Sortance check (our own) | Nothing leaves Sortance. Runs inside the Sortance API itself: syntax, MX and Sortance's own compiled disposable-domain lists. No network call, nothing sent to anyone, no SMTP mailbox check, so it never answers valid. |
| DeBounce | The email address, when the router selects it for this decision. |
| MyEmailVerifier | The email address, when the router selects it for this decision. |
| ZeroBounce | The email address, when the router selects it for this decision. |
Enrichment providers
| Provider | Receives |
|---|---|
| People Data Labs | The company domain for a company lookup. The email address as well for a person lookup, which runs only when a person field is needed. Never for a consumer mailbox such as Gmail. |
| Apollo | The company domain for a company lookup. The email address as well for a person lookup, which runs only when a person field is needed. Never for a consumer mailbox such as Gmail. |
Model provider
| Provider | Receives |
|---|---|
| Anthropic, via Cloudflare AI Gateway, for the policy, ICP and experiment compilers | The text you wrote; the rules of the policy version you are amending, if any; the closed field vocabularies; your list names, kinds and entry counts; your ICP criterion ids; your policy slugs and version numbers. Never an address, a list entry, an enrichment profile or a decision record. Compiles the English you type into a policy, ICP rubric or experiment, once, when you click compile. Called through Cloudflare AI Gateway; api/src/compile/model.ts falls back to Anthropic's API directly only on a deployment with no gateway configured. |
| Anthropic, via Cloudflare AI Gateway when configured, otherwise directly, for the setup assistant in the dashboard | Your chat messages, resent each turn; the compile, replay, segment and recommendation results the assistant asks for (aggregate counts and rules, never rows); your policy slugs and versions; your list names, kinds and entry counts. Never an enrichment profile, a trace row, a list entry or the value of a custom attribute. A message that looks like it contains an email address or a Stripe secret is refused before it is sent. Runs only while you chat with the assistant. lib/assistant/model.ts calls Anthropic's Messages API through Cloudflare AI Gateway when the deployment configures one, and directly otherwise. The conversation is kept in your browser tab only and is not stored by Sortance. |
Screening model
/v1/screen is the one endpoint where an address and a name may reach a model. It makes two kinds of request, each separate:
- The name check sends the address, the name and the free code checks’ results. It happens only when you send a
nameand the address has passed the gate (valid syntax, a resolving MX, not disposable). - The domain check sends the domain name only, with the mail-provider label Sortance derived for it: no address, no part before the @, no name. It needs no name, happens only after the gate has passed, and is skipped for major consumer mailbox providers. Its answer is cached as a single label against the domain for 30 days, so a domain already seen is normally answered from the cache rather than sent again in that time.
Both are on by default; an account can have them turned off on request, which turns off both, and the free address checks keep working without them.
| Provider | Purpose | Receives | Location |
|---|---|---|---|
| TypeSafe (Jev model) | Two separate requests from /v1/screen: judging whether a supplied name is a real person's name and whether the address plausibly belongs to that person; and judging, from the domain name alone, whether the domain looks like a misspelt provider, a brand lookalike, a placeholder or a throwaway inbox. | Name check: the email address, the name, and the results of the free code checks (including any suggested corrected address), only when a name is supplied and the domain passes the gate. Domain check: the domain name and the mail-provider label Sortance already derived for it, and nothing else (no address, no part before the @, no name), only when the domain passes the gate and is not a major consumer mailbox provider; its label is cached per domain for 30 days, so a repeated domain is normally answered from the cache rather than sent again. Sortance stores neither the name nor the address; only the resulting labels and a keyed hash of the address are kept. | United States |
Infrastructure
| Processor | Role |
|---|---|
| Cloudflare | Compute, storage and delivery. Workers, D1, KV, Durable Objects, Queues, R2 (bulk verification files, 7 days) and Analytics Engine — the whole system runs here. |
| Resend | Transactional email: sign-in links, organisation invitations and, when you tick the box on a bulk job, one "Your list is ready" email with counts and a link, never a row of the file. It receives the recipient's address and the message. |
Integrations you connect
These are your vendors, not ours. Data flows from them to Sortance only because you configured it to, and you can revoke either from the API at any time.
| Integration | What it does |
|---|---|
| Stripe | Outcome source. You give Sortance a webhook signing secret and, optionally, a read-only restricted key; Stripe's subscription, invoice, refund and dispute events become funnel events joined to decisions. |
| RB2B | Push source. Website-visitor identification RB2B sends to an ingest URL on your account. |
Where the disposable-domain lists come from
Sortance decides whether a domain is a throwaway service from public lists it downloads, never by sending your data anywhere. These sources are fetched; none of them receives anything from Sortance. How the lists become the block, signal and archive tiers is on domain validation.
| Source | Lists | What it can do to a check |
|---|---|---|
| disposable-email-domains | disposable_email_blocklist.conf | Block tier: a domain on it ends the check as disposable_domain, unless it is allowlisted or popular and not yet reviewed. |
| disposable/disposable-email-domains | domains, domains_mx, domains_strict, domains_strict_mx | domains joins the block tier. The strict-only entries (anonymous-signup and alias services) form the signal tier, which never ends a check. The _mx variants are subsets and only tag a domain. |
| Legacy email-check export | temp-email-domains (2026-10-07) | Archive only. Kept for provenance and never read when a check runs: most of it no longer resolves, and it lists real providers. |
Popularity guard: Tranco (tranco-list.eu), a research-grade ranking, used only to decide which upstream entries need human review. Its ranking ships with the API; it is not called at request time, and it receives nothing from Sortance.
Your account’s own domain rules sit on top of all of this and apply to your account only.
Enrichment moves the boundary
Verification sends an address to a verification provider and keeps nothing readable. Enrichment changes that in two ways, stated here in plain words.
1. The address is disclosed to an enrichment provider
- It happens only when you ask for a profile: a call to
/v1/enrich, a/v1/decidewithenrich: "always", or a/v1/decidewhose policy reaches a rule that reads an enrichment or ICP field. A policy that only reads verification signals never buys a profile. - Company facts are looked up by domain first. The address itself is sent only for a person lookup, and only when a rule or the request needs a person field.
- Consumer mailboxes (Gmail, Outlook.com and the like) are never sent to an enrichment provider at all.
- Every response names who was asked, in
enrichment.sources, with status, timing and cost per provider. - Choosing which enrichment providers your account may use is not exposed through the API yet. Today every provider configured on the deployment is eligible, asked in the order listed above.
2. The answer is personal data about someone else
A person profile carries the signup’s name, title and LinkedIn URL. It may exist in exactly two places:
- The API response you receive, for that request.
- A cache keyed by a keyed hash of the address, never by the address, for 30 days, after which it expires on its own. Company facts are cached by domain for the same period.
And it is kept out of everything else:
- Not in decision records. What a decision stores for later replay keeps company facts and three non-identifying person fields — seniority, department and country — and drops full name, first name, last name, title, LinkedIn URL and region. Replaying a policy therefore sees those fields as unknown, and says so.
- Never sent to a model. The compilers and the setup assistant see field names and vocabularies, not a profile.
- Not in logs. Log lines on the enrichment and decision paths carry the provider name, timings and an error class, never a profile field.
Person profiles are cached under the same deployment-wide keyed hash the verification cache uses, so a profile bought for one customer can answer another customer’s request for the same address within 30 days. They learn facts an enrichment vendor would sell them anyway; they learn nothing about you or that you asked.
What is stored, and for how long
| Data | Form | Retention |
|---|---|---|
| Verification result | Keyed by a peppered hash of the address, never the address | 7 days, extended to 30 days where the evidence is stable |
| Decision trace | Hashed address, providers asked, verdicts, costs, latency | Readable by you at /v1/traces. No published retention window yet. |
| Connected apps (MCP sign-in) | The app’s name and redirect address, who approved it, the organisation and access level; tokens as SHA-256 only | Until disconnected. Expired codes and tokens are deleted nightly; unused app registrations after 30 days. |
| Domain statistics | Aggregate per domain — no address-level data | Rolling |
| Reported outcomes | Trace id, outcome, SMTP code reduced to its numeric codes | Retained as calibration input |
| Account and session | Your own email address, so you can sign in. A bulk job’s completion email, when you ask for one, is sent to the address of the user whose API key created the job (none for a key with no user), looked up when the job finishes; the job record does not copy it | Until you ask for deletion |
| Enrichment profile | Person facts keyed by a keyed hash of the address; company facts keyed by domain | 30 days, then expires |
| Decision record | Hashed address, policy version, decision, matching rule, ICP score, and the decision context with every personal field removed | No published retention window yet |
| Lists | Domain and pattern entries as you wrote them. Address entries as a keyed hash only, so they cannot be exported back as addresses | Until you delete the entry or the list |
| Domain rules | Domains and an optional note, as you wrote them, for your account only. Never an address: entries and notes containing one are refused | Until you remove the domain |
| Address rules | Address rules are stored as a keyed hash and a masked display form (j•••@acme.com), with an optional note, for your account only. The address itself is never stored, so it cannot be listed back, and the hash itself is never returned: a listed rule is named by an id that is different in every account. Notes containing an address are refused | Until you remove the address |
| Bulk verification files | Your uploaded CSV and the results, stored in Cloudflare R2 in Sortance’s account, readable only with your organisation’s key. The only place Sortance stores raw addresses | 7 days, then deleted. DELETE /v1/bulk/jobs/:id deletes them sooner |
| Bulk job record | The file name and column headers (anything address-shaped in them replaced), row counts and the verdict, reason and email-type counts. Each verified address gets an ordinary decision trace, tagged with the job id | Kept after the files are deleted |
| Funnel events | Trace id, event name, time and a few flat properties. Address-shaped values are refused. From Stripe: ids, status, amounts and currency; the customer’s email is hashed for the join and then dropped | Retained as outcome input |
| Integration secrets | Stripe webhook signing secret and restricted key, encrypted at rest. Never returned by the API after you submit them | Deleted when you revoke the integration |
| Screening record | Keyed hash of the address, whether a name was supplied, the screening labels and reasons, the model version, cost and time. Never the name, and nothing that could recover it (no hash, length or script); only the labels about it | No published retention window yet |
| Domain screening cache | A domain name and the one label the domain check gave it. Nothing about any address or person, and shared across accounts because a domain is not personal data | 30 days, then expires |
| Website-visitor signal | Identified, first seen, page count and source, keyed by your account and a keyed hash of the address | 30 days |
The cache is shared, and here is why that is safe
The verification cache is keyed by a hash of the address under a single deployment-wide secret, with no account component — so one entry is shared across customers. That is deliberate: it is how a domain everyone mails stays cheap for everyone.
The one field that is not account-independent, identity_hash, is excluded from the cache outright and re-derived per caller, because it is salted per account precisely so it cannot become a cross-customer identifier. Storing it in a shared cache would mint that identifier by accident.
Another customer verifying the same address may receive a cached result that your call produced. They learn a deliverability fact about a public address; they learn nothing about you, your list, or that you ever asked.
Security posture
- Every provider call is HTTPS. Sortance never speaks SMTP.
- API keys are stored as SHA-256 only, and are metered on the owning user across every key they hold.
- CORS is restricted to the Sortance dashboard origin. The only wildcard is on the OAuth discovery, registration, token and revocation endpoints, which take no cookies and no API key, so a key that leaked into a browser cannot be spent from an arbitrary page.
- Apps that connect to the MCP server by signing in (OAuth 2.1 with PKCE) get tokens that work only on
/mcp, last an hour, and refresh with rotation; reusing an old refresh token disconnects the app. Codes, tokens and client secrets are stored as SHA-256 only. A connected app acts as the person who approved it, with at most member rights, and stops working the moment that person leaves the organisation. The consent page cannot be framed by another site. - Sign-in is an emailed link — no password to reuse, phish or leak.
- Errors never carry an exception message or stack; they carry a
trace_idthat maps to the real error in our logs. - Stripe webhooks are verified against your endpoint’s signing secret over the raw body, with a five-minute tolerance, and the URL token that identifies your account is stored only as its SHA-256.
- The website-visitor ingest URL is authenticated by its token alone. Whether RB2B signs its webhooks is unconfirmed, so no signature is checked there yet.
- A Stripe event joined to a decision by email rather than by
metadata.sortance_trace_idis matched only to your own account’s decisions from the last 90 days, and is labelled as approximate.
Deletion and access
Outside the 7-day bulk verification files, no raw address is stored, so there is generally nothing address-shaped to return or erase — the cache holds a hash that cannot be reversed, and it expires on its own within 30 days. Your account record and your decision traces are a different matter, and both can be deleted on request.
Lists, list entries and connected integrations are yours to delete directly through the API: DELETE /v1/lists/:name removes a list and every entry, DELETE /v1/domains/blocklist and /v1/domains/allowlist remove domain rules, DELETE /v1/addresses/blocklist and /v1/addresses/allowlist remove address rules, DELETE /v1/bulk/jobs/:id deletes a bulk job’s files before the 7 days are up, and DELETE /v1/integrations/stripe revokes Stripe and deletes the stored secrets.
Questions
Reach us through your account. If your question concerns a specific decision, quote its trace_id.