Skip to content
ChangeGuard

Developers

The API and webhooks.

Read your workspace's sets, pages, checks and changes from your own systems, and have each change posted to them as it is found. On the Business plan and up.

Keys

The workspace's owner makes a key in the app, in Settings, Integrations. It is shown once: only a hash of it is kept. Send it as a bearer token. A key reads its own workspace and nothing else, and stops working the moment it is revoked.

curl -G https://changeguard.app/api/v1/changes \
  -d since=2026-10-01T00:00:00Z \
  -H "Authorization: Bearer cg_live_..."

Every answer is JSON, never cached (Cache-Control: private, no-store). The API only reads: every endpoint is a GET. Its OpenAPI description is at /api/v1/openapi.json.

Lists

A list answers {"data": [...], "next_cursor": "..."}. Pass the cursor back as cursor to read on, until it is null. limit takes 1 to 100; the default is 50. /sets answers every set at once, with no cursor. Changes are kept for 45 days.

Limits

60 requests a minute and 10,000 a day for each workspace, all its keys together, across every endpoint. Past them the API answers 429, and its Retry-After says how many seconds to wait.

Errors

As application/problem+json, most with a code to branch on:

400 invalid_cursor, invalid_parameter
A cursor this API did not write, or a time that is not ISO 8601.
400
A parameter it does not take (an api_key in the address included), or a value outside what one takes: no code, and the detail says which.
401
No key, or one that is not in use.
402 plan_required
The workspace's plan no longer includes the API.
403 turned_off
The plan includes the API, but it is turned off for this workspace.
404 not_found
Not in this key's workspace.
429 rate_limited
Over the limit: Retry-After says how many seconds to wait.
503 unavailable
The data could not be read just now: try again shortly. Never read it as an empty list.

Endpoints

Under https://changeguard.app/api/v1.

GET /sets

Your sets, with how many pages each has to review.

Each item (Set)
Fields of Set
Field Type What it is
name string
pages integer
pages_to_review integer
pages_failing integer Pages whose last check failed.
pages_never_checked integer
pages_changed_this_week integer Pages that changed in the last 7 days.
last_checked_at string or null

GET /pages

The pages you watch, oldest first, in batches.

Parameters of GET /pages
Parameter Type What it does
set string Only this set's.
needs_review boolean true: only pages waiting for review (a change, or an update that did not come by its day); false: only pages with none.
status ok | failing | never_checked Only pages whose last check went this way.
cursor string The next_cursor of the last answer, to read on from it.
limit integer, default 50, 1 to 100 How many to answer with, at most.
Each item (Page)
Fields of Page
Field Type What it is
id integer
url string
title string or null
set string
added_at string or null
last_checked_at string or null
last_changed_at string or null
last_reviewed_at string or null
needs_review boolean It waits for someone to review it: a change was found, or an update expected on a set day did not come.
status ok | failing | never_checked How its last check went.
links object
links.review string The page in ChangeGuard's review screen.
links.api string

GET /pages/{page_id}

One page.

Parameters of GET /pages/{page_id}
Parameter Type What it does
page_id integer, in the path
The answer (Page)
Fields of Page
Field Type What it is
id integer
url string
title string or null
set string
added_at string or null
last_checked_at string or null
last_changed_at string or null
last_reviewed_at string or null
needs_review boolean It waits for someone to review it: a change was found, or an update expected on a set day did not come.
status ok | failing | never_checked How its last check went.
links object
links.review string The page in ChangeGuard's review screen.
links.api string

GET /pages/{page_id}/checks

A page's checks, newest first, in batches.

The newest 20 checks of a page, and further back the ones that first found each of its last 100 changes or failures.

