The ConfigCheckup API.

Pull scores, findings, reports, changes, baselines, alerts, licences, devices and the audit log into your PSA, dashboards or data warehouse, export any table the console can, queue assessments and draft fixes from your own automation, and get signed webhooks when assessments finish or a tenant drifts. The read API is included from the Monitor plan; queueing assessments, drafting changes and webhooks from Automate.

curl https://configcheckup.com/api/v1/tenants \
  -H "Authorization: Bearer ccu_…"

Authentication

Create a key in Settings → API and webhooks and send it on every request:

curl https://configcheckup.com/api/v1/tenants -H "Authorization: Bearer ccu_…"

Keys belong to the workspace and see every tenant in it, unless limited to some tenants (by tenant or by tag) when created or since; a key can also be limited to IP ranges. A limited key sees nothing of the other tenants: they answer 404. Keys are shown once, stored only as a hash, and can expire or be revoked at any time. Responses are JSON: { "data": … } on success and { "error": { "code", "message" } } otherwise, with the usual HTTP status. Dates are ISO 8601 in UTC.

Each key may make 120 requests a minute, counted across every server. Every answer to a key carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the minute ends); beyond the limit you get 429 with a Retry-After header. Every answer carries an X-Request-Id: send your own to have it echoed, and quote it if you ask us about a request. Each key’s requests of the last 30 days are listed under it in Settings.

The whole API is described in OpenAPI 3.1 at /api/v1/openapi.json, which tools such as Postman, Insomnia and code generators read directly. This page is drawn from it. For a custom connector in Power Automate, Power Apps or Logic Apps, which take OpenAPI 2.0, import /api/v1/power-automate/apiDefinition.swagger.json: how to.

Lists, a page at a time

Every list answers a page at a time, in a fixed order: { "data": […], "nextCursor": "…" }, with a Link: <…>; rel="next" header. Send nextCursor back as ?cursor= until it is null. ?limit= sets the page size, up to 200. To keep a copy up to date, ask for ?updatedSince= the time of your last sync: only what was made or changed since comes back. A filter value the list does not know, such as an unknown status, is refused with 400 rather than ignored.

async function all(path, key) {
  const rows = [];
  const url = new URL(path, 'https://configcheckup.com');
  for (;;) {
    const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
    const { data, nextCursor } = await res.json();
    rows.push(...data);
    if (!nextCursor) return rows;
    url.searchParams.set('cursor', nextCursor);
  }
}

// Findings at every tenant that changed since the last sync.
const changed = await all('/api/v1/findings?updatedSince=2026-10-01T00:00:00Z&limit=200', process.env.CCU_KEY);

Retrying safely

Send an Idempotency-Key header, such as a UUID, with a POST. If the connection drops and you send the same request again with the same key within 24 hours, you get the first answer again (marked Idempotent-Replayed: true) and nothing is queued or drafted twice. The same key with a different request is refused with 409. A retry sent while the first request is still being handled also gets 409: try again a few seconds later. An answer of 500 or above is not kept, so a retry after one runs the request again.

Changes: drafted by the API, approved by people

A key with the changes:draft scope can draft a fix for a finding, by the finding's id or by its check in the tenant's latest assessment. ConfigCheckup dry-runs it against the live tenant, exactly as “Fix this” does in the console, and returns the draft with what it would change. Where a fix needs a choice (who loses a role, which IP ranges), send it as values; without them the answer says what to send.

If the fix is already in place, no change is drafted and the answer is 409 already_in_place. The finding is read again, and is marked done when its check agrees.

That is as far as a key goes. It never sends a change for approval, approves, runs or rolls one back: people do that in the console, with one or two approvers as the tenant is set up. Follow a draft with GET /api/v1/changes/{id}, which shows its approvals and runs as they happen. Each draft is recorded in the audit log as made by the key, on behalf of the member who created it.

Endpoints

Every endpoint of the API, by area: its method, path, what it does and the key scope it needs
MethodPathWhat it doesKey scope
Tenants
GET/api/v1/tenants

List tenants. Every tenant the key sees, A to Z, a page at a time.

  • ?tag= Only the tenants with this tag, such as managed.
  • ?limit= How many in a page: 200 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only tenants that changed (their score included) since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Tenant, and nextCursor for the next.

read
GET/api/v1/tenants/{id}

Get a tenant. One tenant, with its latest completed assessment. A prospect (a lead’s quick check) is not a tenant here: it answers 404, as it is left out of the list.

Returns data: TenantDetail.

read
Findings
GET/api/v1/tenants/{id}/findings

List a tenant’s findings. Findings from the tenant’s latest completed assessment, heaviest first.

  • ?status= Results to include, comma separated (PASS, FAIL, WARN, NOT_APPLICABLE, ERROR), or all. Failing and warning findings unless asked. A value not in the list is refused with 400.
  • ?custom= true for only the workspace’s own custom checks, false for only ConfigCheckup’s; both unless asked. Anything else is refused with 400.
  • ?limit= How many in a page: 200 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only findings made or changed since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Finding, and nextCursor for the next.

read
GET/api/v1/findings

List findings across tenants. Findings from every tenant’s latest completed assessment (older ones are history, and would count the same issue once per run), heaviest first.

  • ?tenantId= Only this tenant’s findings.
  • ?tag= Only the findings of tenants with this tag, such as managed.
  • ?status= Results to include, comma separated (PASS, FAIL, WARN, NOT_APPLICABLE, ERROR), or all. Failing and warning findings unless asked. A value not in the list is refused with 400.
  • ?state= Where the workspace is with them, comma separated (OPEN, IN_PROGRESS, RESOLVED, ACCEPTED_RISK, SUPPRESSED). Every state unless asked.
  • ?severity= Only these severities, comma separated (critical, high, medium, low, info).
  • ?category= Only this area, such as identity.
  • ?ruleId= Only this check, such as NL-IDN-001.
  • ?custom= true for only the workspace’s own custom checks, false for only ConfigCheckup’s; both unless asked. Anything else is refused with 400.
  • ?limit= How many in a page: 100 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only findings made or changed since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Finding, and nextCursor for the next.

