# No-Spec Vendor JSON API Response Drift Watcher (`kingii98/no-spec-vendor-json-api-response-drift-watcher`) Actor

Watch public JSON GET endpoints for response-shape drift without an OpenAPI spec. The Actor learns the shape from the response, compares it against a stored baseline, and reports each field-level change with a breaking flag.

- **URL**: https://apify.com/kingii98/no-spec-vendor-json-api-response-drift-watcher.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.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

## No-Spec Vendor JSON API Response Drift Watcher

Watch public JSON `GET` endpoints of a third party for response-shape drift **without an OpenAPI
spec and without authentication**.

The Actor sends one plain `GET` to each endpoint, learns the response shape from the body, and
compares that shape against a baseline that it keeps between runs. It reports each field-level
change with a JSON path, a change class, and a breaking flag. The first run creates the baseline
and reports no drift.

### Who this is for

The integration engineer whose product calls the public JSON endpoints of a vendor, who has no
OpenAPI file for those endpoints, and who does not want to add authentication to a monitor.

### What the Actor does

1. Fetches each endpoint with one `GET`. No browser, no proxy, no authentication, no paid API.
2. Infers the shape: every field gets a JSON pointer, a set of types, a nullable flag, and, when
   the field is a small repeated string set, an enum.
3. Loads the baseline from the named key-value store `json-api-shape-baseline`.
4. Diffs the new shape against the baseline and marks each change breaking or not.
5. Writes one dataset record for each endpoint, one summary record, and the new baseline.

### Input

Every field has a default, so a run with an empty input `{}` works.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `endpoints` | array of URL strings | two public demo endpoints | 1 to 25 public JSON `GET` URLs. No authentication. |
| `baselineStore` | string | `SHAPE_BASELINE` | Record key of the shape baseline in the named store `json-api-shape-baseline`. |
| `ignorePaths` | array of JSON pointers | `[]` | Pointers to ignore, for example `/meta/timestamp`. A pointer also ignores everything below it. |
| `sampleDepth` | integer 1–12 | `6` | Maximum object depth for shape inference. |
| `arraySampleSize` | integer 1–200 | `20` | Maximum number of elements read from each array. |
| `breakingRules` | array of change classes | `["removed", "type-changed", "nullability-changed"]` | Which change classes count as breaking. |
| `concurrency` | integer 1–25 | `5` | Endpoints fetched at the same time. |
| `timeoutSecs` | integer 2–120 | `20` | Per-request timeout. |
| `maxRedirects` | integer 0–20 | `5` | Redirect hops followed. Every hop is checked again. |
| `maxResponseBytes` | integer 1000–5000000 | `5000000` | Per-endpoint body cap. |

#### JSON pointers

A pointer names one field, for example `/data/user/id`. All elements of one array share the token
`-`, so `/items/-/price` is the `price` field of any element of `items`. `~1` stands for `/` inside
a key, and `~0` stands for `~`.

### Change classes

| Class | Meaning | Breaking by default |
| --- | --- | --- |
| `added` | A field that the baseline does not have. | No |
| `removed` | A field of the baseline that the response no longer has. | Yes |
| `type-changed` | The type set of the field changed, for example `integer` to `string`. | Yes |
| `nullability-changed` | The field started, or stopped, being null. | Only when it **started** being null |
| `enum-changed` | The inferred small string set of the field changed. | No |

A field becomes an enum only when it is seen at least 3 times, is always a string, has at most 12
distinct values, and each value is at most 64 characters. This keeps free-form text out of the
enum diff.

### Output

The dataset holds one `summary` record and one `endpoint` record for each endpoint.

`endpoint` record: `url`, `finalUrl`, `ok`, `reasonCode`, `httpStatus`, `bytesRead`,
`responseTimeMs`, `shapeHash`, `baselineShapeHash`, `baselineStatus` (`created`, `compared` or
`unavailable`), `fieldCount`, `shapeTruncated`, `changeCount`, `breakingCount`, `diffTruncated`,
`diff`, `error`, `checkedAt`. Each `diff` item has `path`, `changeClass`, `from`, `to` and
`breaking`.

`summary` record: `endpointsRequested`, `endpointsChecked`, `endpointsFailed`, `baselinesCreated`,
`endpointsCompared`, `endpointsWithDrift`, `changeCount`, `breakingCount`, `changeCounts`,
`gatePass`, `gateFailReasons`, `baselineStore`, `baselineKey`, `checkedAt`.

`gatePass` is `false` when the run found a breaking change (`BREAKING_CHANGE`) or could not read an
endpoint (`ENDPOINT_UNAVAILABLE`).

#### Reason codes

`OK`, `HTTP_ERROR`, `NOT_JSON`, `RESPONSE_TOO_LARGE`, `TIMEOUT`, `DNS_FAIL`, `CONNECT_FAIL`,
`BLOCKED_TARGET`, `REDIRECT_LOOP`.

### The run always succeeds

Drift, a failed gate, an unreachable endpoint and a body that is not JSON are **results**, not
faults. They go into the dataset and into the run status message, and the run ends `SUCCEEDED`.
A `FAILED` run means a real malfunction.

### State between runs

The baseline lives in the **named** key-value store `json-api-shape-baseline`, under the record key
that `baselineStore` gives. A named store outlives the run; the default store of a run does not.
The record is written once, at the end of the run, so an aborted run leaves the previous baseline
intact. An endpoint that the run could not read keeps its stored baseline.

The record holds at most 200 endpoints, and each endpoint shape holds at most 2000 fields.

### Schedule it

