# Webhook Sender IP Range Drift and Allowlist Gap Report (`kingii98/webhook-sender-ip-range-drift-and-allowlist-gap-report`) Actor

Fetch the webhook source IP ranges that SaaS providers such as GitHub, Stripe, Datadog, PagerDuty and Bitbucket publish, and compare them with the CIDR blocks that your firewall, WAF or ingress allows. The Actor reports the published ranges that your allo

- **URL**: https://apify.com/kingii98/webhook-sender-ip-range-drift-and-allowlist-gap-report.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

## Webhook Sender IP Range Drift and Allowlist Gap Report

This Actor finds the webhook source IP ranges that your firewall blocks.

Many SaaS providers send webhooks from published IP ranges. GitHub and Stripe are two examples. You allow these ranges at a firewall, a WAF or an ingress. When a provider adds a range and your allowlist does not, deliveries from the new range fail. The provider retries for some time and then stops. You do not get a message.

For each provider, the Actor does these steps:

1. It gets the range list that the provider publishes (one HTTPS GET, at most 1 MB).
2. It compares the list with the CIDR blocks that you allow.
3. It compares the list with the list of the last run.

The Actor never sends a request to your webhook receiver. It reads only the public range lists.

### What you get

- **Gap**: the published ranges that your allowlist does not fully cover. Deliveries from these addresses are blocked. The row also gives the exact uncovered blocks inside each range.
- **New gap**: the gap ranges that no earlier run reported. Only a new gap sends an alert and adds a charge.
- **Stale**: the allowlist entries that touch no published range. The provider no longer uses these addresses, so you can remove them.
- **Added and removed**: the ranges that the provider added or removed since the last run.
- **Gate**: `gate_pass` is `true` only when the Actor read every list and your allowlist covers every published range.

### Built-in provider catalog

We fetched each list on 2026-09-11 and it gave a machine-readable range list.

| Preset ID | Source | What the Actor reads |
|---|---|---|
| `github` | `https://api.github.com/meta` | The `hooks` field |
| `stripe` | `https://stripe.com/files/ips/ips_webhooks.json` | The `WEBHOOKS` field |
| `datadog-us1` | `https://ip-ranges.datadoghq.com/` | The `webhooks` IPv4 and IPv6 prefixes (US1 site) |
| `buildkite` | `https://api.buildkite.com/v2/meta` | The `webhook_ips` field |
| `pagerduty-us` | `https://app.pagerduty.com/webhook_ips` | The list |
| `pagerduty-eu` | `https://app.eu.pagerduty.com/webhook_ips` | The list |
| `bitbucket` | `https://ip-ranges.atlassian.com/` | The egress ranges for the `bitbucket` product |
| `jira` | `https://ip-ranges.atlassian.com/` | The egress ranges for the `jira` product |
| `grafana-cloud` | `https://grafana.com/api/hosted-alerts/source-ips.txt` | One address on each line |

A provider can move its list. If a preset fails, use a custom record with the new URL.

### Input

```json
{
  "providers": [
    "github",
    "stripe",
    {
      "name": "acme",
      "url": "https://acme.example.com/meta",
      "format": "json_path",
      "json_path": "webhooks.*"
    }
  ],
  "allowlist": {
    "github": ["192.30.252.0/22", "185.199.108.0/22", "140.82.112.0/20", "143.55.64.0/20"],
    "stripe": "3.18.12.63\n3.130.192.231",
    "acme": ["203.0.113.0/24"]
  },
  "alert_webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX",
  "baseline_key": "prod-edge"
}
```