read
GET/api/v1/assessments/{id}/findings

List an assessment’s findings. Findings from one assessment, heaviest first. Older history keeps one assessment a week, then one a month, in full; the others have thinnedAt set and keep their score and counts, not their findings.

  • ?status= Results to include, comma separated (PASS, FAIL, WARN, NOT_APPLICABLE, ERROR), or all. Failing and warning findings unless asked. A value not in the list is refused with 400.
  • ?custom= true for only the workspace’s own custom checks, false for only ConfigCheckup’s; both unless asked. Anything else is refused with 400.
  • ?limit= How many in a page: 200 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only findings made or changed since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Finding, and nextCursor for the next.

read
Assessments
GET/api/v1/tenants/{id}/assessments

List a tenant’s assessments. The tenant’s assessments, newest first.

  • ?limit= How many in a page: 20 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only assessments queued, started, finished or thinned since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Assessment, and nextCursor for the next.

read
POST/api/v1/tenants/{id}/assessments

Queue an assessment. Queues an assessment of the tenant and returns it with status QUEUED; poll it until COMPLETED. Asking while one is already queued or running returns that one.

  • Idempotency-Key: Any unique string of up to 255 characters, such as a UUID. Send the same key to retry the same request safely: for 24 hours the first answer is sent again (with Idempotent-Replayed: true) instead of doing the work twice. The same key with a different request is refused with 409.

Returns data: Assessment.

assessments:run
GET/api/v1/assessments/{id}

Get an assessment. Status, score, grade, severity counts and each area’s score. Poll this after queueing.

Returns data: AssessmentDetail.

read
Reports
GET/api/v1/reports

List reports. Reports, newest first.

  • ?tenantId= Only this tenant’s reports.
  • ?limit= How many in a page: 20 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only reports asked for or changed (such as becoming READY) since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Report, and nextCursor for the next.

read
POST/api/v1/tenants/{id}/reports

Queue a report. Queues a report of the tenant’s latest completed assessment (or the one named) and returns it with status QUEUED; poll the reports list until it is READY, then download it. Ask for format DOCX for a Word document the same as the PDF, with charts as tables. Recorded in the audit log as made by the key, on behalf of its creator.

  • Idempotency-Key: Any unique string of up to 255 characters, such as a UUID. Send the same key to retry the same request safely: for 24 hours the first answer is sent again (with Idempotent-Replayed: true) instead of doing the work twice. The same key with a different request is refused with 409.
The JSON body it takes

What report to make: its type, its file format, and the assessment it is made from.

FieldTypeWhat it is
type (optional)one of EXECUTIVE, TECHNICAL, BOARD, REVIEW, LIFECYCLE, DELTA, LICENSING, COPILOTThe report: EXECUTIVE unless asked.
format (optional)one of PDF, HTML, DOCXThe file: DOCX for a Word document; PDF or HTML (saved as a PDF from the browser). The workspace’s usual format unless asked.
assessmentId (optional)stringThe completed assessment to report on; the tenant’s latest unless named.

Returns data: Report.

assessments:run
GET/api/v1/reports/{id}/download

Download a report. The report’s file, PDF, HTML or Word (DOCX), once it is READY. An HTML report is served with a sandboxing Content-Security-Policy.

Returns the file itself (application/pdf or text/html or application/vnd.openxmlformats-officedocument.wordprocessingml.document).

read
GET/api/v1/reports/{id}/model

Get a report’s content as data. Everything the report is drawn from, as JSON: the same model its document is made from, built again from its assessment. Each request is recorded in the audit log.

Returns data: ReportModel.

read
Changes
POST/api/v1/tenants/{id}/changes

Draft a change. Drafts a change from one of the tenant’s findings, by its id or by its check in the latest assessment, and dry-runs it against the live tenant, as “Fix this” does in the console. Returns the draft with its dry run. With scheduledFor, it runs at that time once people approve it (or straight away if they approve it later), and dry-runs again first. The key never sends it for approval, approves, runs or rolls it back: people do that in the console. Needs a plan with automation and the write API, a workspace that is not read-only, automation switched on for the tenant, and a key whose creator is a Consultant or above. Recorded in the audit log as made by the key, on behalf of its creator.

  • Idempotency-Key: Any unique string of up to 255 characters, such as a UUID. Send the same key to retry the same request safely: for 24 hours the first answer is sent again (with Idempotent-Replayed: true) instead of doing the work twice. The same key with a different request is refused with 409.
The JSON body it takes

What to draft a change from: findingId or ruleId, values where the fix takes a choice, when it is to run, and the ticket it is for.

FieldTypeWhat it is
findingId (optional)stringThe finding to fix: one of the tenant’s, from any of its assessments.
ruleId (optional)stringInstead of findingId: the check, such as NL-GOV-002, in the tenant’s latest completed assessment.
values (optional)ChoiceValuesFor a fix that takes a choice. Without it, such a fix answers 422 choice_required, saying what to send.
scheduledFor (optional)date-timeWhen it is to run once approved: an ISO date and time with its offset (Z or ±hh:mm), at least a minute from now and within 90 days. Approved after that time, it runs straight away. Either way it is dry-run again first, and runs only if that matches what was approved; outside the tenant’s change window it waits for the window to open. Not used when the finding already has an open change, which is returned as it is.
ticketRef (optional)stringThe PSA or helpdesk ticket it is for, such as 48213 or T20261011.0042: up to 64 letters and numbers, with spaces and # . _ / : - allowed. It is kept with the change and can be searched on Changes.

