I built this actor; it's a paid tool on Apify with a free trial credit.
Expired certificates still take sites down, and the usual reason is boring: the domain was on nobody's list. Agencies, IT teams and anyone who inherited a pile of client or company domains have the same problem. You don't need a monitoring product to get a first answer. You need one table: domain, certificate issuer, expiry date, days left.
My Website Tech Stack Detector does a TLS handshake and DNS lookups for every site it checks, so that table falls out of it for free. My earlier post used it to find Shopify stores in a domain list. This one ignores the web technologies and uses only the certificate and DNS fields.
The fields
Every result row carries these, alongside the tech stack:
{
"domain": "deathwishcoffee.com",
"tlsIssuer": "Let's Encrypt / YE1",
"tlsExpires": "2026-10-24T15:59:32.000Z",
"emailProvider": "Barracuda",
"emailSenders": ["Microsoft 365", "Zendesk", "HubSpot", "SendGrid"],
"dnsProvider": "GoDaddy",
"mxRecords": ["d344701a.ess.barracudanetworks.com", "d344701b.ess.barracudanetworks.com"]
}
That's a real row from my run on 24 September 2026. emailProvider comes from the MX records, emailSenders from the SPF record (the services the domain allows to send mail as it), and dnsProvider from the nameservers.
What 46 real domains looked like
I ran a mixed list of 46 domains: stores, blogs, news sites, framework homepages. 44 came back with a certificate (one domain doesn't exist, and neverssl.com is plain HTTP on purpose).
Who issued the certificates:
| Issuer | Sites |
|---|---|
| Let's Encrypt | 21 |
| Google Trust Services | 8 |
| GlobalSign | 6 |
| DigiCert | 4 |
| Amazon | 3 |
| Sectigo | 1 |
| Microsoft | 1 |
Days left on the day of the run:
| Sites | |
|---|---|
| Under 35 days | 6 |
| Under 60 days | 20 |
| Over 90 days | 12 |
The median was 66 days. The shortest were kyliecosmetics.com and deathwishcoffee.com at 30 days each, the longest cnn.com at 178. None of that is alarming by itself: all 21 Let's Encrypt certificates had between 30 and 89 days left, which is what a healthy 90-day certificate with auto-renewal looks like. The row to worry about is a short remaining life on an issuer that sells one-year certificates, because that one is probably renewed by hand.
Email was less varied than I expected: of the 45 sites that loaded, 24 use Google Workspace, 6 Microsoft 365, and 3 sit behind Proofpoint.
The code
pip install apify-client
import csv
from datetime import datetime, timezone
from apify_client import ApifyClient
WARN_DAYS = 21 # flag anything closer than this
with open("domains.txt", encoding="utf-8") as f:
domains = [line.strip() for line in f if line.strip()]
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("rel8ble/website-tech-stack-detector").call(run_input={
"urls": domains,
"includeDns": True, # TLS, MX, SPF and nameserver fields
"includeEvidence": False, # smaller output, we only want the headline fields
})
now = datetime.now(timezone.utc)
rows = []
for s in client.dataset(run["defaultDatasetId"]).iterate_items():
if s.get("error"):
rows.append((s["domain"], "ERROR", s["error"], "", "", ""))
continue
expires = s.get("tlsExpires")
days = (datetime.fromisoformat(expires.replace("Z", "+00:00")) - now).days if expires else None
rows.append((
s["domain"],
"NO TLS" if days is None else days,
s.get("tlsIssuer") or "",
(expires or "")[:10],
s.get("emailProvider") or "",
s.get("dnsProvider") or "",
))
# errors and missing certificates first, then soonest expiry
rows.sort(key=lambda r: r[1] if isinstance(r[1], int) else -1)
with open("cert-audit.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["domain", "days_left", "issuer", "expires", "email_provider", "dns_provider"])
w.writerows(rows)
for r in rows:
if not isinstance(r[1], int) or r[1] < WARN_DAYS:
print("CHECK", *r)
Schedule it weekly on Apify or from cron, and send the CHECK lines wherever your team reads alerts. Dead domains come back as an error row instead of vanishing, which is useful in itself: a domain that stopped resolving is something you want to hear about.
Why keep the email columns
-
Offboarding and migrations. If you're moving a company from one mail host to another,
emailProvideracross every domain it owns shows which ones were missed. -
Spoofing surface.
emailSenderslists every service the SPF record trusts. A long list with tools nobody uses any more is worth trimming. -
Inherited domains.
dnsProvidertells you where to log in to fix any of the above.
Things to keep in mind
-
It reads the certificate served on the homepage host. Other subdomains (
api.,shop.,mail.) have their own certificates; add them to the list as separate entries. - Short life isn't bad news on its own. Let's Encrypt certificates last 90 days and Google Trust Services ones about the same, so 30 days left is normal there. Flag by issuer, or use a threshold under 30 days like the code above.
- Behind a CDN you see the CDN's certificate. For Cloudflare-proxied sites that's the edge certificate, which is the one visitors get, but it says nothing about the origin.
-
emailProvideris the inbound gateway. A domain filtered by Proofpoint, Mimecast or Barracuda shows that name, not the mailbox host behind it.emailSendersoften fills in the rest.
Cost
$3.00 per 1,000 results, one result = one website. Domains that fail to load are saved as an error row but not charged. A weekly check of 200 domains is about $0.60 a week, and Apify's free plan includes $5 of monthly credit.
Website Tech Stack Detector on Apify ยท Python examples: website-tech-stack-detector-python on GitHub
This article was drafted with AI and published by me.
Top comments (0)