| Field | Description |
|---|---|
| `providers` | 1 to 20 items. Each item is a preset ID, or a custom record. A custom record has a `name`, one HTTPS `url`, a `format` (`json_path` or `text_lines`) and, for `json_path`, a `json_path`. The default is `["github", "stripe"]`. |
| `allowlist` | An object. The key is the provider name. The value is a list of IPv4 and IPv6 CIDR blocks, or one text with one block on each line. A single address is a `/32` or `/128` block. At most 2,000 blocks in total. The key is not case-sensitive. |
| `alert_webhook_url` | Optional. One public HTTPS URL. The Actor sends one JSON POST for each provider with a new gap. |
| `baseline_key` | The record key of the baseline. Use one key for each allowlist, for example `prod-edge` and `staging-edge`. The default is `DEFAULT`. |

The empty input checks GitHub and Stripe against the ranges that they published on 2026-09-11.

#### The JSON path

A JSON path is a list of segments with a dot between them:

- `hooks` reads the field `hooks`.
- `*` reads each item of a list, or each value of an object. `webhooks.*` reads all the lists inside `webhooks`.
- `?key=value` keeps the records whose `key` is `value`, or whose `key` is a list that contains `value`. `items.?product=bitbucket.?direction=egress.cidr` reads the `cidr` of each matching record.

A list at the end of the path is read item by item.

#### A provider without an allowlist

If a provider has no allowlist key, its verdict is `no_allowlist`. The Actor reports the drift of the provider, but it does not compare the list with an allowlist and does not charge for the provider. The gate fails, because the Actor cannot tell if your firewall allows the provider. An allowlist key that names no provider is shown in `unused_allowlist_keys`.

### Output

The dataset has these records:

- One `summary` record. It is the first record.
- One `provider` record for each provider.
- One `gap_alert` record for each provider with a new gap. This record is also the webhook payload.

A `provider` record:

```json
{
  "record_type": "provider",
  "provider": "github",
  "source_url": "https://api.github.com/meta",
  "fetch_status": "OK",
  "published_cidr_count": 6,
  "baseline_status": "compared",
  "added": ["2606:50c0::/32"],
  "removed": [],
  "gap": ["2606:50c0::/32"],
  "gap_uncovered_blocks": ["2606:50c0::/32"],
  "new_gap": true,
  "new_gap_cidrs": ["2606:50c0::/32"],
  "stale": ["192.30.0.0/24"],
  "verdict": "gap",
  "alert_status": "sent"
}
```

| Verdict | Meaning |
|---|---|
| `covered` | The allowlist covers every published range. |
| `gap` | One or more published ranges are not fully covered. |
| `no_allowlist` | The provider has no allowlist key. |
| `not_checked` | The Actor did not read the list. `fetch_status` and `error` give the cause. |

`fetch_status` is one of `OK`, `HTTP_ERROR`, `TIMEOUT`, `CONNECT_FAIL`, `DNS_FAIL`, `BLOCKED_TARGET`, `REDIRECT_ERROR`, `TOO_LARGE`, `PARSE_ERROR`, `EMPTY_LIST`, `INPUT_REJECTED` or `CHARGE_LIMIT`. A list that gives no IP range is `EMPTY_LIST`. The Actor does not compare it, so an empty list never marks your whole allowlist as stale.

Each list in a record holds at most 500 items. The `*_count` fields always give the full count, and `lists_truncated` is `true` when a list was cut.

The summary record gives `providers_total`, `providers_checked`, `providers_with_gap`, `providers_with_new_gap`, `providers_without_allowlist`, `providers_not_checked`, `gap_total`, `stale_total`, `unused_allowlist_keys` and `gate_pass`.

A gap, a failed gate and an unreachable list are results. The run still ends with the status SUCCEEDED, and the status message gives the verdict. A bad input writes one `input_error` record and ends without a charge.

### Use it on a schedule

A provider can add a range at any time and does not tell each customer. Create a Task with your input and run it on a daily Apify schedule. Set `alert_webhook_url` to get one POST when a new gap occurs.

### State

The Actor keeps one record in the named key-value store `webhook-ip-range-baseline`, under the `baseline_key`. For each provider, the record holds the source URL, the last published range set and the gaps already reported. The added and removed lists need this record. The gap check does not need it.