Parameters of GET /pages/{page_id}/checks
Parameter Type What it does
page_id integer, in the path
cursor string The next_cursor of the last answer, to read on from it.
limit integer, default 50, 1 to 100 How many to answer with, at most.
Each item (Check)
Fields of Check
Field Type What it is
id integer
checked_at string
outcome changed | unchanged | failed
error string or null Why it failed, as a code: timeout, http_error, ip_banned (the site blocks checks), captcha, captcha_unsolved, action_failed (the page's recorded steps no longer work), signin_required (someone must sign in to the page), permanent (the page could not be loaded), transient, snapshot_save_failed, sync_failed or unknown. More may be added: read a code you don't know as a failure.
first_seen_at string or null When the check repeats what an earlier one found (the same change still waiting for review, or another failure in a row): when that was first seen.

GET /changes

Changes found and checks that failed, newest first, in batches.

Each change once, as it was first found: a change still waiting for review is not listed again. Kept for 45 days. A list may hold fewer than limit when a page was deleted since; read on while there is a next_cursor.

Parameters of GET /changes
Parameter Type What it does
since string Found at or after this time (ISO 8601).
until string Found before this time (ISO 8601).
set string Only this set's.
kind change | failure
cursor string The next_cursor of the last answer, to read on from it.
limit integer, default 50, 1 to 100 How many to answer with, at most.
Each item (Change)
Fields of Change
Field Type What it is
id string
kind change | failure A change found, or a check that failed.
detected_at string
first_seen_at string or null
set string
page object
page.id integer
page.url string
page.title string
rating none | minor | substantial or null The AI's rating of the change; null until it is rated, and for a failure.
category string or null What kind of change, as a code: content, date_deadline, new_document, …
summary string or null The AI's summary of the change.
excerpt object or null Lines added and removed, a few of each.
excerpt.added array of string
excerpt.removed array of string
excerpt.more_added integer
excerpt.more_removed integer
signals array of string What changed: text, visual, documents.
keywords array of string Your keywords the change mentions.
error string or null For a failure: why, as a code, the same as a check's error.
comparison_id string or null
links object
links.diff string or null The before-and-after view, which opens without a sign-in.
links.review string
links.page string
links.api string

GET /changes/{change_id}

One change: the same as a webhook's data.change.

Parameters of GET /changes/{change_id}
Parameter Type What it does
change_id string, in the path
The answer (Change)
Fields of Change
Field Type What it is
id string
kind change | failure A change found, or a check that failed.
detected_at string
first_seen_at string or null
set string
page object
page.id integer
page.url string
page.title string
rating none | minor | substantial or null The AI's rating of the change; null until it is rated, and for a failure.
category string or null What kind of change, as a code: content, date_deadline, new_document, …
summary string or null The AI's summary of the change.
excerpt object or null Lines added and removed, a few of each.
excerpt.added array of string
excerpt.removed array of string
excerpt.more_added integer
excerpt.more_removed integer
signals array of string What changed: text, visual, documents.
keywords array of string Your keywords the change mentions.
error string or null For a failure: why, as a code, the same as a check's error.
comparison_id string or null
links object
links.diff string or null The before-and-after view, which opens without a sign-in.
links.review string
links.page string
links.api string

GET /documents

Documents found and not yet reviewed, newest first, in batches.

PDF, Word and Excel files a check found new or changed, until the page they were found on is marked reviewed.

Parameters of GET /documents
Parameter Type What it does
set string Only this set's.
cursor string The next_cursor of the last answer, to read on from it.
limit integer, default 50, 1 to 100 How many to answer with, at most.
Each item (Document)
Fields of Document
Field Type What it is
id integer
url string
label string Its name: the link's words, else its file name.
kind string pdf, docx, xlsx, …
found_at string or null
page object or null A page it was found on.
page.id integer
page.title string or null
page.set string

Webhooks

Add a webhook in Settings, Integrations: an https address on port 443 or 8443, with a name that resolves to public addresses. ChangeGuard posts JSON to it, signed per Standard Webhooks, so any of its libraries checks a delivery: standardwebhooks on PyPI and npm, and more.

It gets each change in the sets it covers as it is found (only substantial ones, if it is set to), for the pages within the plan's instant-alert pages, and a report when a set's scheduled check finds something new.

change.detected
A change found on a page, as it is found. data.change is the same object GET /changes answers with.
check.failed
A page that could not be checked: data.check names the page and the reason, as a code.
update.missed
A page expected to change on a set day was not updated by then: data.update names the page and the day.
run.completed
A set's scheduled check found something new: the counts and up to 25 of the changes it found, in data.run.
webhook.test
Sent when you press Send test in the app.

Headers

  • webhook-id: the message's id, the same on every try of it. Keep the ones you have handled and skip a repeat.
  • webhook-timestamp: when this try was sent, in Unix seconds.
  • webhook-signature: v1, and the base64 HMAC-SHA256 of {id}.{timestamp}.{body}, keyed with your signing secret's bytes: the base64 after whsec_, decoded. For a day after you make a new secret there are two, space-separated: one per secret.

Retries

Answer with a 2xx within 15 seconds. Otherwise the message is tried again after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14, 20 and 24 hours: ten tries in about three days, in order for each webhook. A 429, 502, 503 or 504 with Retry-After is waited out without using up a try, for up to a day. A 410 turns the webhook off. A message still not delivered four days after it was queued is dropped. After a day of failures the workspace's owner gets an email.

A message is at most 20 KB: a long change loses its excerpt first, then the end of its summary.

A delivery

{
  "type": "change.detected",
  "timestamp": "2026-10-18T14:03:00Z",
  "data": {
    "change": {
      "id": "48211",
      "kind": "change",
      "detected_at": "2026-10-18T14:03:00Z",
      "first_seen_at": "2026-10-18T14:03:00Z",
      "set": "fees",
      "page": {
        "id": 731,
        "url": "https://www.agency.example/fees",
        "title": "Fee schedule"
      },
      "rating": "substantial",
      "category": "date_deadline",
      "summary": "The filing fee rises from $350 to $400 on 1 January 2027.",
      "excerpt": {
        "added": [
          "Filing fee: $400 (from 1 January 2027)"
        ],
        "removed": [
          "Filing fee: $350"
        ],
        "more_added": 0,
        "more_removed": 0
      },
      "signals": [
        "text"
      ],
      "keywords": [
        "filing fee"
      ],
      "error": null,
      "comparison_id": "7f3c9a2e-5b1d-4c8e-9f60-2d4b8e1a7c55",
      "links": {
        "diff": "https://changeguard.app/api/comparisons/7f3c9a2e-5b1d-4c8e-9f60-2d4b8e1a7c55?src=webhook",
        "review": "https://changeguard.app/review?q=id&id=731&ws=12&src=webhook",
        "page": "https://www.agency.example/fees",
        "api": "https://changeguard.app/api/v1/changes/48211"
      }
    }
  }
}

Checking one, in Python

import base64, hashlib, hmac, time

def verify(secret: str, headers: dict, body: bytes) -> bool:
    """True when the delivery is ChangeGuard's and recent."""
    msg_id = headers["webhook-id"]
    stamp = headers["webhook-timestamp"]
    if abs(time.time() - int(stamp)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{stamp}.".encode() + body
    digest = hmac.new(key, signed, hashlib.sha256).digest()
    expected = base64.b64encode(digest).decode()
    # One signature per secret in use: two for a day after a new one.
    sent = [s.split(",", 1)[1] for s in headers["webhook-signature"].split()]
    return any(hmac.compare_digest(expected, s) for s in sent)

Slack

In Slack, add the Incoming Webhooks app to the channel and copy its address, which starts with https://hooks.slack.com/services/. Paste it in Settings, Integrations, Add destination, then press Send test. If Slack says the channel is archived or the hook is gone, alerts to it stop and the owner is told.

Microsoft Teams

In Teams, open Workflows and use the template “Send webhook alerts to a channel”. Copy the address it gives you and paste it in Settings, Integrations. Older Workflows addresses on logic.azure.com, which Microsoft retired, are refused, as are the Office 365 connector addresses Workflows replaced.

Start watching your first five pages. Free.

An account takes a minute: email and password, no card. Add a page, run the first check, then choose how often its set is checked.