ChoiceValues: What a fix that takes a choice asks for, as the finding’s form in the console does. Each is checked against the finding’s evidence.

FieldTypeWhat it is
picked (optional)list of stringThe targets chosen: ids from the finding’s evidence, such as accounts, groups or domains.
input (optional)stringA typed answer: an owner’s address, IP ranges, a domain or a list of domains.
name (optional)stringA name for what the fix creates, where it creates something.
choice (optional)stringThe answer, for a fix with a few set answers, such as how to classify alerts.

Returns data: ChangeDetail.

changes:draft
GET/api/v1/changes

List changes. Changes to the workspace’s tenants, newest first, with each one’s dry run and approval count.

  • ?tenantId= Only this tenant’s changes.
  • ?status= Only changes in these statuses, comma separated (DRAFT, PENDING_APPROVAL, APPROVED, REJECTED, SCHEDULED, EXECUTING, SUCCEEDED, FAILED, ROLLED_BACK, CANCELED, EXPIRED). Every status unless asked. A status not in the list is refused with 400.
  • ?limit= How many in a page: 20 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only changes that changed (a status, an approval, a run) since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Change, and nextCursor for the next.

read
GET/api/v1/changes/{id}

Get a change. One change: its dry run in full, approvals so far with names and times, and each run with its results, read-back and rollback. Poll this to follow a change you drafted.

Returns data: ChangeDetail.

read
Baselines
GET/api/v1/baselines

List baselines. Every baseline, A to Z, with each tenant’s result against it from its latest completed assessment. Checks that could not be read are counted as unknown, never as met.

  • ?limit= How many in a page: 200 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only baselines whose checks or tenants were changed since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Baseline, and nextCursor for the next.

read
GET/api/v1/baselines/{id}

Get a baseline. One baseline with each check across its tenants, and each tenant’s result for each check.

Returns data: BaselineDetail.

read
Compliance
GET/api/v1/tenants/{id}/frameworks

Get a tenant’s framework readiness. The tenant’s readiness for each framework (Cyber Essentials, ISO 27001, NIS2, PCI DSS and the rest), from its latest completed assessment: a percentage and its controls by state. Ask for one framework to have each of its controls and the checks behind it. Readiness is not certification.

  • ?framework= One framework’s id, such as ISO-27001, for its controls in full. An id not known is refused with 400.

Returns data: FrameworkReadiness.

read
Licences and devices
GET/api/v1/tenants/{id}/licences

List a tenant’s licences. The licences the tenant subscribes to, by name, with seats bought, assigned and free, from its latest completed assessment.

  • ?limit= How many in a page: 200 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only when read (with an assessment) since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Licence, and nextCursor for the next.

read
GET/api/v1/tenants/{id}/devices

List a tenant’s devices. The tenant’s devices from Intune and Entra ID, by name, with where each is in its life, from its latest completed assessment.

  • ?show= Only those a Devices filter shows: attention, replace, plan, update, review, inactive, noncompliant, unencrypted, or all (the default).
  • ?q= Words in its name, serial number, model, maker, system or primary user.
  • ?limit= How many in a page: 200 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only when read (with an assessment) since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Device, and nextCursor for the next.

read
Alerts
GET/api/v1/security-events

List alerts. Security alerts at the tenants the key sees, newest first: new admins, risky app consents, MFA methods removed, Microsoft Defender alerts and matches of the workspace’s own alert rules.

  • ?tenantId= Only this tenant’s alerts.
  • ?tag= Only the alerts of tenants with this tag, such as managed.
  • ?severity= Only these, comma separated: high, medium.
  • ?kind= Only this kind, such as admin_role_added or defender_alert.
  • ?status= open, dismissed, or all (the default).
  • ?limit= How many in a page: 50 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only alerts raised or changed (claimed, released, dismissed) since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Alert, and nextCursor for the next.

read
GET/api/v1/drift

List drift. Each time the hourly drift check found checks that passed at a tenant’s last assessment and do not now, newest first.

  • ?tenantId= Only this tenant’s drift.
  • ?tag= Only the drift of tenants with this tag, such as managed.
  • ?seen= seen, unseen, or all (the default).
  • ?limit= How many in a page: 50 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only drift found, found again or marked seen since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of Drift, and nextCursor for the next.

read
Audit log
GET/api/v1/audit-log

List the audit log. The workspace’s audit log, newest first: who did what, when, from where. Needs a key made by an Admin or Owner, as the console’s Audit log does. A key limited to some tenants sees only what happened at them.

  • ?actorId= Only what this member (or key) did, by id.
  • ?actorType= Only what members (USER), keys (API_KEY) or ConfigCheckup itself (SYSTEM) did.
  • ?action= Only this action, such as finding.state_changed; end it in . or .* for every action that starts so, such as finding.*
  • ?group= Only these kinds of thing done, as the console's Audit log groups them, separated by commas: approvals, changes, settings, clients, findings, alerts, admin, compliance, portal, reports, tickets, team, sign-in, billing, data, other.
  • ?tenantId= Only this tenant’s entries.
  • ?until= Only before this time: ISO 8601 with its offset, or a date.
  • ?q= Words in the action, who did it or what it was done to.
  • ?limit= How many in a page: 100 unless asked, at most 200.
  • ?cursor= The nextCursor of the page before, for the page after it. Leave it out for the first page. A cursor is for the list that gave it out.
  • ?updatedSince= Only entries made since this time: ISO 8601 with its offset, or a date (midnight UTC).

