Monitor SSL certificate expiry with a REST API
An expired SSL certificate takes your site offline and shows a browser warning that scares away customers. Let's Encrypt certs auto-renew, but misconfigured DNS, failed cron jobs, and forgotten manual certificates still cause outages. You need a way to monitor expiry dates across all your domains.
The botoi API provides two endpoints for this. One returns certificate details (issuer, validity dates, days until expiry). The other checks HTTPS support and scans security headers. Together, they cover both expiry monitoring and security posture audits.
Get certificate details with /v1/ssl-cert/certificate
This endpoint connects to the domain, reads the TLS certificate, and returns structured data you can parse in any language.
curl -X POST https://api.botoi.com/v1/ssl-cert/certificate \
-H "Content-Type: application/json" \
-d '{"domain": "stripe.com"}' Response:
{
"success": true,
"data": {
"domain": "stripe.com",
"subject": "CN=stripe.com",
"issuer": "C=US, O=Let's Encrypt, CN=E6",
"valid_from": "2026-02-18T00:00:00.000Z",
"valid_to": "2026-05-19T00:00:00.000Z",
"days_until_expiry": 51,
"serial": "04:A3:9B:7C:2D:1E:8F:00:5A:B2:C4:D6:E8:F0:12:34",
"fingerprint": "A1:B2:C3:D4:E5:F6:78:90:AB:CD:EF:01:23:45:67:89",
"san": ["stripe.com", "*.stripe.com"]
}
}
The days_until_expiry field is the one you'll build alerts around. The
san array shows all domains the certificate covers, useful for verifying
that wildcard certs include the subdomains you expect.
Check HTTPS support and security headers with /v1/ssl
Knowing your certificate is valid isn't enough. You also want to confirm that security
headers like HSTS and CSP are in place. The /v1/ssl endpoint handles that.
curl -X POST https://api.botoi.com/v1/ssl \
-H "Content-Type: application/json" \
-d '{"domain": "stripe.com"}' Response:
{
"success": true,
"data": {
"domain": "stripe.com",
"ssl_supported": true,
"protocol": "TLSv1.3",
"headers": {
"strict-transport-security": "max-age=63072000; includeSubDomains; preload",
"content-security-policy": "default-src 'self'; script-src 'self' js.stripe.com",
"x-frame-options": "SAMEORIGIN",
"x-content-type-options": "nosniff",
"referrer-policy": "strict-origin-when-cross-origin"
}
}
}
The ssl_supported boolean confirms HTTPS works. The headers
object surfaces HSTS, CSP, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy.
Missing headers indicate gaps in your security configuration. A TLS protocol below 1.2
is a red flag.
GitHub Actions: weekly SSL check with auto-created issues
This workflow runs every Monday, checks a list of domains, and opens a GitHub issue if
any certificate expires within 30 days. Create
.github/workflows/ssl-check.yml:
name: SSL Expiry Check
on:
schedule:
# Every Monday at 9:00 UTC
- cron: '0 9 * * 1'
workflow_dispatch:
jobs:
check-ssl:
runs-on: ubuntu-latest
steps:
- name: Check SSL certificates
run: |
DOMAINS=("stripe.com" "api.stripe.com" "dashboard.stripe.com")
THRESHOLD=30
FAILURES=""
for DOMAIN in "\${DOMAINS[@]}"; do
RESPONSE=$(curl -s -X POST https://api.botoi.com/v1/ssl-cert/certificate \
-H "Content-Type: application/json" \
-d "{\"domain\": \"$DOMAIN\"}")
DAYS=$(echo "$RESPONSE" | jq -r '.data.days_until_expiry')
ISSUER=$(echo "$RESPONSE" | jq -r '.data.issuer')
EXPIRY=$(echo "$RESPONSE" | jq -r '.data.valid_to')
echo "## $DOMAIN" >> $GITHUB_STEP_SUMMARY
echo "- Expires: $EXPIRY" >> $GITHUB_STEP_SUMMARY
echo "- Days left: $DAYS" >> $GITHUB_STEP_SUMMARY
echo "- Issuer: $ISSUER" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$DAYS" -lt "$THRESHOLD" ]; then
FAILURES="$FAILURES\n- $DOMAIN expires in $DAYS days ($EXPIRY)"
fi
done
if [ -n "$FAILURES" ]; then
echo "::error::Certificates expiring within $THRESHOLD days:$FAILURES"
exit 1
fi
echo "All certificates have more than $THRESHOLD days remaining."
- name: Open GitHub issue on failure
if: failure()
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'SSL certificate expiring soon',
body: 'The weekly SSL check found certificates expiring within 30 days. See the [workflow run](' + context.serverUrl + '/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for details.',
labels: ['infrastructure', 'urgent']
});
The workflow loops through each domain, queries the API, and accumulates failures. If
any certificate falls below the 30-day threshold, it fails the job and creates a GitHub
issue tagged infrastructure and urgent. The job summary shows
the full report for each domain.
Adjust DOMAINS and THRESHOLD to match your setup. The free
tier handles up to 100 requests per day, which covers ~14 domains checked weekly.
Node.js: monitor multiple domains in a script
For integration into your own monitoring stack, here's a Node.js script that checks an array of domains in parallel and flags certificates close to expiry:
const DOMAINS = [
"stripe.com",
"api.stripe.com",
"dashboard.stripe.com",
"docs.stripe.com",
];
const THRESHOLD_DAYS = 30;
async function checkCert(domain) {
const res = await fetch("https://api.botoi.com/v1/ssl-cert/certificate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.BOTOI_API_KEY,
},
body: JSON.stringify({ domain }),
});
const { data } = await res.json();
return { domain, ...data };
}
async function checkAll() {
const results = await Promise.all(DOMAINS.map(checkCert));
const expiring = results.filter(
(r) => r.days_until_expiry < THRESHOLD_DAYS
);
console.log("SSL Certificate Report");
console.log("=".repeat(50));
for (const r of results) {
const status =
r.days_until_expiry < THRESHOLD_DAYS ? "WARNING" : "OK";
console.log(
`[\${status}] \${r.domain} - \${r.days_until_expiry} days left (expires \${r.valid_to})`
);
}
if (expiring.length > 0) {
console.log(
`\n\${expiring.length} certificate(s) expiring within \${THRESHOLD_DAYS} days.`
);
}
return { results, expiring };
}
checkAll();
Run this on a cron schedule or integrate it into your existing health-check pipeline.
The Promise.all call checks all domains concurrently, so the total
execution time stays close to the latency of a single API call.
Slack webhook alert when a certificate expires soon
Pair the certificate check with a Slack incoming webhook to notify your team when a certificate needs attention:
async function sendSlackAlert(domain, daysLeft, validTo) {
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `:rotating_light: SSL certificate for \${domain} expires in \${daysLeft} days`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: [
"*SSL Certificate Expiry Warning*",
`Domain: \`\${domain}\``,
`Days remaining: *\${daysLeft}*`,
`Expires: \${validTo}`,
].join("\n"),
},
},
],
}),
});
}
// After running the certificate check
async function checkAndAlert() {
const DOMAINS = ["stripe.com", "api.stripe.com"];
for (const domain of DOMAINS) {
const res = await fetch(
"https://api.botoi.com/v1/ssl-cert/certificate",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.BOTOI_API_KEY,
},
body: JSON.stringify({ domain }),
}
);
const { data } = await res.json();
if (data.days_until_expiry < 30) {
await sendSlackAlert(domain, data.days_until_expiry, data.valid_to);
}
}
}
checkAndAlert(); The message includes the domain, days remaining, and expiry date. Your team sees the alert in Slack and can investigate before the certificate expires.
Full security audit: combine both endpoints
For a complete picture, run both endpoints in parallel per domain. This gives you certificate expiry data and security header coverage in a single pass:
async function auditSsl(domain) {
const [cert, ssl] = await Promise.all([
fetch("https://api.botoi.com/v1/ssl-cert/certificate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.BOTOI_API_KEY,
},
body: JSON.stringify({ domain }),
}).then((r) => r.json()),
fetch("https://api.botoi.com/v1/ssl", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.BOTOI_API_KEY,
},
body: JSON.stringify({ domain }),
}).then((r) => r.json()),
]);
const issues = [];
// Certificate checks
if (cert.data.days_until_expiry < 30) {
issues.push(`Certificate expires in \${cert.data.days_until_expiry} days`);
}
// Security header checks
const headers = ssl.data.headers || {};
if (!headers["strict-transport-security"]) {
issues.push("Missing HSTS header");
}
if (!headers["content-security-policy"]) {
issues.push("Missing Content-Security-Policy header");
}
if (!headers["x-frame-options"]) {
issues.push("Missing X-Frame-Options header");
}
return { domain, cert: cert.data, ssl: ssl.data, issues };
}
// Run audit across all domains
const domains = ["stripe.com", "api.stripe.com"];
Promise.all(domains.map(auditSsl)).then((results) => {
for (const r of results) {
console.log(`\n\${r.domain}:`);
console.log(` Certificate: \${r.cert.days_until_expiry} days left`);
console.log(` TLS: \${r.ssl.protocol}`);
console.log(` Issues: \${r.issues.length === 0 ? "None" : r.issues.join(", ")}`);
}
}); This script reports certificate expiry, TLS protocol version, and missing security headers for each domain. Add it to your weekly security review or wire it into your incident response dashboard.
Key points
-
/v1/ssl-cert/certificatereturns the issuer, validity dates,days_until_expiry, serial number, fingerprint, and SAN list for any domain's TLS certificate. -
/v1/sslchecks HTTPS support, reports the TLS protocol version, and scans for HSTS, CSP, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy headers. -
Both endpoints work anonymously at 5 requests per minute. Pass an
X-Api-Keyheader for higher limits. - A weekly GitHub Actions cron job with a 30-day threshold catches renewal failures before they become outages.
- Combine both endpoints to run certificate expiry monitoring and security header audits in a single script.
Frequently asked questions
- How do I check SSL certificate expiry via API?
- Send a POST request to https://api.botoi.com/v1/ssl-cert/certificate with a JSON body containing the domain. The response includes the certificate's valid_from, valid_to, and days_until_expiry fields. No openssl CLI or manual browser inspection needed.
- What is the difference between /v1/ssl-cert/certificate and /v1/ssl?
- The /v1/ssl-cert/certificate endpoint returns certificate details: issuer, subject, serial number, validity dates, and days until expiry. The /v1/ssl endpoint checks whether HTTPS is supported and scans security headers like HSTS, CSP, X-Frame-Options, and Referrer-Policy. Use the first for expiry monitoring and the second for security audits.
- Can I check SSL certificates without an API key?
- Yes. Anonymous access allows 5 requests per minute and 100 requests per day with IP-based rate limiting. No signup or API key required. For higher throughput, paid plans start at $9/month.
- How often should I check SSL certificate expiry?
- Weekly is a good baseline for most teams. Let's Encrypt certificates renew every 90 days with auto-renewal at 30 days before expiry. A weekly check with a 30-day warning threshold gives you enough time to fix renewal failures before the certificate expires.
- Does this work with self-signed or internal certificates?
- The API connects to the domain over the public internet, so it works with any certificate served on port 443. Self-signed certificates will return certificate details, but the issuer field will show the self-signed entity instead of a trusted CA.
Try this API
SSL Certificate Check API — interactive playground and code examples
More integration posts
Start building with botoi
150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.