Privacy: exactly what we store
Last updated: October 26, 2026 (every stored field listed, read from the database schema and the log code)
- Visit log (30 days): time, route, status, which of our addresses was used, a keyed hash of a checked hostname and a visitor code that changes every day. Never your IP address or the hostname itself.
- Paid-tier button count (30 days): only the time of each press and one word for where it came from and what happened.
- Monitoring records, only if you start 24/7 monitoring: your monitored hostnames, your alert webhook URL, a SHA-256 hash of your manage token, the latest check results and an alert log; on the paid plan also your Stripe customer ID and a SHA-256 hash of your checkout session ID. Kept until you remove them or the subscription ends.
- Ended-subscription hashes, only after a paid subscription ends: SHA-256 hashes of its Stripe subscription ID, its checkout session hash and its manage token hash, so a late payment notice cannot switch it back on.
- Pending recovery link, at most one hour: a SHA-256 hash of a lost-link recovery link you asked for.
- Cache of public certificate data from crt.sh for hostnames that were checked, with the time it was fetched, and for the README badge each hostname's certificate expiry date under a keyed hash of the hostname (7 days).
- Share pages you published by pressing "Share audit" or Copy in the badge box: the public page, its preview image and the time it was published.
- Web server access log (14 days), written by the nginx server in front of this site: this one does include IP addresses.
Below, every field of every record is named exactly as it is stored, with what it holds, why we keep it and when it is deleted. Things kept only in server memory (never written to disk) are listed at the end of section 1.
1. Free check
- No cookies of any kind, and no analytics, ad or tracking scripts.
- List check (/list): the hostnames you paste are checked and sent back in the same response, then dropped. They are not in the visit log, not even hashed (it records only the route /api/check-list), and not stored anywhere. The CSV download is built in your browser.
Visit log (one file per day, visits-YYYY-MM-DD.jsonl)
For each check, page request or README badge load that reaches us through our public proxy (000256.xyz/api, or the older address), one line with exactly these six keys. A badge in a GitHub README is usually fetched by GitHub's image proxy, so that line's visitor code is GitHub's, not the reader's. Preview images, monitoring, checkout and requests from our own server are not logged. Each daily file is deleted once it is 30 days old, so no line is kept 30 days.
| Key | What it holds and why | Deleted |
|---|---|---|
t | Time of the request (UTC, to the second), to count visits per day. | With the daily file, after 30 days |
route | Which page or check was used, as a fixed name such as /api/check, /api/check-list, /api/audit, /privacy or /api/badge/{host}.svg (literally {host}). Never the hostname itself. | With the daily file, after 30 days |
status | The response status (for example 200), so we see failing checks. | With the daily file, after 30 days |
host | A one-way hash (HMAC-SHA256, 16 hex characters) of the hostname you checked, keyed with a random secret that stays on our server. It lets us count different hostnames without storing their names. For a badge load, the badge's hostname. Empty for list checks and pages. | With the daily file, after 30 days |
visitor | A visitor code: a one-way hash (12 hex characters) of your IP address as our proxy reports it, mixed with a random salt that exists for one UTC day only. It lets us count distinct and returning visitors within one day. Your IP address itself is never written, and codes from different days cannot be linked to each other or to you. | With the daily file, after 30 days; the salt that made it is deleted the next day |
via | Which of our two web addresses the request came in on (000256.xyz or the older one), so we know when the old links stop being used. | With the daily file, after 30 days |
Two small secret files make those hashes: .hostname-key, one random key for the hostname hash, kept so the same hostname always gives the same hash; and .visitor-salt-YYYY-MM-DD, today's random salt, deleted at the first request of the next day. Neither holds anything about you.
Paid-tier button count (one file per day, paid-clicks-YYYY-MM-DD.log)
Each press of the monitoring checkout or upgrade button adds one line with exactly these two fields, so we can count how many people want the paid tier. No hostname, destination, IP address, visitor code or cookie is recorded. Each daily file is deleted once it is 30 days old.
| Field | What it holds and why | Deleted |
|---|---|---|
time | Time of the press (UTC, to the second). | With the daily file, after 30 days |
source | One word. For a press that came through our public proxy: created (a Stripe checkout page was opened), not_enabled (checkout was switched off) or refused (the form had an error). Not counted: own (our own tests: they came from our own servers or a headless browser, which is decided from the IP address and browser name at that moment; neither is written), local (did not come through the proxy), probe (our own automated check of the payment path), and proxy (lines written before 27 October 2026, which did not record this). | With the daily file, after 30 days |
Cache of public certificate data
- Certificate Transparency list: for a checked hostname, the certificate list crt.sh returns is saved in one file named by a hash of the hostname. It holds the hostname, the time it was fetched and public certificate data only (issuer, dates, serial numbers, names on the certificate), nothing about who checked it. It is used for 6 hours, so repeat checks don't hit crt.sh again; after that the next check of that hostname replaces it. The file is not otherwise deleted.
- README badge results (
badge-cache.json): for each hostname whose badge was loaded or that was checked with the free check (so its badge loads instantly), the time of the check and only the certificate's expiry date, or whether the badge was grey "unreachable" or "invalid host". It is stored under the same keyed hostname hash as the visit log, never the hostname. A result is used for 6 hours (a grey one 30 minutes), then refreshed by the next load; an entry is deleted once it is 7 days old. Nothing about who loaded the badge. - Certificate form: for the certificate a host serves, whether crt.sh lists it as a final certificate or only as a precertificate, saved by serial number with the time checked. Reused for 7 days (final) or 6 hours (precertificate only), then replaced at the next check. No hostname and nothing about you.
Share pages
Only when you press "Share audit" (or Copy in the badge box), we publish a public page at 000256.xyz/audit/<hostname>/ with that hostname's certificate and domain expiry dates and its preview image, and note the hostname with the time it was published, so the page is refreshed instead of duplicated. It contains nothing about you, is marked noindex for search engines, and stays up so the link keeps working.
Web server access log
The nginx web server that serves 000256.xyz writes its standard access log for every request: your IP address, the time, the requested address including anything after the "?" (so a manage link token or a Stripe checkout session ID in the address is in it), the response status and size, the referring page and your browser's user agent. The log is rotated daily and deleted after 14 days. Only we read it, to count visits. The visit log above is our app's own, separate log.
Kept in server memory only (never written to disk)
- Registry (RDAP) results: 5 minutes per domain, so we don't hit registries repeatedly.
- Lost-link limits: for up to one hour, the time of each recovery request with the client address our proxy reports, the subscription it went to and, for receipt checks, the email entered, to cap guessing. Gone when the server restarts.
- Test alert limits: "Send test alert" next to the monitoring form works before you have a manage link, so to stop it being used to flood someone's webhook we keep, for up to one hour, the time of each such test with the webhook URL it went to and the client address our proxy reports. Gone when the server restarts.
2. 24/7 monitoring (free and paid)
Monitoring data lives in one database with five tables. Each table below lists every column it has. Nothing is stored before monitoring starts: free monitoring starts only after a test alert reached your destination, paid monitoring only after Stripe confirms the payment.
| Column | What it holds and why | Deleted |
|---|---|---|
id | Row number, so the manage page can remove one hostname. | With the hostname |
management_token | The SHA-256 hash of your manage token, never the token itself. It is how your manage link finds your monitors. Between payment and your first visit to the return page it holds unclaimed: plus the checkout session hash instead. The token exists only in your manage link, so we cannot see it or send it to you again; a lost link can only be replaced. | With the hostname; replaced by a new hash when you get a new link |
customer_email | Left from an earlier design. Our code never writes it: it is always empty, so the address you gave Stripe is not stored here. | Always empty |
stripe_session_id | Paid plan only: a SHA-256 hash of your Stripe checkout session ID, never the ID itself, so the link from your checkout finds the monitors that payment switched on, and one payment makes one set of monitors. Empty on the free plan. | With the hostname |
stripe_subscription_id | Left from an earlier design. Our code never writes it: it is always empty. The subscription ID is only ever stored hashed, after the subscription ends (see ended subscriptions). | Always empty |
domain | The monitored hostname (with :port if you gave one), which our server connects to and looks up. | When you press Remove next to it, stop monitoring, cancel, or the subscription ends |
port | The port to connect to (443 unless you gave another). | With the hostname |
channel | The kind of alert destination: Discord, Slack, Microsoft Teams, Google Chat, ntfy, Telegram or your own HTTPS webhook, so we know how to format the alert. | With the hostname; overwritten when you save a new destination |
target | Your alert webhook URL (Discord or Slack webhook URL, Teams Workflows webhook URL, Google Chat space webhook URL, ntfy topic URL, Telegram bot URL with chat ID or your own HTTPS URL), which is where alerts are sent. A webhook URL can contain a secret, so the manage page shows it only shortened. | With the hostname; overwritten when you save a new destination |
interval | The billing: free, month or year, for its limits. | With the hostname |
plan | Which plan: free (up to 3 hostnames) or pro (up to 10), for its hostname limit. | With the hostname |
subscriber_id | Free monitoring, only once you press "Upgrade to 24/7 Pro": a random code (cms_ plus 32 hex digits) that names no one. It is the one thing the upgrade checkout tells Stripe about you, so the payment notice can find these monitors without Stripe ever seeing your manage token or its hash. | With the hostname |
check_interval_seconds | How often the hostname is checked (86400, once a day). | With the hostname |
created_at | When monitoring of this hostname started; also caps free sign-ups per hour. | With the hostname |
last_checked_at | When the last check ran, shown on the manage page. | With the hostname |
next_check_at | When the next check is due, for the scheduler. | With the hostname |
active | Whether the row is being checked (1). | With the hostname |
tls_status | Last certificate result as a level (pending until the first check, then a level such as success, warning or danger), for the manage page and alerts. | With the hostname; overwritten at each check |
tls_days_remaining | Days until the served certificate expires. | With the hostname; overwritten at each check |
tls_expiry | Expiry date of the served certificate. | With the hostname; overwritten at each check |
tls_issuer | Who issued the certificate (for example Let's Encrypt). | With the hostname; overwritten at each check |
rdap_status | Last domain registration result as a level. | With the hostname; overwritten at each check |
rdap_days_remaining | Days until the domain registration expires, when the registry publishes it. | With the hostname; overwritten at each check |
rdap_expiry | Domain registration expiry date. | With the hostname; overwritten at each check |
rdap_registrar | The domain's registrar, as the registry publishes it. | With the hostname; overwritten at each check |
last_alert_sent_at | When the last real alert for this hostname was sent, so an ongoing TLS problem alerts at most once a day. | With the hostname |
last_alert_type | Which alert that was (for example tls_warning_14d or undeployed_renewal). | With the hostname |
renewal_alerted_serial | The serial number of the renewed certificate (public, from the CT logs) we last alerted you about as not deployed, so that alert goes out once per renewed certificate and is never repeated. | With the hostname; replaced when a newer renewal is alerted |
tls_problem | A TLS problem the last check found (for example a renewed certificate in the CT logs that the server does not serve yet), or empty. | With the hostname; overwritten at each check |
destinations | The same alert destination (kind and URL) once more, in the format the manage page saves. | With the hostname; overwritten when you save a new destination |
stripe_customer_id | Paid plan only: your Stripe customer ID, an opaque reference such as cus_…. It lets the manage page read your plan, price and renewal date from Stripe (shown, not stored), lets our server ask Stripe once a day whether your subscription is still active, tells Stripe not to renew it when you press "Cancel subscription", and matches a receipt in lost-link recovery. It holds no card data and none of the details you typed at Stripe checkout. Empty on the free plan. | With the hostname, when the subscription is cancelled or ends |
| Column | What it holds and why | Deleted |
|---|---|---|
id | Row number. | With the hostname it belongs to |
monitor_id | Which monitored hostname (row of monitors) the alert was for. | With the hostname it belongs to |
domain | The hostname the alert was about. | With the hostname it belongs to |
alert_type | Which alert it was, so we can tell whether alerts work. | With the hostname it belongs to |
channel | The kind of destination it went to. | With the hostname it belongs to |
target | The alert webhook URL it was sent to. | With the hostname it belongs to |
status | Whether delivery succeeded (delivered or failed). | With the hostname it belongs to |
details | The destination's answer or the delivery error (for example an HTTP status), to debug failed alerts. | With the hostname it belongs to |
created_at | When the alert was sent; also so the same expiry alert is never sent twice for one certificate or registration. | With the hostname it belongs to |
attempts | How many tries the delivery took: a temporary error at the destination (no answer, too many requests, a server error) is tried up to three times. | With the hostname it belongs to |
alert_ref | For a renewed-but-not-deployed certificate alert, that certificate's public serial number, so each of your destinations gets that alert once. | With the hostname it belongs to |
| Column | What it holds and why | Deleted |
|---|---|---|
ref_hash | A SHA-256 hash of one identifier of the ended subscription: its Stripe subscription ID, its checkout session hash or its manage token hash (one row each). It lets us ignore a delayed or repeated payment notice from Stripe for that subscription, so your monitors are not created again. It contains no name, hostname or destination and cannot be turned back into the ID. | Kept: no expiry, because it names nothing |
ended_at | When the subscription ended. | Kept with its hash |
| Column | What it holds and why | Deleted |
|---|---|---|
token_hash | A SHA-256 hash of the one-time recovery link sent to your alert destination, never the link itself. | When used, when a newer link replaces it, when monitoring stops, or after one hour |
owner | Which subscription the recovery link opens (its manage token hash). | With the recovery link |
expires_at | When the recovery link stops working (one hour after it was made). | With the recovery link |
| Column | What it holds and why | Deleted |
|---|---|---|
id | Row number. | With the destination or when the subscription ends |
management_token | A SHA-256 hash of the manage token this destination belongs to, never the token itself. | When the destination is removed or when the subscription ends |
label | A human-readable label you gave this destination (for example Client A or NOC). | When the destination is removed or when the subscription ends |
channel | The kind of alert destination: Discord, Slack, Microsoft Teams, Google Chat, ntfy, Telegram or your own HTTPS webhook. | When the destination is removed or when the subscription ends |
target | The webhook URL where alerts are sent. | When the destination is removed or when the subscription ends |
created_at | When this destination was added. | When the destination is removed or when the subscription ends |
| Column | What it holds and why | Deleted |
|---|---|---|
name | A table name (monitors or alert_logs). SQLite keeps it so row numbers are never reused. | Kept: names nothing |
seq | The highest row number used so far in that table. | Kept: names nothing |
"Send test alert", next to the monitoring form and on the manage page, posts one sample alert to the webhook URL and shows you the HTTP status it answered. Nothing about a test alert is written to disk. We do not store your name, an account, a password or card details. Treat your manage link like a password: anyone who has it can see your hostnames and a shortened destination, and change or stop your monitoring.
Deleting: "Remove" next to a hostname deletes that hostname's row and its alert log.
"Stop monitoring" (free) deletes every hostname, the destination, the alert log and any pending recovery link under your manage link at once.
"Cancel subscription" (paid) tells Stripe to end your subscription at the end of the period you paid for (Stripe's cancel_at_period_end), and the page shows the end date Stripe answered. You are not charged again; your hostnames stay monitored until that date. When Stripe ends the subscription, Stripe tells our server, or our daily check with Stripe finds it, and we delete the same data as "Stop monitoring". If Stripe does not accept the cancellation, the page says so and names the operator's contact.
A subscription that ends on Stripe's side for another reason (a payment fails for good) is handled the same way.
After a paid subscription ends, only the hashes in ended_subscriptions remain.
Lost manage link
At /manage/recover you can get a new manage link in two ways. Either way the new token is stored only as its SHA-256 hash and the old link stops working.
- Through your alert destination: you enter a monitored hostname. If it is monitored, our server sends a one-time recovery link (valid one hour) to the alert destination already saved for it. The page gives the same answer whether or not the hostname is monitored. The hostname you type is not stored.
- With your Stripe receipt (paid plan): you enter the email address you paid with and the invoice or receipt number from Stripe's receipt email. Our server sends both to Stripe to find a customer with that email and a paid invoice or payment with that number, then compares the Stripe customer ID with the one stored for your monitors. Neither the Stripe email nor the receipt number is written to disk.
3. Stripe (paid plan only)
Seller name: Payment is processed by Stripe. The seller and card statement show Mätkyt.fi, which operates 000256.xyz. It is the business name on our Stripe account; receipts from Stripe carry the same name.
You pay on Stripe's own checkout page (checkout.stripe.com). Stripe collects your email address and card details there under its own privacy policy; we never see your card number.
What we send to Stripe when you press the subscribe button, stored by Stripe as metadata on the checkout session so your monitors can be created after payment: the hostnames you entered, your alert channel and destination URL, the plan (Pro), the billing interval and a tag saying the session is CertMonitor's. No manage token: it does not exist yet.
Coming back from Stripe: Stripe sends you to 000256.xyz/api/manage?session_id=cs_… Our server asks Stripe for that checkout session. Only if Stripe answers that it is complete and paid does the server switch your monitors on (storing the Stripe customer ID and the session ID's hash), create a random manage token, store its SHA-256 hash and send you to your private manage link, 000256.xyz/api/manage/<token>. A missing or wrong token there is a 404 that shows nothing. Opening the link from your checkout again creates a new token, and the previous manage link stops working. Any other session ID is refused and nothing is stored.
Upgrading free monitoring to Pro from your manage page: the checkout session's metadata holds only the random subscriber_id above and the CertMonitor tag (Stripe's page also shows your hostnames above the Subscribe button). Pressing the button again while that checkout is still open reuses it. Just before your browser goes to Stripe, the manage page puts your manage token in this browser tab's sessionStorage (not a cookie: it is never sent to any server, Stripe's page cannot read it, and it is gone when the tab closes). If you back out, Stripe sends you to 000256.xyz/api/manage?checkout=cancelled, where the page reads the token back, deletes it from sessionStorage and reopens your dashboard; in another tab it just says the checkout was cancelled. After payment, Stripe's notice upgrades the same hostnames and alert destination in place to the 10-hostname limit, and your manage link stays the same.
Manage page and cancelling: each time you open your manage link, our server asks Stripe for your subscription using the Stripe customer ID and shows the plan, price and renewal date Stripe returns; none of it is written to disk. "Cancel subscription" sends Stripe only your subscription's ID with cancel_at_period_end=true. We do not use Stripe's billing portal.
What we do not store: your name, billing address, card details, the email address you give Stripe, the plain checkout session ID or the plain subscription ID. The manage page sends your token to our server in a request header, so it does not appear in the addresses of those requests; the token and a checkout session ID do appear in the address of the manage page itself, which the web server's access log records for 14 days.
Once a day our server also asks Stripe whether each paid subscription is still active, using only the Stripe customer ID, and deletes the monitors of any subscription that has ended.
Every five minutes our server also reads CertMonitor's completed checkout sessions from Stripe (it only reads; it never charges, refunds or changes anything there). A session that Stripe reports paid but that has no monitors yet, for example because you closed the tab before the return page loaded, gets its monitors switched on exactly as above, storing the same fields and nothing else from the session (not your email, name or address). A session whose subscription has ended has its monitors deleted as described under cancelling.
One confirmation message: when paid monitoring switches on, one message titled "CertMonitor is on" goes to your alert destination, naming your hostnames and where to get a new manage link. It is sent once per payment and nothing about it is stored.
4. Who else sees what
- The host you check receives a normal TLS connection from our server.
- crt.sh (the public Certificate Transparency search run by Sectigo) receives the hostname and its wildcard parent (for www.example.com also *.example.com) from our server, at most once every 6 hours per hostname, to list the certificates that cover it.
- rdap.org and the RDAP registries receive a lookup for the domain from our server: rdap.org forwards it to the registry IANA lists for that TLD.
- Your alert destinations (Discord, Slack, Microsoft Teams, Google Chat, Telegram, ntfy, your own endpoint) receive test alerts and alerts, under their own privacy policies. A Teams alert goes through the Microsoft Power Automate workflow behind your Teams webhook URL, which posts it to your channel. With ntfy.sh, anyone who knows your topic name can read those messages.
- Stripe receives what section 3 lists.
- One address: this site's pages only call 000256.xyz/api, on the same site, which our web server forwards to the CertMonitor backend on our own server. Your browser does not contact any other site to run a check. No third-party API or tracking service receives this data.
- Older links (legacy address): badges and check links made before 24 October 2026 point at dota2.fi/api, our other domain. They keep working and reach the same backend, with the same visit log described above; nothing new links there.
All of these requests come from our server, not your browser, so these services do not see your IP address.