Returns data: a page of AuditEvent, and nextCursor for the next.

read
Exports
GET/api/v1/exports/{table}

Export a console table. Any table the console can export, as its Export button makes it: every row, with the filters given as the list’s own (the same query parameters as its page, such as ?client= for one tenant’s table). CSV unless asked; xlsx for an Excel workbook; json for the rows as data. The key’s creator needs the role the table’s page needs. A key limited to some tenants exports one tenant’s tables, or, limited to one tag, the lists that can be cut to it. Each export is recorded in the audit log as made by the key.

  • ?format= csv (the default), xlsx or json.

Returns the file itself (text/csv or application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); with format=json, data: ExportTable.

read
Reference
GET/api/v1/openapi.json

This document. The OpenAPI 3.1 description of the API. No key needed.

Returns this document.

No key needed
GET/api/v1/power-automate/apiDefinition.swagger.json

Power Automate connector. Every operation that takes a key, as the OpenAPI 2.0 (Swagger) definition Power Automate, Power Apps and Logic Apps import for a custom connector. Import it from this address, or download it from Settings › API and webhooks. No key needed.

Returns the connector’s OpenAPI 2.0 definition.

No key needed
GET/api/v1/power-automate/apiProperties.json

Power Automate connector properties. The connector’s properties for Microsoft’s paconn command-line tool, beside the definition: how it asks for the key, and its colour. No key needed.

Returns the connector’s properties.

No key needed

What comes back

Tenant

A tenant in the workspace.

FieldTypeWhat it is
idstringConfigCheckup’s id for the tenant.
displayNamestringThe tenant’s name, as the workspace has it.
microsoftTenantIdstringThe Microsoft Entra tenant id.
defaultDomainstring or nullThe tenant’s default domain, once it has been read.
statusone of PENDING_CONSENT, CONNECTED, DEGRADED, ERROR, DISCONNECTEDThe connection: whether ConfigCheckup can read the tenant.
tagslist of stringThe workspace’s tags on the tenant, lower case.
lastScoreinteger or nullPosture score, 0 to 100, from the latest completed assessment.
lastAssessmentAtdate-time or nullWhen it was last read in full.
createdAtdate-timeWhen it was added to the workspace.
updatedAtdate-timeWhen anything about it last changed, its score included: what updatedSince compares.

TenantDetail

A tenant with its latest completed assessment.

Every field of Tenant, and:

FieldTypeWhat it is
latestAssessmentAssessment or nullIts latest completed assessment; null before the first.

Assessment

One read of a tenant, and its score.

FieldTypeWhat it is
idstringConfigCheckup’s id for the assessment.
tenantIdstringThe tenant it read.
statusone of QUEUED, COLLECTING, EVALUATING, COMPLETED, FAILED, CANCELEDWhere it is: queued, reading the tenant, evaluating, or finished one way or another.
triggerone of MANUAL, SCHEDULED, API, ONBOARDING, FIX_CHECKWhat started it: a person, the schedule, the API, onboarding, or a check after a fix.
scoreinteger or nullPosture score, 0 to 100; null until it completes.
gradestring or nullThe score as a grade: A, A-, B+, B, C+, C, D or E; null until it completes.
countsobject (critical, high, medium, low, info) or nullFailing and warning findings by severity; null until it completes.
errorstring or nullWhy it failed, when it did.
createdAtdate-timeWhen it was queued.
startedAtdate-time or nullWhen it began reading the tenant.
finishedAtdate-time or nullWhen it finished.
thinnedAtdate-time or nullSet when older history was thinned: the score and counts stay, the findings and collected data do not.
stillReadingbooleanTrue while ConfigCheckup is still reading what Exchange Online had left after the assessment. The score and findings can still change; assessment.completed is sent once it is done.

AssessmentDetail

An assessment with the score of each area.

Every field of Assessment, and:

FieldTypeWhat it is
categorieslist of CategoryScore or nullEach area’s score; null until it completes.

CategoryScore

The score of one area of the tenant: identity, devices, data and so on. A score of -1 means nothing in the area could be assessed.

FieldTypeWhat it is
categorystringThe area: identity, access, data, devices, collaboration, threat, licensing or governance.
scorenumberThe area’s score, 0 to 100, or -1 when nothing in it could be assessed.
passedintegerChecks passed.
failedintegerChecks failed.
warnedintegerChecks partly in place.
notEvaluatedintegerChecks that could not be assessed, counted neither way.

Finding

The result of one check in one assessment.

FieldTypeWhat it is
idstringConfigCheckup’s id for the finding.
assessmentIdstringThe assessment it is from.
tenantIdstringThe tenant it is about.
ruleIdstringThe check, such as NL-IDN-001, or CUS-… for one of the workspace’s own custom checks.
custombooleanTrue for one of the workspace’s own custom checks (from a template or written as an expression). Custom checks never count towards the score.
titlestringThe check’s name.
categorystringThe area of the tenant it belongs to.
severityone of critical, high, medium, low, infoHow much it matters if the check fails.
statusone of PASS, FAIL, WARN, NOT_APPLICABLE, ERRORThe check’s result: PASS, FAIL, WARN, NOT_APPLICABLE, or ERROR when it could not be assessed.
stateone of OPEN, IN_PROGRESS, RESOLVED, ACCEPTED_RISK, SUPPRESSED, PENDING_CONFIRMATIONWhere the workspace is with it: open, in progress or marked resolved; or the risk is accepted, which is ACCEPTED_RISK until the next assessment and SUPPRESSED after it (treat the two as one); or pending confirmation, while an approved automated fix runs or waits to show in the tenant.
summarystringWhat was found.
affectedCountintegerAccounts, devices, apps or settings affected.
remediationstring or nullHow to fix it, in a sentence.
controlsanyThe framework controls the check maps to.
firstSeenAtdate-timeWhen it was first seen failing, across assessments.
dueDatedate-time or nullWhen the workspace wants it fixed by, if set.
updatedAtdate-timeWhen it was made by its assessment, or last changed since (its state, owner, due date or snooze): what updatedSince compares.

