# Third-Party GraphQL Schema Monitor (`monema/third-party-graphql-schema-monitor`) Actor

Monitor authorized third-party GraphQL introspection schemas for breaking, dangerous, additive, and other structural changes with persistent cross-run baselines.

- **URL**: https://apify.com/monema/third-party-graphql-schema-monitor.md
- **Developed by:** [Maarten Vreeburg](https://apify.com/monema) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 successful graphql schema checks

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use 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

## Third-Party GraphQL Schema Monitor

Monitor authorized third-party GraphQL introspection endpoints for schema drift. The Actor keeps an explicit cross-run baseline and emits GraphQL-aware change records for breaking, dangerous, additive, and other structural changes.

This is useful when your application depends on a vendor or partner GraphQL API but you do not control its deployment pipeline or schema registry.

### What it does

For each endpoint, the Actor:

1. sends one fixed standard GraphQL introspection query;
2. validates and bounds the JSON response;
3. rebuilds a GraphQL schema with `graphql-core`;
4. normalizes the schema to deterministic SDL;
5. compares it with the previous successful schema in a named key-value store;
6. classifies changes and writes one dataset row;
7. charges `graphql_schema_check` only after the successful schema is stored.

Typical statuses are:

- `initial_snapshot` — first valid schema stored;
- `unchanged` — normalized schema matches the previous successful check;
- `changed` — schema changed, with bounded details;
- `fetch_error` — network, redirect, TLS, HTTP, or destination validation failed;
- `schema_error` — response was not usable bounded introspection JSON/schema;
- `state_error` — a stored baseline could not be rebuilt safely.

Failures are not charged and do not replace a successful baseline.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `monitorId` | string | `qa-default` | Stable watchlist ID. Changing it creates a separate baseline namespace. |
| `endpointUrls` | string[] | public Countries GraphQL example | 1–50 authorized public HTTPS GraphQL endpoints. Duplicate URLs are checked once. |
| `authorizationHeader` | secret string | empty | Optional complete Authorization value shared by every endpoint in the run, such as `Bearer …`. Never logged or persisted. |
| `timeoutSeconds` | integer | `30` | Total per-endpoint check timeout, 5–60 seconds. |
| `maxChanges` | integer | `200` | Maximum detailed changes in one result, 1–500. Summary counts remain present. |
| `maxConcurrency` | integer | `5` | Concurrent endpoint checks, 1–5. |

Example:

```json
{
  "monitorId": "partner-graphql-production",
  "endpointUrls": [
    "https://api.example.com/graphql"
  ],
  "authorizationHeader": "Bearer <authorized-read-only-token>",
  "timeoutSeconds": 30,
  "maxChanges": 200,
  "maxConcurrency": 3
}
````

Only use endpoints and credentials you are authorized to query. The Actor does not discover endpoints, bypass disabled introspection, or send arbitrary GraphQL operations.

### Output

Each dataset row includes:

- endpoint, final URL, HTTP status, content type, and check time;
- stable current/previous SHA-256 schema hashes;
- type, field, argument/input-field, enum-value, and directive counts;
- status and concise summary;
- breaking, dangerous, additive, and informational counts;
- bounded change details (`severity`, `type`, `location`, `description`);
- named-state mode and charge-event fields;
- a bounded error on non-billable failures.

Example changed row:

```json
{
  "endpoint_url": "https://api.example.com/graphql",
  "status": "changed",
  "type_count": 42,
  "field_count": 318,
  "breaking_count": 1,
  "dangerous_count": 1,
  "additive_count": 2,
  "informational_count": 0,
  "change_count": 4,
  "changes": [
    {
      "severity": "breaking",
      "type": "FIELD_REMOVED",
      "location": "schema",
      "description": "Product.legacyName was removed."
    }
  ],
  "state_store_mode": "named_key_value_store",
  "charged_event": "graphql_schema_check"
}
```

Change labels follow GraphQL type-system rules, but they are review aids—not proof of actual client impact. A schema-level breaking change may not affect operations your application uses. Runtime behavior can also change without schema drift.

### Persistent state

State is stored in the explicit named key-value store:

```text
third-party-graphql-schema-monitor-state-v1
```

Keys are SHA-256 digests of `monitorId` plus endpoint URL. Stored snapshots contain normalized schema SDL, hashes, counts, and source metadata. They never contain the optional Authorization value. Failed checks do not overwrite the last successful schema.

Use the same `monitorId` and endpoint URL on scheduled runs to reuse history. Use a new `monitorId` for an independent baseline.

### Security and limits

Production checks enforce:

- public HTTPS on port 443 only;
- no URL credentials, fragments, whitespace, or control characters;
- private, loopback, link-local, multicast, reserved, and other non-global destinations blocked;
- every destination resolved and the connection pinned to a validated IP;
- every redirect re-resolved;
- cross-origin redirects rejected so Authorization cannot leave the configured origin;
- fixed introspection POST only—no user-controlled GraphQL query;
- two bounded attempts and at most five redirects within one total timeout;
- identity encoding only, 2 MB response cap, bounded UTF-8/JSON parsing, duplicate-key rejection;
- depth/node/schema type/field/input/enum limits;
- at most 50 endpoints and five concurrent checks;
- error truncation and secret redaction;
- no raw response or credential persistence.

For least privilege, use a read-only credential limited to schema introspection. Avoid sharing one secret across unrelated providers because `authorizationHeader` applies to every endpoint in the run; use separate runs when credentials differ.

### Pricing

The intended Store model is pay per event:

- event: `graphql_schema_check`;
- one event only after a schema is fetched, validated, compared, and stored;
- input, destination, HTTP, introspection, parse, schema, and state failures are not charged by this custom event.

The Actor respects Apify's per-run PPE cost limit before starting bounded endpoint work.

### Limitations

- Many production GraphQL services disable introspection to reduce attack surface. The Actor cannot monitor those endpoints unless the provider exposes introspection to an authorized credential.
- The fixed compatibility-oriented introspection query omits newer optional introspection fields so it works with more servers. Changes visible only through omitted optional metadata may not be detected.
- This is not a schema registry for an API owner, a GraphQL traffic analyzer, operation registry, vulnerability scanner, or availability SLA.
- The Actor does not validate your actual queries against the new schema or calculate client-specific blast radius.
- Endpoint availability, schema accuracy, and usage rights remain the provider's and user's responsibility.

### Local development

```bash
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
python smoke_test.py
npx --yes apify-cli validate-schema
PATH="$PWD/.venv/bin:$PATH" npx --yes apify-cli run --purge
```

The deterministic smoke test proves `initial_snapshot → unchanged → schema_error → unchanged → changed`, baseline preservation after failure, GraphQL breaking/dangerous/additive classifications, duplicate/deep JSON rejection, private-target blocking, Authorization redaction, and cross-origin redirect blocking.

### Independence and lawful use

This is an independent utility and is not affiliated with or endorsed by the GraphQL Foundation, Apollo, The Guild, GitHub, GitLab, Twenty, dotCMS, the public default example, or any monitored API provider. GraphQL and third-party names belong to their respective owners.

You are responsible for authorization, provider terms, polling frequency, credential scope, and decisions based on the results. The default endpoint is a public community example for QA; replace it with an endpoint you are authorized to monitor.

# Actor input Schema

## `monitorId` (type: `string`):

A stable ID that keeps this watchlist's history separate from other runs.

## `endpointUrls` (type: `array`):

Authorized public HTTPS GraphQL endpoints that allow the standard introspection query. Duplicate URLs are checked once.

## `authorizationHeader` (type: `string`):

Optional complete Authorization value shared by every endpoint in this run, for example Bearer <token>. It is never logged or persisted. Use only credentials authorized for schema introspection.

## `timeoutSeconds` (type: `integer`):

Bounded timeout for each GraphQL endpoint check.

## `maxChanges` (type: `integer`):

Summary counts cover classified changes; detailed change rows are capped.

## `maxConcurrency` (type: `integer`):

Number of endpoints checked concurrently.

## Actor input object example

```json
{
  "monitorId": "qa-default",
  "endpointUrls": [
    "https://countries.trevorblades.com/"
  ],
  "timeoutSeconds": 30,
  "maxChanges": 200,
  "maxConcurrency": 5
}
```

# 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 = {
    "endpointUrls": [
        "https://countries.trevorblades.com/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("monema/third-party-graphql-schema-monitor").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 = { "endpointUrls": ["https://countries.trevorblades.com/"] }

# Run the Actor and wait for it to finish
run = client.actor("monema/third-party-graphql-schema-monitor").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "endpointUrls": [
    "https://countries.trevorblades.com/"
  ]
}' |
apify call monema/third-party-graphql-schema-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=monema/third-party-graphql-schema-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Third-Party GraphQL Schema Monitor",
        "description": "Monitor authorized third-party GraphQL introspection schemas for breaking, dangerous, additive, and other structural changes with persistent cross-run baselines.",
        "version": "0.1",
        "x-build-id": "wtxKqFLwIkI6bbUgu"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/monema~third-party-graphql-schema-monitor/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-monema-third-party-graphql-schema-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/monema~third-party-graphql-schema-monitor/runs": {
            "post": {
                "operationId": "runs-sync-monema-third-party-graphql-schema-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/monema~third-party-graphql-schema-monitor/run-sync": {
            "post": {
                "operationId": "run-sync-monema-third-party-graphql-schema-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "monitorId",
                    "endpointUrls"
                ],
                "properties": {
                    "monitorId": {
                        "title": "Monitor ID",
                        "minLength": 1,
                        "maxLength": 200,
                        "type": "string",
                        "description": "A stable ID that keeps this watchlist's history separate from other runs.",
                        "default": "qa-default"
                    },
                    "endpointUrls": {
                        "title": "GraphQL endpoint URLs",
                        "minItems": 1,
                        "maxItems": 50,
                        "type": "array",
                        "description": "Authorized public HTTPS GraphQL endpoints that allow the standard introspection query. Duplicate URLs are checked once.",
                        "items": {
                            "type": "string"
                        },
                        "default": [
                            "https://countries.trevorblades.com/"
                        ]
                    },
                    "authorizationHeader": {
                        "title": "Optional Authorization header",
                        "type": "string",
                        "description": "Optional complete Authorization value shared by every endpoint in this run, for example Bearer <token>. It is never logged or persisted. Use only credentials authorized for schema introspection."
                    },
                    "timeoutSeconds": {
                        "title": "Per-endpoint timeout (seconds)",
                        "minimum": 5,
                        "maximum": 60,
                        "type": "integer",
                        "description": "Bounded timeout for each GraphQL endpoint check.",
                        "default": 30
                    },
                    "maxChanges": {
                        "title": "Maximum detailed changes per endpoint",
                        "minimum": 1,
                        "maximum": 500,
                        "type": "integer",
                        "description": "Summary counts cover classified changes; detailed change rows are capped.",
                        "default": 200
                    },
                    "maxConcurrency": {
                        "title": "Maximum concurrent endpoint checks",
                        "minimum": 1,
                        "maximum": 5,
                        "type": "integer",
                        "description": "Number of endpoints checked concurrently.",
                        "default": 5
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
