# Certificate Renewal Deployment Gap Gate (`kingii98/certificate-renewal-deployment-gap-gate`) Actor

Find the renewal that did not reach every server. For each hostname the Actor resolves every A and AAAA address, opens a TLS handshake to each address with the hostname as SNI, and compares the certificate that each address serves with the newest certific

- **URL**: https://apify.com/kingii98/certificate-renewal-deployment-gap-gate.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 run starteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Certificate Renewal Deployment Gap Gate

Find the renewal that did not reach every server.

Automatic renewal works until it does not. A new certificate is issued, one
server in the pool takes it, and one does not. The name still answers, the
monitoring still says "days to expiry: 60", and one address out of four keeps
serving the old certificate until it expires.

This Actor is not an expiry checker. It answers one question for each hostname:
**is the certificate that every address serves the newest certificate that the
CA issued for this name?**

### How it works

For each hostname:

1. It resolves the A and AAAA records over DNS-over-HTTPS, so it sees the whole
   address pool and not one cached address.
2. It opens one TLS handshake to **each address**, with the hostname in the SNI
   extension, and reads the certificate that this one address serves.
3. It reads the Certificate Transparency logs for the exact name and takes the
   newest certificate that is still valid.
4. It compares the two and gives one verdict.

### Verdicts

| Verdict | Meaning |
| --- | --- |
| `current` | Every address serves the same certificate, and the CT logs hold nothing newer. |
| `renewed-not-deployed` | The CT logs hold a newer certificate than one or more addresses serve. The renewal happened and did not reach every server. |
| `mixed-across-ips` | The addresses behind the name serve different certificates. |
| `renewal-overdue` | The certificate is past the renewal point and the CT logs hold no newer certificate. The renewal did not happen. |
| `unreachable` | The name did not resolve, every address was refused, or no address completed a handshake. |

`renewed-not-deployed` wins over `mixed-across-ips`, and `mixed-across-ips`
wins over `renewal-overdue`. The `verdict_reasons` field lists every condition
that holds, so a hostname that is both mixed and behind CT shows both.

The comparison always uses the **oldest** certificate that any address serves.
One laggard address is the whole failure that this Actor reports.

#### Certificate Transparency lag

A CT log shows a certificate minutes to hours after the CA issues it. A
certificate that a server already serves and the log does not show yet is
therefore treated as `current`, never as `renewal-overdue`. A difference below
60 seconds does not count as newer. When both CT sources fail, the run reports
`ct_status: failed`, gives no `renewed-not-deployed` and no `renewal-overdue`
verdict, and still reports `mixed-across-ips`.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `hostnames` | array | `["apify.com", "iana.org"]` | 1 to 300 fully qualified hostnames. Add a port with a colon, for example `api.example.com:8443`. The default port is 443. An IP address is refused. |
| `max_ips_per_hostname` | integer | `4` | 1 to 8. How many addresses of the name get a handshake. |
| `renewal_point_fraction` | number | `0.33` | A certificate is due for renewal when less than this fraction of its lifetime is left. |
| `alert_webhook_url` | string | empty | One public HTTPS URL. One JSON POST goes out when a run finds a gap. |
| `request_timeout_seconds` | integer | `15` | Timeout for one DNS query, one handshake, one CT query and the POST. |
| `concurrency` | integer | `4` | How many hostnames the run checks at the same time. |
| `max_run_seconds` | integer | `240` | Wall-clock deadline for the list. |

Every field has a default, so a run with an empty input works and checks the two
demo hosts. The Actor needs no API key and no secret.

It reads three public sources: **Cloudflare** and **Google** for DNS-over-HTTPS,
**crt.sh** for the CT history, and **Cert Spotter** when crt.sh does not answer.
Cert Spotter sends no serial number, so a fallback comparison uses the validity
dates.

### Output

#### One record for each hostname

| Field | Description |
| --- | --- |
| `target`, `hostname`, `port` | The name that this record describes. |
| `verdict`, `is_gap`, `verdict_reasons` | The verdict, whether it is billed as a gap, and every condition that holds. |
| `reason_code` | `OK`, `DNS_EMPTY`, `DNS_FAIL`, `BLOCKED_TARGET`, `HANDSHAKE_FAIL`, `RUN_DEADLINE`, `CHARGE_LIMIT` or `INVALID_HOSTNAME`. |
| `ip_addresses_checked`, `ip_addresses_reachable` | The addresses behind the name, and the ones that answered. |
| `certificates` | One entry for each address: `ip_address`, `fingerprint_sha256`, `serial_number`, `not_before`, `not_after`, `issuer`, `san_names`, `name_match`, `handshake_ms`, `error`. |
| `renewal_gap_ip_addresses` | The addresses that serve a certificate older than the newest one in CT. |
| `deployed_fingerprints`, `distinct_certificates` | The SHA-256 fingerprints in the pool. More than one means `mixed-across-ips`. |
| `oldest_deployed_*` | The fingerprint, serial, dates and issuer of the laggard certificate. |
| `remaining_lifetime_fraction`, `days_to_expiry`, `renewal_due` | Where the laggard certificate stands against the renewal point. |
| `ct_status`, `ct_source`, `ct_entries_matched` | Whether the CT lookup worked, and which source answered. |
| `ct_serial_number`, `ct_not_before`, `ct_not_after`, `ct_issuer` | The newest valid certificate for the exact name in the CT logs. |