Report

A report made from an assessment.

FieldTypeWhat it is
idstringConfigCheckup’s id for the report.
tenantIdstringThe tenant it is about.
assessmentIdstringThe assessment it is made from.
typeone of EXECUTIVE, TECHNICAL, BOARD, DELTA, LICENSING, REVIEW, LIFECYCLE, COPILOTExecutive, technical or board report; tenant review; device lifecycle; what changed (DELTA); licensing review; or Copilot readiness.
formatone of PDF, HTML, JSON, DOCXThe file’s format: PDF, HTML (saved as a PDF from the browser) or DOCX (a Word document).
statusone of QUEUED, RENDERING, READY, FAILEDQueued, being made, ready to download, or failed.
titlestringThe report’s title, with the tenant’s name.
sizeBytesinteger or nullThe file’s size once ready.
createdAtdate-timeWhen it was asked for.
updatedAtdate-timeWhen it last changed, such as becoming READY: what updatedSince compares.

ReportModel

A report’s content as structured data, the same model its document is drawn from.

FieldTypeWhat it is
reportobject (id, title, type, tenantId, assessmentId, createdAt)Which report this is: its id, title, type, tenant, assessment and when it was asked for.
modelobject (kind, schemaVersion, title, tenantName, score, grade, sections)Everything the report is drawn from: score and grade, each area, headlines, top risks, the roadmap, open and all findings with their evidence, licensing, coverage, history and progress. New fields may be added; schemaVersion changes if any is removed or changes meaning.

Change

A change to a tenant: drafted, dry-run, approved by people in the console, then run, read back and, where possible, rolled back.

FieldTypeWhat it is
idstringConfigCheckup’s id for the change.
tenantIdstringThe tenant it changes.
findingIdstring or nullThe finding it was drafted from, if any.
ruleIdstring or nullThat finding’s check, such as NL-IDN-001.
actionTypestringThe kind of change, such as tenant.updateAuthorizationPolicy.
titlestringWhat the change does, in a few words.
statusone of DRAFT, PENDING_APPROVAL, APPROVED, REJECTED, SCHEDULED, EXECUTING, SUCCEEDED, FAILED, ROLLED_BACK, CANCELED, EXPIREDDRAFT until someone sends it for approval in the console; then PENDING_APPROVAL, APPROVED or REJECTED; then SCHEDULED, EXECUTING, SUCCEEDED, FAILED or ROLLED_BACK. CANCELED if it went no further. EXPIRED when a draft, or a change waiting for approval, had no decision by expiresAt.
riskone of LOW, MEDIUM, HIGHThe risk of making the change at this tenant: the kind of change’s own, raised to HIGH by its settings or by what its dry run found at the tenant, never lowered.
undoone of full, partial, noneHow far it can be undone: full (rolling back puts everything back), partial (rolling back cannot put everything back; riskReasons says what) or none.
riskReasonslist of object (code, words)Why it is high-risk at this tenant, what cannot be undone and what rolling back cannot put back. Approving a change with any of these needs a tick on its page in the console.
blockedstring or nullSet when the change is not safe to make at this tenant: why. It cannot be sent for approval, approved or run.
approvalsNeededintegerHow many people must approve it: none for a change that only adds, otherwise one or two as the tenant asks, and two for some changes whatever the tenant asks.
approvalsCountedintegerApprovals given to the dry run stored now. One given to an earlier dry run that came out differently does not count.
dryRunDryRun
createdBystringThe member who drafted it, or on whose behalf an API key did.
throughApibooleanDrafted through the API.
apiKeyIdstring or nullThe key it was drafted through; null for the console and automation.
ticketRefstring or nullThe PSA or helpdesk ticket it was made for, as typed.
createdAtdate-timeWhen it was drafted.
updatedAtdate-timeWhen it last changed.
expiresAtdate-time or nullWhen a draft, or a change waiting for approval, expires (becomes EXPIRED) if no one has decided: 14 days after it was drafted, and again from when it was sent for approval. A change that holds every approval it needs, or waits for its scheduled time, does not expire. Null for a change drafted before expiry began, which never expires.
scheduledFordate-time or nullWhen it is to run once approved, if a time was set (“Schedule for…” in the console, or scheduledFor when drafted through the API). It is dry-run again first; outside the tenant’s change window it waits for the window to open.

ChangeDetail

A change with its dry run in full, its approvals and its runs.

Every field of Change, and:

FieldTypeWhat it is
descriptionstringWhat this kind of change does.
rationalestring or nullWhy it was drafted, such as the finding it fixes.
dryRunItemslist of object (target, before, after, skipReason, skipKind)Each object the dry run found: what it is now and what it would become, or why it would be skipped.
dryRunWarningslist of stringWhat an approver must read before saying yes.
approvalslist of ChangeApprovalDecisions so far, oldest first.
runslist of ChangeRunIts runs, oldest first.
verificationone of PASSED, RECHECKING, STILL_FAILING or nullAfter a successful run: whether the finding it fixes passes again, is being checked, or still fails.