The value of a baseline grows with the number of comparisons. Run the Actor on a schedule, for
example every hour or every day, against the same `baselineStore` key.

### Pricing (pay per event)

| Event | Unit | Counted as |
| --- | --- | --- |
| `run-started` | one Actor run | Charged once for each run, before any endpoint is fetched. It pays for the container start and the summary write. |
| `endpoint-checked` | one endpoint fetched and shape-compared | Charged once for each endpoint whose body was read and whose shape was inferred. An endpoint that timed out, was blocked, or did not answer JSON is **not** charged. |
| `drift-report-emitted` | one endpoint with one or more changes | Charged once for each checked endpoint whose diff has at least one item. |

The run never checks more endpoints than its maximum charge allows. When the limit covers fewer
endpoints than the input asks for, the Actor logs a warning, checks the endpoints it can pay for,
and leaves the baseline of the others as it is.

### Safety limits

- `GET` only, `http` and `https` only.
- URLs with credentials, `localhost` targets, and hosts that resolve to a private or reserved
  address are rejected. Each redirect hop is checked again.
- Bounded input (25 endpoints, 50 ignore paths), concurrency, redirects, response bytes, timeout,
  shape fields (2000), diff items (200), and baseline entries (200).

### Development

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `endpoints` (type: `array`):

1 to 25 public JSON GET URLs. No authentication, no browser and no proxy: the Actor sends a plain GET and reads the JSON body. If you leave this field empty, the Actor checks the two public example endpoints shown below.

## `baselineStore` (type: `string`):

Name of the record that holds the shape baseline in the named key-value store 'json-api-shape-baseline'. Use one key for each monitored fleet of endpoints. The record is written once, at the end of the run.

## `ignorePaths` (type: `array`):

JSON pointers to ignore, for example /meta/timestamp or /items/-/viewCount. A pointer also ignores everything below it. Use the token '-' for any array element. Ignored fields change neither the shape hash nor the diff.

## `sampleDepth` (type: `integer`):

Maximum object depth for shape inference. The walk stops below this depth, and the record reports shapeTruncated.

## `arraySampleSize` (type: `integer`):

Maximum number of elements read from each array. All elements of one array share the shape pointer that ends with '-'.

## `breakingRules` (type: `array`):

Which change classes count as breaking. The default is: field removed, type changed, nullability added. A nullability change counts as breaking only when the field became nullable.

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

Maximum number of endpoints fetched at the same time.

## `timeoutSecs` (type: `integer`):

Per-request timeout for each GET. An endpoint that exceeds it gets the TIMEOUT reason code.

## `maxRedirects` (type: `integer`):

Maximum redirect hops followed for each endpoint. Every hop is checked again against the private-address rules. A chain that repeats a URL gets the REDIRECT\_LOOP reason code.

## `maxResponseBytes` (type: `integer`):

Per-endpoint body cap. A larger body gets the RESPONSE\_TOO\_LARGE reason code and is not parsed.

## Actor input object example

```json
{
  "endpoints": [
    "https://jsonplaceholder.typicode.com/todos/1",
    "https://registry.npmjs.org/-/package/left-pad/dist-tags"
  ],
  "baselineStore": "SHAPE_BASELINE",
  "ignorePaths": [],
  "sampleDepth": 6,
  "arraySampleSize": 20,
  "breakingRules": [
    "removed",
    "type-changed",
    "nullability-changed"
  ],
  "concurrency": 5,
  "timeoutSecs": 20,
  "maxRedirects": 5,
  "maxResponseBytes": 5000000
}
```

# 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 = {
    "endpoints": [
        "https://jsonplaceholder.typicode.com/todos/1",
        "https://registry.npmjs.org/-/package/left-pad/dist-tags"
    ],
    "baselineStore": "SHAPE_BASELINE",
    "ignorePaths": [],
    "sampleDepth": 6,
    "arraySampleSize": 20,
    "breakingRules": [
        "removed",
        "type-changed",
        "nullability-changed"
    ],
    "concurrency": 5,
    "timeoutSecs": 20,
    "maxRedirects": 5,
    "maxResponseBytes": 5000000
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/no-spec-vendor-json-api-response-drift-watcher").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 = {
    "endpoints": [
        "https://jsonplaceholder.typicode.com/todos/1",
        "https://registry.npmjs.org/-/package/left-pad/dist-tags",
    ],
    "baselineStore": "SHAPE_BASELINE",
    "ignorePaths": [],
    "sampleDepth": 6,
    "arraySampleSize": 20,
    "breakingRules": [
        "removed",
        "type-changed",
        "nullability-changed",
    ],
    "concurrency": 5,
    "timeoutSecs": 20,
    "maxRedirects": 5,
    "maxResponseBytes": 5000000,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/no-spec-vendor-json-api-response-drift-watcher").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 '{
  "endpoints": [
    "https://jsonplaceholder.typicode.com/todos/1",
    "https://registry.npmjs.org/-/package/left-pad/dist-tags"
  ],
  "baselineStore": "SHAPE_BASELINE",
  "ignorePaths": [],
  "sampleDepth": 6,
  "arraySampleSize": 20,
  "breakingRules": [
    "removed",
    "type-changed",
    "nullability-changed"
  ],
  "concurrency": 5,
  "timeoutSecs": 20,
  "maxRedirects": 5,
  "maxResponseBytes": 5000000
}' |
apify call kingii98/no-spec-vendor-json-api-response-drift-watcher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/no-spec-vendor-json-api-response-drift-watcher"
        }
    }
}

```

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/DcEhozxbt3ldyBJCQ/builds/oNwxfn4dJsjaJz6O7/openapi.json
