Privacy policy
Last updated 2026-08-25.
DomainCanary stores what it needs to read your DMARC reports, and this policy says what that is.
Who we are
DomainCanary is a DMARC monitoring service at domaincanary.com. It is operated by Final Dynamics LLC, doing business as DomainCanary.
The contact for this policy is contact@domaincanary.com.
What the service is
Customers publish a DNS record naming an address we control. Mailbox providers (Google, Microsoft, Yahoo and others) then send us aggregate reports about mail claiming to be from that customer's domain. We parse those reports and email the customer a weekly digest.
The free plan covers one domain. The paid plans add domains and features: realtime alerts, one consolidated digest instead of one per domain, the option to skip the digest on a week where nothing needs attention, and the DNS cleanup views in the dashboard. They are billed monthly through Stripe: card details are entered on Stripe's own pages and held by Stripe, never by us. Stripe is the seller of record for the payment, under its Link brand, which is why receipts arrive from Link and why Stripe collects a billing address.
Eight free tools run on the public site and need no account: checkers for DMARC, SPF and DKIM, one checker that reads all three at once, a generator for an SPF record and one for a DMARC record, a pre-flight that compares a proposed SPF record against the published one, and an analyzer that reads a DMARC aggregate report you paste or upload. None of them stores what you submit; what each one does with it is under The free tools.
What we store
Account
When you create an account we store:
- the email address
- a scrypt hash of the password (N=32768), never the password itself
- created_at and verified_at
- whether the account is marked as an operator account
- the source tag: the page that sent you to signup, from the
?from=tag on links such as?from=tool-clean. It is a page name, not an ad identifier. No third-party ad or analytics network is involved. - the IP address the signup request came from and the browser user-agent string it carried, written when the account is created through the email and password form. The user-agent is stored as sent, cut at 512 characters. An account created by signing in with Google holds neither. They are there so that signups made by one automated run can be matched to each other; nothing else on the account makes two such accounts look related. Both are cleared 90 days after the account was created. See How long we keep it.
- seven notification answers: whether the account wants the weekly digest, alerts for any new sender, alerts for new failing senders, sender regression alerts, product updates and marketing email, and whether clean weeks are recorded on the dashboard instead of emailed. Each domain can override the digest, three alert and quiet-week answers for itself.
- the account digest address and whether that mailbox has confirmed, when one is set instead of the account's own address, and, while a new address is waiting to confirm: that address, a SHA-256 hash of its confirmation token, and the token's expiry
- which plan the account holds, which is what its domain limit is read from
- your Stripe customer reference, assigned at the account's first checkout. It is written when the checkout session completes, whether or not the payment went through, so an abandoned checkout can leave one on an account that never bought anything.
- when the account started the one 14 day trial it gets, when that trial runs out, and which page the button was on (
billing,data-readyand the like). Nothing clears these trial stamps. They are how we know the trial has been used. - three trial-email stamps recording that the trial's opening, reminder and closing emails are settled for this account, so none of them can go twice.
- product-notification stamps recording whether we have sent the one-time note that your reports now cover enough time for the DNS cleanup pages to have something to say, and whether you closed the banner in the dashboard that says the same thing.
- the same two product-notification facts for the second and last note, sent at most once, when your oldest domain's reports cover the full 90 day window the DNS cleanup pages judge against and its SPF record has a term that window would call a removal. Deciding that reads the record from public DNS, the same read the sending routes page makes.
Subscription and payment
Buying a plan happens on Stripe's pages. No card number, expiry date or security code reaches this product, and none is stored here.
Opening a checkout is the moment anything of yours first goes to Stripe. We send them your account's email address, the id of the plan's price, and our own numeric user id, which comes back with the completed session so we know which account paid.
Stripe then collects two things from you that we never ask for and never receive: a name and a billing address. It needs them to work out the sales tax, VAT or GST it charges and pays to the tax authority. They are held by Stripe, they sit on the customer record in our Stripe account, and this product does not copy them into its own database or show them anywhere in the dashboard. Stripe may also offer to save your details as a Link account, which is an account with Stripe rather than with us.
What we store is what Stripe tells us: the Stripe customer id on the account row above, and one row per account holding the Stripe subscription id, the price id, the subscription status (active, trialing, past_due, canceled and the rest), when the current paid period ends, when we last wrote the row, and the timestamp of the newest Stripe event we applied, which is what a later event about the same subscription is checked against so a retried delivery cannot undo a newer one.
The card, the invoices and the payment history sit with Stripe. What Stripe receives is under Who else holds the data.
Sign in with Google
Signing in with Google is optional, and the buttons exist only where a Google client id and secret are configured.
If you use it, Google returns an id token to us over our own connection to Google. We store two things from it: Google's subject identifier for that Google account, which is the stable id a later sign-in is matched on, and the email address the token asserted, as it stood when the link was made. The row also carries the date it was created.
We check the token and do not keep it. Neither the id token nor the access token from that exchange is stored, and neither is written to a log.
Linking a Google account to a DomainCanary account that already exists sends a notice to the account address, because a link nobody was told about would be a silent takeover. The one address it is kept from is one that has already told us to stop mailing it.
A Google sign-in in progress uses a second cookie, described under Cookies.
Session
We store a SHA-256 hash of the session token, never the token itself, plus created_at and expires_at.
The cookie is dc_session. It is host-only and HttpOnly, with SameSite=Lax, path /, a fixed 30 day Max-Age, and Secure when the site is served over HTTPS. It is a login cookie. DomainCanary sets no advertising, analytics, or tracking cookies.
We set it after a successful sign-in, after you verify your address, and after you reset your password. Expired session rows are swept from the database on a later request, at most once every ten minutes.
Single-use tokens
We store SHA-256 hashes of email-verification and password-reset tokens, with an expiry and a used_at. Verification links are valid for 24 hours. Password reset links are valid for 1 hour.
A spent link is deleted, not just marked used. A cleanup pass runs every 15 minutes and removes every token row that has been used or has expired. People often assume a used password-reset link lingers in a database somewhere. Here it does not.
Unsubscribe links have their own records, so a mailbox can stop our mail without signing in. Each record holds a token hash, the email type the link stops, the domain or account it belongs to, the recipient email address, the date it was created, and the date it was used. Issuing a later token for the same domain or account clears records older than 400 days. Without a later token they stay.
Domains
For each domain on an account we store:
- the domain name and created_at
- the owning user
- the local part of the reporting address the customer publishes in public DNS
- a per-domain secret used internally, which is never published
- the token the customer publishes in DNS to prove control
- verified_at
- the digest email address and whether that mailbox has confirmed
- while an alternate digest address is waiting to confirm: that address, a SHA-256 hash of its confirmation token, and the token's expiry. Confirmation tokens are valid for 7 days. An expired one stays in the row until it is replaced, cleared, confirmed, or the domain is deleted.
- a legacy random identifier still generated and stored for every domain
- five per-domain notification overrides: whether this domain departs from the account's settings for the weekly digest, each of the three alert categories, and quiet weeks. An empty override follows the account setting.
Aggregate reports
Per report we store the reporting organisation's name, the report id, the period, the DMARC policy observed, and the time the report reached us.
Per record inside a report we store a sending IP address, a message count, the disposition, DKIM and SPF results, the header-from value, the envelope-from value, the DKIM and SPF signing domains, and up to 10 DKIM authentication results with their selectors.
We store the header-from and envelope-from values as the report supplied them, up to a length limit. The envelope-from value can be a full mailbox address rather than a domain on its own. We do not trim it to a domain.
The raw report email
Mailbox providers send reports as email. Cloudflare Email Routing and our Email Worker receive the whole message and forward it to the origin, which holds the message and its attachments in memory while it parses them.
Two stores receive it from there. The archive bucket listed under Who else holds the data keeps the whole message, byte for byte, filed by domain and by the day it arrived, for 12 months. A report is sent once and cannot be asked for again, so the archive is what lets us read back what a provider actually sent: after a parser fix, or when a customer asks where a number came from. A message our parser rejects is archived all the same, and a report file uploaded by hand in the dashboard is archived the same way. The database keeps the parsed fields above, plus a count of how many bytes the message was; the raw message, its headers, and its attachments stay out of it.
When a message is rejected, the reason can name an attachment filename, and the forwarder logs the reply.
Sending IP addresses in those reports
A DMARC aggregate report names the IP addresses that sent, or spoofed, mail as the customer's domain. Those IPs belong to whoever was on the sending side of that mail. That can be the customer's own servers, a provider the customer sends through, or someone forging the domain. The report does not say which.
Mailbox providers generate these reports and send them to the address in the customer's DNS. The data is unsolicited: we do not choose its contents, and we cannot obtain consent from the operator of each IP it names. That is how DMARC reporting works.
An IP address can be personal data under the GDPR. The IPs in a report can therefore be personal data about people who have no account with us and no contract with us.
We store those IPs as they arrive, parse them, and show them to the account that owns the domain they were reported against. We do not pool report data across customers. We do not sell it, rent it, or publish it.
If you run a domain with us, you are the person who published the record that sends this data to us. Whether you owe notices of your own to the people behind those IPs, or to your own senders, depends on the law that applies to you; we do not send notices on your behalf.
Your own IP address, when you use the site
We use the visitor IP address to throttle abuse on the free tools and on the signup, login, resend, and password-reset endpoints.
The key sits in the running process's memory, with a 10 minute window for the tools and a 15 minute window for the auth endpoints. Old entries are dropped the next time the limiter runs, and everything in it disappears when the process restarts.
Your IP also reaches the origin access log, described under Logs.
Creating an account through the email and password form writes that IP address to the account row as well, together with the browser user-agent string the request carried. Signing up with Google writes neither. Both sit on the row until they are cleared 90 days after the account was created. What they are for, and how they are stored, is under Account.
A signup submission that trips one of the bot checks on that form writes the IP address and the user-agent to a record of the trip as well. That record exists whether or not an account was created, and it is described under Anti-abuse ledgers.
Derived and operational records
From the reports and from running the service we also store:
- per-domain sender records: each sending IP with first-seen and last-seen, and running counts of its messages that passed and failed DMARC. These IPs come from the reports, so they have the same third-party character as the report records.
- sent-digest records: one per digest we sent, with its period and send date. The rendered HTML is kept for 56 days and then pruned. The record itself stays; it is the send mutex. A week the digest-skip option held back is recorded this way too, with nothing rendered in it.
- daily ingest totals: per domain per day, the emails accepted, bytes received, records parsed, records inserted, quarantined emails, and whether a high-volume notice has been claimed for that day. The byte figure counts the whole inbound message. Reports uploaded by hand in the dashboard do not update this ledger.
- failing-sender alert records: per domain, one per sending IP that arrived new and failing DMARC, with the count of its failing messages, when the domain first saw it, when the record was written, and when we mailed about it. A domain holds at most 200 records waiting to be mailed about; past that, a new source gets no record and is reported in the weekly digest instead. The IP comes from the reports, so it has the same third-party character as the report records.
- new-sender alert records: per domain, one per sending IP that arrived new and passing DMARC, with the count of its passing messages, when the domain first saw it, when the record was written, and when we mailed about it. The same 200-record waiting ceiling applies as for failing-sender alerts. The IP comes from the reports, so it has the same third-party character as the report records.
- sender-regression alert records: per domain, one per known sending IP whose mail had been passing DMARC and then started failing, with the count of failing messages, when the regression was noticed, when the record was written, when we mailed about it, and when the source was later seen passing again. The same 200-record waiting ceiling applies as for failing-sender alerts. The IP comes from the reports, so it has the same third-party character as the report records.
- the log of alert emails sent: per domain, one record per alert email we sent, with which of the three kinds it was, how many sending addresses that message named, and when it went out. It holds no IP addresses: those stay in the three kinds of alert record above and in the reports. Records here are written once and never edited. A record is written after the message is accepted for delivery, so a send that failed leaves none.
- DNS record change history: per domain, what your own published SPF record and DKIM keys said, kept as change events. Each record holds the kind of DNS record, the zone we read it at and the name under that zone (both empty for the SPF record, which sits at the root of your domain; for a DKIM key, the signing domain your reports name and the selector), the value we read, and when we first read that value. An event is written only when the value differs from the one before it, and an empty value means the lookup answered with no record there. This is your own public DNS; see the DNS lookups section.
- DNS cleanup page snapshots: per domain and per product, the verdict summary the sending routes or signing keys page reached the last time it ran, holding the counts it printed, a short list of the terms or selectors it named with the verdict against each, and when it was computed. The summary that one replaced, and that older summary's date, sit beside it; one step back and no further. A snapshot is written when a DNS cleanup page renders, so the page and the weekly digest can say later what they saw without reading every report again. It is derived from your own domain's reports and your own published DNS, and nothing else goes into it.
- records of tripped signup checks: one per signup submission that tripped one of the bot checks on the signup form, holding the email address as it was submitted, which check fired, whether the submission was blocked or allowed through, the IP address the request came from, the browser user-agent string it carried, cut at 512 characters, the source tag from the link the person arrived on, and when it happened. What the checks are, and why the records are kept, is under Anti-abuse ledgers. A record is deleted 90 days after it is written, and sooner when the address it names has an account that is then deleted. This data category also holds at most 5,000 records: anyone can write to it without an account, so past that ceiling the oldest go first.
- the DMARC watch state: per domain, the last check and last success times, how many checks in a row have missed, the observed state or error, whether each warning has already gone into a digest, whether our reporting address is still in the DNS record, whether an external party is authorised to receive reports, the current DMARC policy, and any pending change to it.
Addresses that refused our email
Two tables record a refusal.
The suppression table stores a canonical mailbox key, a reason (bounce, complaint, or declined), and a date. An unsubscribe is not a suppression: it turns the matching notification setting off, and the settings pages turn it back on.
The digest-decline table stores a hash of the declined confirmation token, the email address, and a date. It carries no reason. It exists so that pressing the same decline link twice gives the same answer.
Both are tied to no account. See What survives deletion.
Anti-abuse ledgers
Two ledgers stop one mailbox being flooded through signup or confirmation.
The auth-mail send ledger records a mailbox key, the kind of message, and a timestamp. The digest-confirmation send ledger records a mailbox key and a timestamp, with no message kind.
The same cleanup pass that clears spent tokens runs every 15 minutes and deletes auth rows older than 14 days and confirmation rows older than 7 days. Those two figures are enforced maximums, not intentions.
The signup form carries two checks of its own, and a third field that only watches. One check is a box hidden from anyone filling the form in a browser, which a person therefore never ticks; an unticked box sends no value at all, so arriving ticked is the whole signal. The other is a hidden stamp of the moment the form was drawn: a submission arriving less than two seconds after that was not typed by a person. A submission that ticks the hidden box, or whose stamp is too recent, missing, unreadable, or in the future, is discarded. No account is created and no email is sent.
A discarded submission is answered with the same page a real signup gets, so whatever sent it cannot tell the two apart. An error message would say precisely what to change.
The third field is a hidden text box that stops nothing. When it arrives filled the signup proceeds as normal, account and confirmation email and all. It is there to find out whether a field of that kind would ever catch a real person, before anything is allowed to depend on one.
All three write a line to the application log naming which of them fired and the IP address the submission came from. No email address is in that line. Those lines sit with the rest of the application log, under Logs, and last as long as it does.
All three also write a record to the database. It holds which check fired, whether the submission was blocked or allowed through, the address it was submitted with, the IP address and the browser user-agent the request carried, the source tag from the link the person arrived on, and the time. The two armed checks mark a record blocked; the field that only watches marks it allowed, because that signup went through. Unlike the log line, the record holds the email address. It is the record of a tripped signup check listed under Derived and operational records, and it is deleted 90 days after it is written, or sooner if the address it names has an account and that account is deleted.
The rows are kept so a rejection can be read back. The same answer that stops a bot learning it was caught stops a person learning it: somebody a check fired on by mistake waits for a confirmation email that was never sent, and the form gives them nothing to say so. The row holds the address they typed, which is what makes writing to them possible. Without it, a rejection that caught a real signup would leave nothing anybody could act on.
Retired waitlist
The retired early-access waitlist keeps the email address submitted, the domain submitted, the date, and the source tag from the link the person arrived on. Nothing writes new rows to it.
Logs
The origin writes an access log. It records the client IP address, request metadata, and the URL.
Single-use credentials are stripped before the line is written: verification, reset, digest confirm and decline tokens in token=, the authorization code and state value on the Google sign-in callback, and the one-click unsubscribe token in the URL path, are all replaced with REDACTED. Other things people put in a URL do reach the log, including the domain and selector typed into the DNS checkers. Email capture forms submit by POST, so the address is not put in the URL.
The access log rolls at 20 MB and five rolled files are kept. That is a size limit rather than a time limit, so the number of days it covers moves with traffic.
The application also logs to the container's output. Those lines can contain a recipient email address, a monitored domain, a reporting organisation and report id, header-from domains from a report, ingest dates and volume totals, and error detail. No rotation or retention period is configured for those logs today.
DNS lookups we make
The checkers, the two record generators, the SPF pre-flight, domain verification, the daily watch on your DMARC record, and the DNS cleanup views in the dashboard all send the queried name to a recursive DNS resolver. The checkers and the signing keys page also send a DKIM selector.
Once a day we also read each verified domain's own SPF record and the DKIM keys for the selectors its reports name, on every plan, and store the value when it differs from the last one we read. That is at most 21 queries per domain per day: one for the SPF record, and one each for up to 20 selectors. Nothing is guessed here: a selector is queried only when this domain's own reports name it, unlike the signing keys page, which also tries a short list of common names.
Anything that walks an SPF record queries the domains that record includes, which belong to the senders it names rather than to you. The signing keys page follows a selector's CNAME to wherever it points, which for a delegated selector is a sending provider's zone.
The free tools
The four checkers take the domain, and the DKIM checker its selector, in the URL. That is what puts them in the access log, as described under Logs.
The two record generators, the SPF pre-flight and the report analyzer are POST forms. What you type or upload is in the request body, which no log line touches, and every answer is sent with Cache-Control: no-store.
The analyzer is the one that receives a file. A pasted or uploaded aggregate report is parsed in memory and dropped when the response is written: no database row, no file on disk, no copy in a cache. That report is somebody's mail data, and it names third-party sending IPs the same way the reports we receive by email do. The limits are 5 MB on the submission, 8 MB decompressed, and 10,000 records; a report that breaks a limit or fails to parse is refused and the page says why. If a parse fails in a way the code did not expect, the error log gets the name of the exception and nothing out of the file. The name of the file you uploaded is echoed back to you on the page, shortened, and is not logged.
None of the free tools writes to the database. Their only ledger is the per-IP throttle counter in memory described above, which all eight share.
What this list does not cover
A domain typed into the four free checkers reaches the access log through the URL, as described above. Nothing else stores it, and what goes into the other four tools does not reach the log at all.
Support email you send to contact@domaincanary.com sits in Google Workspace, and is kept like ordinary business correspondence.
What we do not do
- No advertising, no third-party analytics, no tracking pixels, no tracking scripts. That covers the application code we write. Cloudflare sits in front of the site and its zone features are outside that code.
- Our public pages ship no executable JavaScript. Article pages carry JSON-LD structured data in a script tag, which is data for search engines and does not run. One first-party script exists, on the logged-in domain setup page only. It copies a DNS value to the clipboard and makes no network request.
- We do not sell or rent customer data, publish reports, or pool report data across customers. We do pass data to the infrastructure providers listed below, because that is how the service runs.
- No card data. Card details are entered on Stripe's pages and stay with Stripe. We hold the subscription state described above and never a card number.
- No CAPTCHA, no third-party fraud service, no session replay, no heatmaps.
Why we hold it
We hold account, session, token, and domain data so you can sign in, prove control of a domain, and receive the digest.
We hold reports, the records inside them, per-domain sender records, daily ingest totals, the DMARC watch state, and sent-digest records so we can parse what mailbox providers sent and mail you the weekly digest.
We read that same report data to answer the questions the dashboard asks of it. The DNS cleanup views are the clearest case: they line up what your reports say about a sender or a selector with what your DNS says about it today, so a verdict is per mechanism and per selector rather than one number for the domain. The reports read are your own domains' reports. One thing is written down: DNS cleanup page snapshots keep the summary each of those pages last computed, so the page and the digest can say afterwards what they saw without working it out again.
We hold the trial stamps and the four product-notification stamps so the single trial an account gets is counted once, and so each email about it is sent once.
We hold the log of alert emails sent so the dashboard can show you which alerts went out and when. The alert records themselves are working state and are reused when a sender breaks a second time, so they cannot be the record of an email you already had.
We hold DNS record change history so a page can answer when one of your records changed. DNS keeps no history of its own: a zone file has no past and a resolver's memory is measured in hours, so the trail has to be collected before anybody needs it.
We hold the subscription state so an account gets the plan it is paying for, and a linked Google identity so a returning sign-in reaches the account it belongs to rather than a second one.
We hold the visitor IP throttle keys and the anti-abuse ledgers so signup and confirmation cannot be used to flood a mailbox, and so the free tools cannot be hammered.
We hold the record of a tripped signup check so a rejection can be looked at afterwards, and the person written to when the check turns out to have caught a real signup.
We hold the signup IP address and user-agent so a run of automated signups can be recognised as one run. An account created by a script looks like any other account on its own; those two fields are what makes two of them match.
We hold suppressions and digest declines so we do not write to an address that already refused. The reasoning for those two is in What survives deletion.
Who decides
We decide what the service stores, how long it keeps it, and how it is secured. Customers do not instruct us on any of that. Mailbox providers send the reports straight to us, at an address we control, because the customer published that address in their own DNS. We never receive the data from the customer.
Automated decisions
Nothing here scores you, ranks you, or profiles you. The digest reports what mailbox providers sent.
One automated control runs on ingest: a per-domain daily circuit breaker that stops processing a domain's forwarded report mail once the day's volume passes its budget. It acts on a domain's report volume. It reaches no conclusion about a person.
How long we keep it
The published retention floor, which this policy does not undercut: at least 12 months of report-level detail, source by source, plus daily totals per sending source for the life of the account, on every plan including free.
Today the code goes further than the floor. Nothing deletes reports or report records by age. They stay until the domain or the account is deleted.
Rendered digest HTML is kept for 56 days. The digest scheduler prunes it on each run, so a stopped scheduler delays the prune until it runs again.
A cleanup pass runs every 15 minutes and enforces six of these limits: expired sessions go, used and expired single-use tokens go, auth-mail send-ledger records go at 14 days, digest-confirmation send-ledger records go at 7 days, records of tripped signup checks go at 90 days, and the signup IP address and user-agent are emptied from the account 90 days after it was created. Those are maximums. The last of the six clears two fields rather than deleting a record, unlike the five before it: the account itself stays.
Records of tripped signup checks have a second limit the 90 days does not describe: at most 5,000 are held, and past that a new record pushes out the oldest. A burst of trips therefore shortens the life of what came before it, and the ceiling is there because the endpoint that writes these records is open to anyone.
The subscription state and any linked Google identity stay for the life of the account and go when it is deleted. So do the trial stamps and the four product-notification stamps: they are what stops a second trial and a repeated email.
DNS cleanup page snapshots are not deleted by age. There is at most one per domain per product, and the next render of that page replaces the one before it. They go when the domain or the account is deleted.
An alert row of any of the three kinds that has been mailed about is deleted 90 days after that mail, by the same 15 minute pass. A row still waiting to be mailed about is not deleted by age; it goes when the domain or the account does.
One sender-regression row outlives the 90 days on purpose: one that was mailed about and whose source has never been seen passing again. That row is the only record we hold that the outage was already reported, and deleting it would mail you a second time about a failure that never ended. It goes when the source recovers, 90 days later, or when the domain or the account is deleted.
A record in the log of alert emails sent is deleted 90 days after the mail it records, by that same 15 minute pass. Deleting one changes nothing about whether an alert is sent; the three kinds of alert record decide that on their own.
DNS record change history is not deleted by age. When a record last moved is the whole question the history answers, and a change from three years ago answers it. Each DNS record keeps at most 200 change events, and past that the oldest go first. They all go when the domain or the account is deleted.
Sessions last 30 days.
Verification tokens are valid for 24 hours, password reset tokens for 1 hour, and digest confirmation tokens for 7 days. The first two are deleted once spent or expired. A digest confirmation token lives in the domain row rather than the token table, so an expired one stays there until it is replaced, cleared, confirmed, or the domain is deleted.
When you delete the account, the rows listed under Deletion are erased in the same transaction. The exceptions are under What survives deletion.
Email we send
Transactional mail: address verification, password reset, a notice when someone tries to sign up with an address that already has an account, digest-address confirmation requests, and a notice to the account address when a Google account is linked to it.
An address other than the account's own receives a confirmation request. It must confirm before it receives a digest. The person at that address can refuse without having an account.
A digest is eligible only when the domain is verified, the weekly digest is on for it, a recipient resolves (the domain's own digest address, else the account's), that mailbox has confirmed, and that mailbox is not suppressed. A suppression comes from a permanent bounce, a spam complaint, or a decline. The weekly schedule and a duplicate-send guard also have to allow the run.
On a paid plan a domain also gets realtime alerts between the weekly digests, sent to that same confirmed digest address. The account settings turn them off everywhere, and each domain's settings can override that either way. Two things trigger one: a source nobody has seen before showing up failing DMARC, and a source that had been passing for that domain starting to fail. An alert is eligible on the same terms as the digest, and a domain sends at most one a day whichever kind it is.
Every digest, every alert, and both product-update notes carry List-Unsubscribe and the RFC 8058 one-click POST header, plus a visible unsubscribe link. Unsubscribing turns the matching notification setting off in our own database.
Your rights
What you can do yourself, right now
- Download everything the export covers:
/dashboard/account/export. See Export. - Delete the account:
/dashboard/account/delete. See Deletion. - Stop any of our email types: the notification settings on the account page, each domain's settings, or the unsubscribe link and one-click header in the mail itself.
- Refuse a digest sent to an address that is not the account's own, with no account and no sign-in.
What to ask us for
Email contact@domaincanary.com. We act on a request that comes from the account's own address, and we ask for proof if it comes from anywhere else.
Ask for a copy of what we hold, a correction, deletion, a restriction on what we do with your data, or an objection to a particular use of it.
The product has no self-serve way to change the account email address. Email us if you need it changed, and expect us to check the request is real before acting on it.
If a report names your IP and you are not a customer
DMARC reports name sending infrastructure, so an IP you operate can appear in a report we hold for someone else's domain.
Against that IP we hold what the report said: the IP, a message count, a disposition, DKIM and SPF results, the header-from and envelope-from values, the signing domains, and DKIM selectors. We hold no name, no account, and nothing that tells us who runs the IP. We cannot look you up from it, and we cannot confirm an IP is yours.
Email contact@domaincanary.com and we will tell you what a report contains for an IP you can prove you operate.
Complaining about us
Email contact@domaincanary.com first and we will answer.
You can also complain to a data protection regulator instead of, or as well as, writing to us.
Children
DomainCanary is for people who run a mail domain. We do not ask anyone's age and we hold no age data. We do not knowingly create accounts for children. If you tell us an account belongs to a child, we delete it.
If there is a breach
If we find that account data or report data has been exposed, we email the address on the account. That mail says what happened, what we know so far, and what to do about it. We send it whether or not a law requires it.
Legal process
Nobody outside the parties listed under Who else holds the data gets customer data from us. A binding legal order is the only thing that would change that.
Export
/dashboard/account/export returns a JSON download.
It includes the account profile with its digest address, that address's confirmation state, the seven notification answers the account gave, and the signup IP address and user-agent for as long as the account still carries them; each domain's name, dates, the local part of its reporting address, the token published in DNS to prove control, its digest address and confirmation state; reports and their records; per-domain sender records with first-seen and last-seen; and digest periods and send dates.
It leaves out the password hash, session tokens, single-use token records, the operator flag, the per-domain secret used internally, the legacy identifier stored for every domain, the rendered digest HTML, the archived original report files, the daily ingest totals, the DMARC watch state, pending confirmation fields, the per-domain notification overrides, the passing and failing counters on the sender records, unsubscribe link records, suppression and decline records, both abuse ledgers, the records of tripped signup checks, retired waitlist records, the plan, the Stripe customer reference, the trial stamps and the four product-notification stamps on the account, the subscription record, all three kinds of alert record (new-sender, failing-sender and sender-regression), the log of the alert emails those records produced, the DNS record change history, the DNS cleanup page snapshots, and the linked Google account records.
Four of those omissions have a reason worth stating. A linked Google account record holds an opaque subject id Google issued to us rather than anything the customer wrote, and the address beside it is the account's own, which the file already carries. An alert record of any kind adds nothing the file does not already hold: its source IP, message count and first-seen date are the sources and report records themselves, and the dates left over are our own record of writing the alert record, mailing about it, and seeing a failing source pass again. The DNS record change history holds no content of yours that the file does not already imply: the values are your own published DNS records, and what we add is the dates we read them on, which are our observations rather than your data. A DNS cleanup page snapshot is our own reading of those same records against your reports, worked out again from scratch whenever the page is opened, so it holds nothing you gave us.
The file carries every domain, report, record, source row and digest period the account holds, whatever the count. It is streamed as it is built, so its size is not bounded by what fits in memory. Until 2026-08-28 it stopped at 20 domains and 500 reports per domain, which meant an account on a plan allowing more domains than that could not export all of its own data. A category left out altogether has nothing to report, so the file cannot flag it. The list above is the full account of what is missing.
Deletion
/dashboard/account/delete requires the account's own address typed back and the password. An account created by signing in with Google has to set a password through the reset flow first, because the form asks for one.
If the account holds a running subscription, we cancel it at Stripe before anything is deleted, and if that cancellation fails nothing is deleted at all. A subscription Stripe no longer holds is the one exception: there is nothing left to cancel, so the deletion goes ahead rather than leaving an account that can never be erased. It then deletes, in one transaction:
the account, its sessions, its single-use tokens, its domains, reports, records inside those reports, DKIM results inside those records, sent-digest records, per-domain sender records, failing-sender alert records, new-sender alert records, sender-regression alert records, the log of alert emails sent, DNS record change history, DNS cleanup page snapshots, unsubscribe link records, daily ingest totals, the DMARC watch state, the subscription record, linked Google account records, the auth-mail and digest-confirmation send-ledger records filed under the account's own mailbox, and any retired waitlist record or record of a tripped signup check whose address resolves to that mailbox.
Two things about that are worth stating plainly.
It removes rows from the live database. It does not reach into the origin access log, the application logs, mail held by our sending provider, the raw report archive, or the machine snapshots and off-site backups described below. Those copies age out on their own schedules. The database is not byte-wiped either: the application runs no secure-delete or VACUUM pass after the transaction. Deletion does reach Stripe, but only to cancel: the customer, the cancelled subscription and the invoices stay as Stripe's own record of payments that happened, kept on Stripe's terms rather than ours.
What survives deletion
Deleting the account puts your data into three groups.
Almost everything goes at once. Every row listed under Deletion is gone when the transaction commits.
Two refusal records are permanent. The suppression list and the digest-decline list are not erased. A suppression row is a mailbox key, a reason (bounce, complaint, or declined), and a date. A decline row is a token hash, an email address, and a date. Neither is tied to an account.
They survive because a deleted-then-recreated account must not be able to email an address that already refused. That protects the person who asked not to be contacted. It is also a hard requirement of bulk sender rules. This is a legitimate-interest retention of a minimal record for the protection of the person who asked not to be contacted. If the departing user's own address is in those tables, it stays.
Confirmation-ledger rows about other people's mailboxes stay for up to seven days. If a domain on the account sent a confirmation request to an address that was not the account holder's, the ledger row for that send is filed under the recipient's mailbox, not under the account. Deletion clears the rows filed under the account holder's own address and leaves those. The 7 day cleanup pass removes them.
That is deliberate. The row exists so that one mailbox cannot be mail-bombed through the confirmation flow, and it counts against a daily limit protecting the person who received the mail. If deleting an account wiped it, anyone could clear a victim's limit by deleting and recreating an account, which turns an erasure feature into a way to attack a third party. The row is also not really the departing account holder's data: it is a record about somebody else, kept to protect them, and it expires on its own within a week.
Who else holds the data
These parties run the infrastructure.
| Party | Role | What it receives | Location |
|---|---|---|---|
| DigitalOcean | The virtual server that runs everything | The live database, the access log, the application logs, and whole-machine snapshots | nyc3, United States |
| Cloudflare | Authoritative DNS, reverse proxy in front of the site, and Email Routing that receives the inbound DMARC reports | Visitor IP addresses and request metadata, and the complete inbound report email | Edge, location not established by our own records |
| Amazon Web Services (SES) | Sending outbound email | The recipient address and the full message | us-east-2, United States |
| Cloudflare R2 | Off-site database backups and the raw report archive | A copy of the database, and every report email and uploaded report file as it arrived | Eastern North America (ENAM) |
| Stripe | Seller of record and payment processing for the paid plans | Your email address, our own user id and the plan's price id, the card details you enter with them, the name and billing address it collects to work out tax, and the subscription record | Stripe, Inc., United States |
| Sign in with Google, for accounts that use it | The sign-in request, which tells Google that one of its accounts signed in to DomainCanary | Data region not established by our own records | |
| Google Workspace | The mailbox that receives support email sent to us | Support correspondence | Data region not established by our own records |
Stripe's own privacy policy is at stripe.com/privacy. An account that never opens a checkout sends Stripe nothing, and nothing goes to Google for sign-in unless somebody uses that button.
The origin database is on the DigitalOcean server in nyc3, the SES region is us-east-2, and the backup bucket is in Cloudflare R2's Eastern North America region. Our own records do not establish where Cloudflare processes traffic, where Stripe processes payments, or where Google holds mail and sign-in data, so this policy does not claim a processing region for any of them. Customers outside the United States are sending their data to an origin in the United States.
If the business changes hands
If the service is sold or transferred, the data described here goes with it, and whoever takes it over is bound by this policy until they publish a replacement.
You get notice before that happens, at the account address, with time to export and delete first.
Security
- Passwords hashed with scrypt (N=32768)
- Sessions stored as hashes only, and every emailed link stored as a hash of the token rather than the token
- Single-use credentials stripped out of the URL before an access log line is written
- Browser form requests that rely on the login cookie are checked for a same-origin Origin or Referer. One-click unsubscribe requests, report ingest, and provider webhooks carry their own credentials instead: a token, a shared secret, or a signed envelope.
- Per-IP and per-recipient rate limits
- Public browser traffic terminates TLS at the origin's reverse proxy, and the Cloudflare to origin path is HTTPS. Inside the machine, the reverse proxy reaches the app over a private network without TLS. Outbound mail to SES goes over HTTPS.
- The origin firewalled to Cloudflare's ranges
- Every ingest path has per-request size, decompression, and record budgets. The forwarded-email path adds a per-domain daily circuit breaker. Reports uploaded by hand in the dashboard skip that daily breaker and keep the per-request limits.
- The app and backup containers run with all Linux capabilities dropped and no new privileges. The reverse proxy and mail containers do not set those options.
- Two security reviews were done and their findings fixed
No SOC 2. No ISO 27001. No penetration-test report. No cyber insurance we know of.
Cookies
DomainCanary sets three cookies. Two of them exist only to sign you in, and the third remembers how you asked the pages to look.
dc_session is described under Session.
dc_oauth is set only when you start a sign-in with Google. It carries the OAuth state value, the PKCE verifier, and the page tag the sign-in started from, between the redirect out to Google and the callback back. It is HttpOnly with SameSite=Lax and path /, expires after 10 minutes, is Secure when the site is served over HTTPS, and is deleted when the callback runs. A callback the rate limiter refuses leaves it to expire on its own. It holds no email address.
dc_theme is set when you pick light or dark in your account settings. It holds that one word and lasts a year. It is SameSite=Lax with path /, and Secure when the site is served over HTTPS. It is not HttpOnly, because the value carries no authority: it decides which colours a page is drawn in and nothing else. Picking auto again deletes it, and with no cookie the pages follow the setting on your device. Nothing about it is stored in the database, and it is not read by anything but the code that renders the page.
None of the three is an advertising, analytics or tracking cookie.
Changes
If this policy changes, the new version carries a new "last updated" date.
A change that reduces what we promise here follows the notice rule in the terms of service: existing users hear about it before it takes effect.
Contact
Final Dynamics LLC, doing business as DomainCanary.