DryRun

The dry run a change is approved on: what it would do to the tenant as it is now. Read-only; nothing is changed by it.

FieldTypeWhat it is
outcomeone of succeeded, failed, not_runHow the dry run against the live tenant went. A change whose dry run failed cannot be sent for approval until one succeeds.
summarystring or nullWhat the change would do, in a sentence; for a failed dry run, why it failed.
impactedCountinteger or nullObjects that would actually change; null when the dry run failed or has not run.
ranAtdate-time or nullWhen the dry run stored now was run.
nextStepstring or nullFor a failed dry run, what to do about it.

ChangeApproval

A decision on a change, made by a person in the console.

FieldTypeWhat it is
namestringWho decided.
decisionone of APPROVED, REJECTEDApproved or rejected.
atdate-timeWhen.
commentstring or nullWhat they wrote, if anything.
countsbooleanFor an approval: whether it was given to the dry run stored now, and so counts.

ChangeRun

One run of an approved change.

FieldTypeWhat it is
idstringThe run’s id.
statusone of RUNNING, SUCCEEDED, FAILED, PARTIAL, ROLLED_BACKRunning, succeeded, failed, partly done, or rolled back.
startedAtdate-time
finishedAtdate-time or null
ranBystring or nullThe approver who started it; null when it ran on approval or on its schedule.
succeededintegerSteps that succeeded.
failedintegerSteps that failed.
errorstring or nullWhy it failed or stopped, when it did.
stoppedAtdate-time or nullWhen it stopped part-way, if it did; it can be resumed from the console for seven days.
readBackobject (confirmed, differs, unreadable, unread) or nullEach changed object read back from Microsoft after the run: confirmed, reading differently, could not be read, or not read yet.
stepslist of object (target, status, error)Each object the run touched, and how it went.
rollbackobject (status, at, by, error) or nullThe last rollback of this run, if any: only SUCCEEDED puts the change back to ROLLED_BACK.

Baseline

A baseline: a set of checks, and each tenant’s result against it from its latest completed assessment. Read-only.

FieldTypeWhat it is
idstringConfigCheckup’s id for the baseline.
namestring
descriptionstring or null
ruleIdslist of stringThe checks a tenant must pass to meet it.
tenantTagslist of stringTenants with any of these tags are held to it; empty for every tenant.
allowWarningsbooleanWhether a check partly in place still counts as met.
clientCountintegerTenants held to it.
fullyCompliantintegerTenants meeting every check, with none unread.
driftedintegerChecks, across tenants, met at the assessment before and not now.
clientslist of BaselineClientEach tenant held to it, least compliant first.
createdAtdate-time
updatedAtdate-time

BaselineDetail

A baseline with each check’s result for each tenant.

Every field of Baseline, and:

FieldTypeWhat it is
checkslist of BaselineCheckEach check, those most tenants miss first.
resultslist of BaselineResultEach tenant’s result for each check.

BaselineClient

One tenant’s result against a baseline.

FieldTypeWhat it is
tenantIdstring
tenantNamestring
assessedAtdate-time or nullIts latest completed assessment; null when never assessed.
metintegerChecks met (passing, or not applicable).
notMetintegerChecks failing, or partly in place when the baseline does not allow that.
unknownintegerChecks that could not be read, or a tenant never assessed. Never counted as met.
compliancePercentinteger or nullShare of the checks read that are met, 0 to 100; null when none could be read.
driftedintegerChecks met at the assessment before and not now.

BaselineCheck

One check of a baseline, across its tenants.

FieldTypeWhat it is
ruleIdstring
titlestring
severityone of critical, high, medium, low, info
categorystring
metintegerTenants meeting it.
notMetintegerTenants not meeting it.
unknownintegerTenants where it could not be read.

BaselineResult

One tenant’s result for one check of a baseline.

FieldTypeWhat it is
tenantIdstring
ruleIdstring
stateone of met, excepted, not_met, unknownMet, not met, or unknown: could not be read, or never assessed.
statusone of PASS, FAIL, WARN, NOT_APPLICABLE, ERROR or nullThe check’s result in the tenant’s latest assessment; null when it has none.
driftedbooleanMet at the assessment before and not now.

FrameworkReadiness

A tenant’s readiness for each framework, from its latest completed assessment.

FieldTypeWhat it is
tenantIdstring
assessedAtdate-time or nullThe latest completed assessment it is drawn from; null before the first.
frameworkslist of Framework

Framework

A tenant’s readiness for one framework. Readiness is not certification; it covers the controls Microsoft 365 can evidence.

FieldTypeWhat it is
idstringThe framework, such as Cyber-Essentials-Plus, ISO-27001 or NIS2.
namestring
percentinteger or nullShare of the controls that could be read that are met, counting partly met as half; null when none could be.
countsobject (met, partly, notMet, unknown, notEvidenced)Controls by state.
controlslist of FrameworkControl or nullEach control, when one framework is asked for with ?framework=; null otherwise.

FrameworkControl

One control of a framework, and whether the tenant meets it.

FieldTypeWhat it is
controlstringThe control’s reference, such as A.8.5 or 3.1.12.
groupstring or nullThe heading it sits under, for frameworks listed in full.
stateone of met, partly, not_met, unknown, not_evidencedMet, partly met, not met, unknown (could not be read), or not evidenced by any check (your own records).
checkslist of object (ruleId, title, status, accepted)The checks that evidence it.

Licence

A licence a tenant subscribes to, and its seats.