- The first run for a key writes the record. It reports each gap as new.
- If the source URL of a provider changes, the Actor starts a new baseline for that provider.
- If the Actor cannot read a list, it keeps the old baseline entry for that provider.
- When a gap closes, the Actor forgets it. If the same range opens again, it is a new gap again.

### Pricing: pay per event

| Event | Unit | Price (USD) |
|---|---|---|
| `run-started` | One Actor run | 0.01 |
| `provider-range-checked` | One provider range list fetched and compared with the allowlist in one run | 0.01 |
| `allowlist-gap-flagged` | One provider with a new published range that the allowlist does not cover | 0.05 |

- `run-started` is charged once, when the run starts.
- `provider-range-checked` is charged for each provider with the verdict `covered` or `gap`. A list that the Actor did not read, a rejected record and a provider without an allowlist are not charged.
- `allowlist-gap-flagged` is charged once for each provider with a new gap. A gap that an earlier run with the same `baseline_key` reported is not charged again.

Example: 5 providers on a daily schedule, with no new gap, cost 0.01 + 5 × 0.01 = USD 0.06 for each run, about USD 1.80 for each month.

The Actor reads your maximum total charge before it requests a list. When the limit covers only some providers, the Actor does not request the other lists. Their rows have the status `CHARGE_LIMIT`, and their baseline entries stay unchanged.

### Limits and safety

- At most 20 providers and 2,000 allowlist blocks.
- One GET for each provider, with one retry for a timeout, a connection error, HTTP 429 or HTTP 5xx.
- Each response is at most 1 MB and at most 10,000 entries. Each request has a 15-second timeout.
- At most 3 redirects. Each redirect must use HTTPS.
- 5 requests at the same time.
- The Actor refuses a URL that resolves to a private or reserved address, before each request and each redirect.
- GitHub allows 60 requests for each hour from one IP address without a token. On shared cloud IP addresses, the `github` preset can get HTTP 403. The row then gives `HTTP_ERROR`, the Actor retries on the next scheduled run, and the run is not charged for that provider.
- The Actor does not use a browser, a proxy or a paid API.

# Actor input Schema

## `providers` (type: `array`):

1 to 20 providers. Each item is a preset ID from the built-in catalog (bitbucket, buildkite, datadog-us1, github, grafana-cloud, jira, pagerduty-eu, pagerduty-us, stripe), or a custom record with a "name", one HTTPS "url" of a published range list, a "format" ("json\_path" or "text\_lines") and, for json\_path, a "json\_path" such as "hooks" or "webhooks.\*". The Actor sends one GET for each provider and reads at most 1 MB. If you leave this field empty, the Actor checks GitHub and Stripe.

## `allowlist` (type: `object`):

The CIDR blocks that your firewall, WAF or ingress allows for each provider. The key is the provider name (the preset ID, or the name of a custom record). The value is a list of IPv4 and IPv6 CIDR blocks, or one text with one block on each line. A single address is a /32 or /128 block. At most 2,000 blocks in total. A provider without a key gets the verdict no\_allowlist: the Actor reports its drift, but does not compare it and does not charge for it.

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

Optional. One public HTTPS URL. The Actor sends one JSON POST for each provider with a new gap: a published range that the allowlist does not cover and that no earlier run reported. Private and reserved addresses are refused, and a redirect is not followed. Leave it empty to read the gaps in the dataset only.

## `baseline_key` (type: `string`):

The record key of the stored baseline in the named key-value store "webhook-ip-range-baseline". Use one key for each allowlist, for example "prod-edge" and "staging-edge". The first run for a key writes the baseline and reports each gap as new.

## Actor input object example

