Skip to content
integration

Monitor SSL certificate expiry with a REST API

| 6 min read
Green padlock icon on a browser address bar
Photo by Towfiqu barbhuiya on Unsplash

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.

Server monitoring dashboard with uptime graphs
Photo by Luke Chesser on Unsplash

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/certificate returns the issuer, validity dates, days_until_expiry, serial number, fingerprint, and SAN list for any domain's TLS certificate.
  • /v1/ssl checks 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-Key header 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.