FieldTypeWhat it is
skuIdstringMicrosoft’s id for the licence (SKU).
skuPartNumberstringMicrosoft’s short name, such as SPE_E3.
namestringThe licence’s name.
seatsinteger or nullSeats bought; null when unlimited (a free licence).
assignedintegerSeats assigned to accounts.
availableinteger or nullSeats bought and not assigned; null when unlimited.
suspendedintegerSeats suspended (lapsed, not yet removed).
warningintegerSeats in their grace period before suspension.
trialbooleanEvery subscription behind it is a trial.
renewsAtdate-time or nullWhen its earliest subscription next renews, ends or converts, when Microsoft says.
listPriceGbpnumber or nullUK list price per seat a month, excluding VAT, when known.
readAtdate-timeWhen it was read, with the tenant’s latest completed assessment.

Device

A device in a tenant, from Intune and Entra ID, with where it is in its life.

FieldTypeWhat it is
deviceIdstringThe device’s id in Intune, or in Entra ID for a device not in Intune.
namestring
manufacturerstring or null
modelstring or null
serialNumberstring or null
operatingSystemstring or null
osVersionstring or null
ownershipstring or nullcompany or personal, as Intune has it.
complianceStatestring or nullcompliant, noncompliant and so on, as Intune reports it.
encryptedboolean or null
managedboolean or nullManaged by Intune.
autopilotboolean or nullRegistered with Windows Autopilot.
primaryUserstring or nullIts primary user’s sign-in name, where set.
primaryUserNamestring or null
lastSeenAtdate-time or nullIts latest check-in or sign-in.
daysSinceSeeninteger or null
enrolledAtdate-time or null
ageYearsnumber or nullEstimated hardware age in years.
lifecycleone of end_of_life, approaching_end_of_life, supported, unknownWhere it is in its life, as the Devices page says.
actionone of replace, plan_replacement, update_os, review, noneWhat to do about it: replace it, plan its replacement, update its system, review it, or nothing.
replaceBydate-time or nullWhen it needs replacing, where known.
readAtdate-timeWhen it was read, with the tenant’s latest completed assessment.

Alert

A security alert in a tenant: a new admin, a risky app consent, an MFA method removed, a Microsoft Defender alert or a match of the workspace’s own alert rule.

FieldTypeWhat it is
idstringConfigCheckup’s id for the alert.
tenantIdstringThe tenant it happened in.
kindstringWhat happened, such as admin_role_added, app_consent, mfa_method_removed, defender_alert or custom_rule.
severityone of high, mediumHow urgent it is.
titlestringWhat happened, in a few words.
detailstringWhat happened, in a sentence.
actorstring or nullWho did it, when known.
targetstring or nullWho or what it was done to, when known.
occurredAtdate-timeWhen it happened in the tenant.
statusone of open, dismissedOpen, or dismissed by someone in the workspace.
dismissedAtdate-time or nullWhen it was dismissed.
dismissReasonstring or nullWhy: expected, false_positive or handled.
dismissedBystring or nullWho dismissed it.
claimedBystring or nullWho is looking into it (“I’m on it” in the console), if anyone.
createdAtdate-timeWhen ConfigCheckup raised it.
updatedAtdate-timeWhen it last changed (claimed, released, dismissed): what updatedSince compares.

Drift

Drift: checks that passed at a tenant’s last assessment and do not now, found by the hourly drift check.

FieldTypeWhat it is
idstringConfigCheckup’s id for the drift check.
tenantIdstring
checkedAtdate-timeWhen the tenant was checked and the drift found.
confirmedAtdate-time or nullWhen a later check last found exactly this again.
seenbooleanSomeone has marked it seen.
seenAtdate-time or null
regressionslist of DriftRegressionThe checks that stopped passing since the tenant’s last assessment.

DriftRegression

A check that stopped passing.

FieldTypeWhat it is
ruleIdstring
titlestring
summarystring
fromstringIts result before: pass, warn or not_applicable.
tostringIts result now: warn or fail.

AuditEvent

One entry in the workspace’s audit log.

FieldTypeWhat it is
idstring
atdate-timeWhen it happened.
actionstringWhat happened, as a stable code such as finding.state_changed or workspace.api_key_created.
sentencestringWhat happened, in words, as the console’s audit log says it.
actorTypeone of USER, API_KEY, SYSTEMA member, an API key or ConfigCheckup itself.
actorIdstring or nullThe member’s or key’s id.
actorNamestring or nullThe member’s name, or the key as api:<its name>.
tenantIdstring or nullThe tenant it was about; null for the workspace itself.
targetTypestring or null
targetIdstring or null
ipstring or nullThe address it came from, when it came from a person or a key.
metadataanyThe detail recorded with it. Its fields depend on the action.

ExportTable

A console table as data: the same columns and rows its CSV or Excel export holds.

FieldTypeWhat it is
titlestringThe table’s name, as the file would be called.
columnslist of object (label, type)Each column: its heading and the type of its values.
rowslist of list of anyEach row’s values, in the columns’ order. Dates are ISO 8601 in UTC.

Error

Every error: a stable code and a sentence saying what went wrong.

FieldTypeWhat it is
errorobject (code, message)code stays the same for the life of v1, so code can act on it; message is for people.

Webhooks

Add an HTTPS endpoint and choose its events. Each delivery is a POST with a JSON body { id, event, createdAt, data } and these headers: ConfigCheckup-Event, ConfigCheckup-Delivery (the event id, the same on every retry, so you can drop repeats), ConfigCheckup-Attempt (1 for the first try), ConfigCheckup-Replay: true on a delivery sent again by hand, and ConfigCheckup-Signature: t=…,v1=…. Answer with any 2xx within ten seconds.