```json
{
  "providers": [
    "github",
    "stripe"
  ],
  "allowlist": {
    "github": [
      "192.30.252.0/22",
      "185.199.108.0/22",
      "140.82.112.0/20",
      "143.55.64.0/20",
      "2a0a:a440::/29",
      "2606:50c0::/32"
    ],
    "stripe": [
      "3.18.12.63/32",
      "3.69.109.8/32",
      "3.120.168.93/32",
      "3.130.192.231/32",
      "13.235.14.237/32",
      "13.235.122.149/32",
      "18.211.135.69/32",
      "35.154.171.200/32",
      "35.157.207.129/32",
      "52.15.183.38/32",
      "54.88.130.119/32",
      "54.88.130.237/32",
      "54.187.174.169/32",
      "54.187.205.235/32",
      "54.187.216.72/32"
    ]
  },
  "alert_webhook_url": "",
  "baseline_key": "DEFAULT"
}
```

# 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 = {
    "providers": [
        "github",
        "stripe"
    ],
    "allowlist": {
        "github": [
            "192.30.252.0/22",
            "185.199.108.0/22",
            "140.82.112.0/20",
            "143.55.64.0/20",
            "2a0a:a440::/29",
            "2606:50c0::/32"
        ],
        "stripe": [
            "3.18.12.63/32",
            "3.69.109.8/32",
            "3.120.168.93/32",
            "3.130.192.231/32",
            "13.235.14.237/32",
            "13.235.122.149/32",
            "18.211.135.69/32",
            "35.154.171.200/32",
            "35.157.207.129/32",
            "52.15.183.38/32",
            "54.88.130.119/32",
            "54.88.130.237/32",
            "54.187.174.169/32",
            "54.187.205.235/32",
            "54.187.216.72/32"
        ]
    },
    "alert_webhook_url": "",
    "baseline_key": "DEFAULT"
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/webhook-sender-ip-range-drift-and-allowlist-gap-report").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 = {
    "providers": [
        "github",
        "stripe",
    ],
    "allowlist": {
        "github": [
            "192.30.252.0/22",
            "185.199.108.0/22",
            "140.82.112.0/20",
            "143.55.64.0/20",
            "2a0a:a440::/29",
            "2606:50c0::/32",
        ],
        "stripe": [
            "3.18.12.63/32",
            "3.69.109.8/32",
            "3.120.168.93/32",
            "3.130.192.231/32",
            "13.235.14.237/32",
            "13.235.122.149/32",
            "18.211.135.69/32",
            "35.154.171.200/32",
            "35.157.207.129/32",
            "52.15.183.38/32",
            "54.88.130.119/32",
            "54.88.130.237/32",
            "54.187.174.169/32",
            "54.187.205.235/32",
            "54.187.216.72/32",
        ],
    },
    "alert_webhook_url": "",
    "baseline_key": "DEFAULT",
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/webhook-sender-ip-range-drift-and-allowlist-gap-report").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 '{
  "providers": [
    "github",
    "stripe"
  ],
  "allowlist": {
    "github": [
      "192.30.252.0/22",
      "185.199.108.0/22",
      "140.82.112.0/20",
      "143.55.64.0/20",
      "2a0a:a440::/29",
      "2606:50c0::/32"
    ],
    "stripe": [
      "3.18.12.63/32",
      "3.69.109.8/32",
      "3.120.168.93/32",
      "3.130.192.231/32",
      "13.235.14.237/32",
      "13.235.122.149/32",
      "18.211.135.69/32",
      "35.154.171.200/32",
      "35.157.207.129/32",
      "52.15.183.38/32",
      "54.88.130.119/32",
      "54.88.130.237/32",
      "54.187.174.169/32",
      "54.187.205.235/32",
      "54.187.216.72/32"
    ]
  },
  "alert_webhook_url": "",
  "baseline_key": "DEFAULT"
}' |
apify call kingii98/webhook-sender-ip-range-drift-and-allowlist-gap-report --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/webhook-sender-ip-range-drift-and-allowlist-gap-report"
        }
    }
}
```

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/2YchcUDVEOXSQh8hD/builds/BpaThN2Adq7VQUCl7/openapi.json