#### One summary record

`gate_pass`, `hostnames_in_input`, `hostnames_checked`, `renewal_gaps`,
`verdict_counts`, `gap_hostnames`, `unreachable_hostnames` and `alert_status`.

`gate_pass` is true only when every hostname has the `current` verdict. A gap
and an unreachable host both turn it off.

A failed gate, an unreachable host and an input mistake are results, not
failures. The run always ends **succeeded**, and the verdict is in the dataset
and in the run status message. Wire `gate_pass` into your pipeline.

### Pricing: pay per event

| Event | Price | When the Actor charges it |
| --- | --- | --- |
| `run-started` | USD 0.01 | Once for each run, before the first query. |
| `hostname-checked` | USD 0.004 | Once for each hostname that the run resolved, handshaked and compared with the CT logs. An **unreachable** hostname is charged here, because the work was done. A hostname that the input got wrong, and one that the run deadline or the charge limit cut off, are not charged. |
| `renewal-gap-flagged` | USD 0.02 | Once for each hostname with a `renewed-not-deployed`, `mixed-across-ips` or `renewal-overdue` verdict. An `unreachable` hostname is never a gap. |

The default maximum total charge is USD 3 for each run. When a run reaches that
limit it checks fewer hostnames; a hostname that it did not check gets the
`CHARGE_LIMIT` reason code, and a gap that the limit did not cover is counted in
`gaps_not_charged` and still reported.

### Schedule it

Run it on a **daily** Apify schedule. Automatic renewal runs on a cycle and each
cycle can miss one server. From 2026 the CA/Browser Forum cuts the maximum
certificate lifetime in steps, so there are more renewal cycles each year and
more chances for one of them to go half way.

### Limits

- 1 to 300 hostnames for each run, and 1 to 8 addresses for each hostname.
- At most 64 KB from one DNS answer and 8 MB from one CT answer.
- The run stops at the `max_run_seconds` deadline.
- Loopback, private, link-local and reserved addresses are never contacted, in
  the hostname list, in the DNS answers and in the webhook URL.
- Plain HTTPS and plain TLS. No browser, no proxy, no key, no stored state.

# Actor input Schema

## `hostnames` (type: `array`):

1 to 300 hostnames. Write one fully qualified name for each line, for example www.example.com. Add a port with a colon, for example api.example.com:8443; the default port is 443. An IP address is refused, because the check needs a name for the SNI extension and for the Certificate Transparency search.

## `max_ips_per_hostname` (type: `integer`):

The Actor resolves the A and AAAA records of the name over DNS-over-HTTPS and opens one TLS handshake to each address, with the hostname as SNI. Raise this for a name behind a large pool of servers.

## `renewal_point_fraction` (type: `number`):

A certificate is due for renewal when less than this fraction of its lifetime is left. The default of 0.33 is the point at which ACME clients renew. A hostname that is past this point and has no newer certificate in the Certificate Transparency logs gets the renewal-overdue verdict.

## `alert_webhook_url` (type: `string`):

Optional public HTTPS URL. The Actor sends one JSON POST when a run finds a gap. Loopback, private and reserved addresses are refused. Leave it empty to read the result in the dataset only.

## `request_timeout_seconds` (type: `integer`):

Timeout for one DNS query, one TLS handshake, one Certificate Transparency query and the webhook POST.

## `concurrency` (type: `integer`):

The largest number of hostnames that the run checks in parallel. Keep this low: the Certificate Transparency sources refuse a burst of parallel queries.

## `max_run_seconds` (type: `integer`):

Wall-clock deadline for the hostname list. A name that the run does not reach gets a record with the RUN\_DEADLINE reason code and is not charged.

## Actor input object example

```json
{
  "hostnames": [
    "apify.com",
    "iana.org"
  ],
  "max_ips_per_hostname": 4,
  "renewal_point_fraction": 0.33,
  "alert_webhook_url": "",
  "request_timeout_seconds": 15,
  "concurrency": 4,
  "max_run_seconds": 240
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "hostnames": [
        "apify.com",
        "iana.org"
    ],
    "max_ips_per_hostname": 4,
    "renewal_point_fraction": 0.33,
    "alert_webhook_url": "",
    "request_timeout_seconds": 15,
    "concurrency": 4,
    "max_run_seconds": 240
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/certificate-renewal-deployment-gap-gate").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "hostnames": [
        "apify.com",
        "iana.org",
    ],
    "max_ips_per_hostname": 4,
    "renewal_point_fraction": 0.33,
    "alert_webhook_url": "",
    "request_timeout_seconds": 15,
    "concurrency": 4,
    "max_run_seconds": 240,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/certificate-renewal-deployment-gap-gate").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "hostnames": [
    "apify.com",
    "iana.org"
  ],
  "max_ips_per_hostname": 4,
  "renewal_point_fraction": 0.33,
  "alert_webhook_url": "",
  "request_timeout_seconds": 15,
  "concurrency": 4,
  "max_run_seconds": 240
}' |
apify call kingii98/certificate-renewal-deployment-gap-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/certificate-renewal-deployment-gap-gate"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/zjdVPyHczHgvJJezV/builds/M5v4miTJcXeKS74nL/openapi.json