A delivery that fails is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours: seven tries over about 21 hours. A redirect, or a 4xx other than 408 or 429, is not tried again. Once twenty deliveries in a row have failed, over at least a day, the endpoint is paused and the workspace’s Owners and Admins are emailed; events while it is paused are logged as not sent, to send again. Every delivery is kept for 30 days on the endpoint’s page in Settings, with what your endpoint answered, and can be sent again from there, with the same event id.

  • assessment.completedAn assessment finished. When Exchange Online is read after the assessment, this is sent once that read is done, with the final score. data: assessmentId, tenantId, tenantName, score, grade, counts
  • assessment.failedAn assessment failed. data: assessmentId, tenantId, tenantName, error
  • drift.detectedA critical control stopped passing. data: tenantId, tenantName, regressions
  • security.eventSecurity events in a tenant: a new admin, a risky app consent, an MFA method removed and similar, a Microsoft Defender alert (kind defender_alert), or a match of the workspace’s own alert rule (kind custom_rule). data: tenantId, tenantName, events
  • report.readyA report is ready to download. data: reportId, tenantId, assessmentId, type, format, title
  • lead.createdSomeone ran your free health check and became a lead. data: leadId, domain, company, contactName, contactEmail, grade, score, microsoft365, source, createdAt
  • finding.openedA check that was not failing or warning at a tenant now does: one event for each, from an assessment or a finding check. A check that could not be read in between is measured against the last result that was read, so one that failed, could not be read and fails again sends nothing. Not sent for a tenant’s first assessment, which has nothing to compare with (assessment.completed covers it), nor for a check whose risk is accepted or which is suppressed. data: findingId, assessmentId, tenantId, tenantName, ruleId, custom, title, category, severity, status, previousStatus, summary, affectedCount, firstSeenAt
  • finding.resolvedA check that was failing or warning at a tenant now passes, or no longer applies: one event for each. A check that could not be read in between is measured against the last result that was read. data: findingId, assessmentId, tenantId, tenantName, ruleId, custom, title, category, severity, status, previousStatus, summary, affectedCount, firstSeenAt
  • change.draftedA change was drafted and dry-run: in the console, by an automatic fix or a tenant’s portal request, or through the API. data: changeId, tenantId, tenantName, actionType, title, risk, status, findingId, ruleId, createdBy, throughApi, ticketRef, dryRun
  • change.approvedA change has every approval it needs and is set to run (status APPROVED or SCHEDULED). data: changeId, tenantId, tenantName, actionType, title, risk, status, findingId, ruleId, createdBy, throughApi, ticketRef, approvedBy, scheduledFor
  • change.succeededA change ran to the end. steps.failed counts any steps that could not be made; partlyDone is true when there are any. data: changeId, tenantId, tenantName, actionType, title, risk, status, findingId, ruleId, createdBy, throughApi, ticketRef, steps, finishedAt, partlyDone
  • change.failedA change failed, stopped part-way, or could not start. When resuming is true the run was stopped by a restart and carries on by itself, so this is not its end: wait for the change.succeeded or change.failed that follows before treating it as final. data: changeId, tenantId, tenantName, actionType, title, risk, status, findingId, ruleId, createdBy, throughApi, ticketRef, steps, finishedAt, error, partlyDone, resuming
  • change.rolled_backEvery step of a change was undone. A rollback that undid only some steps sends nothing. data: changeId, tenantId, tenantName, actionType, title, risk, status, findingId, ruleId, createdBy, throughApi, ticketRef, rolledBackBy, rolledBackAt
  • tenant.connectedA new tenant’s consent completed and its connection works: once for each tenant. A lead’s tenant sends it when the lead is made a tenant. data: tenantId, tenantName, microsoftTenantId, defaultDomain, connectedAt
  • tenant.disconnectedA tenant was removed from the workspace, with its data. ConfigCheckup no longer reads it. data: tenantId, tenantName, microsoftTenantId, removedAt, removedBy
  • review.signedA tenant signed an access review on their portal. data: reviewId, tenantId, tenantName, signedBy, signedAt, entries, removals
  • quote.acceptedA tenant accepted a quote. data: quoteId, number, tenantId, tenantName, title, total, acceptedBy, acceptedAt, via
  • pingThe test delivery sent from Settings. data: message

Verify every delivery. v1 is HMAC-SHA256 of t.rawBody under the endpoint's signing secret. When the secret is replaced, the old one can go on signing for up to a week: each delivery then carries two v1 values, one per secret, so accept a delivery if any v1 matches.

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: the request body exactly as received, before JSON parsing.
// After the secret is replaced, a delivery carries two v1 values for a while
// (one per secret): accept it if any of them matches yours.
export function verify(rawBody, header, secret) {
  const parts = (header ?? '').split(',').map((p) => p.trim().split('='));
  const t = parts.find(([k]) => k === 't')?.[1];
  const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => Buffer.from(v ?? ''));
  const age = Math.abs(Date.now() / 1000 - Number(t));
  if (!t || signatures.length === 0 || !(age <= 300)) return false; // reject replays older than 5 minutes
  const expected = Buffer.from(createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'));
  // Compare only signatures of the same length: timingSafeEqual throws otherwise.
  return signatures.some((given) => given.length === expected.length && timingSafeEqual(expected, given));
}

Questions or something missing? Email support@configcheckup.com.

ConfigCheckup

Build against the API on your own tenants.

The docs are open now. The read API comes with Monitor, and the write API and webhooks with Automate; the trial does not include them.

14 days of daily monitoring and alerts, every finding with its fix guide, and reports on up to 3 tenants, from your first connected tenant. No card. Then choose the plan that fits.