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
| Method | Path | What it does | Key scope | |||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Tenants | ||||||||||||||||||||||||||||||||||||
| GET | /api/v1/tenants | List tenants. Every tenant the key sees, A to Z, a page at a time.
Returns | 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 | read | |||||||||||||||||||||||||||||||||
| Findings | ||||||||||||||||||||||||||||||||||||
| GET | /api/v1/tenants/{id}/findings | List a tenant’s findings. Findings from the tenant’s latest completed assessment, heaviest first.
Returns | 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.
Returns | 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.
Returns | read | |||||||||||||||||||||||||||||||||
| Assessments | ||||||||||||||||||||||||||||||||||||
| GET | /api/v1/tenants/{id}/assessments | List a tenant’s assessments. The tenant’s assessments, newest first.
Returns | 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.
Returns | 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 | read | |||||||||||||||||||||||||||||||||
| Reports | ||||||||||||||||||||||||||||||||||||
| GET | /api/v1/reports | List reports. Reports, newest first.
Returns | 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.
The JSON body it takesWhat report to make: its type, its file format, and the assessment it is made from.
Returns | 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 | 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.
The JSON body it takesWhat 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.
Returns | 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.
Returns | 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 | 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.
Returns | 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 | 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.
Returns | 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.
Returns | 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.
Returns | 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.
Returns | 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.
Returns | 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.
Returns | 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.
Returns the file itself (text/csv or application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); with format=json, | 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.
| Field | Type | What it is |
|---|---|---|
| id | string | ConfigCheckup’s id for the tenant. |
| displayName | string | The tenant’s name, as the workspace has it. |
| microsoftTenantId | string | The Microsoft Entra tenant id. |
| defaultDomain | string or null | The tenant’s default domain, once it has been read. |
| status | one of PENDING_CONSENT, CONNECTED, DEGRADED, ERROR, DISCONNECTED | The connection: whether ConfigCheckup can read the tenant. |
| tags | list of string | The workspace’s tags on the tenant, lower case. |
| lastScore | integer or null | Posture score, 0 to 100, from the latest completed assessment. |
| lastAssessmentAt | date-time or null | When it was last read in full. |
| createdAt | date-time | When it was added to the workspace. |
| updatedAt | date-time | When anything about it last changed, its score included: what updatedSince compares. |
TenantDetail
A tenant with its latest completed assessment.
Every field of Tenant, and:
| Field | Type | What it is |
|---|---|---|
| latestAssessment | Assessment or null | Its latest completed assessment; null before the first. |
Assessment
One read of a tenant, and its score.
| Field | Type | What it is |
|---|---|---|
| id | string | ConfigCheckup’s id for the assessment. |
| tenantId | string | The tenant it read. |
| status | one of QUEUED, COLLECTING, EVALUATING, COMPLETED, FAILED, CANCELED | Where it is: queued, reading the tenant, evaluating, or finished one way or another. |
| trigger | one of MANUAL, SCHEDULED, API, ONBOARDING, FIX_CHECK | What started it: a person, the schedule, the API, onboarding, or a check after a fix. |
| score | integer or null | Posture score, 0 to 100; null until it completes. |
| grade | string or null | The score as a grade: A, A-, B+, B, C+, C, D or E; null until it completes. |
| counts | object (critical, high, medium, low, info) or null | Failing and warning findings by severity; null until it completes. |
| error | string or null | Why it failed, when it did. |
| createdAt | date-time | When it was queued. |
| startedAt | date-time or null | When it began reading the tenant. |
| finishedAt | date-time or null | When it finished. |
| thinnedAt | date-time or null | Set when older history was thinned: the score and counts stay, the findings and collected data do not. |
| stillReading | boolean | True 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:
| Field | Type | What it is |
|---|---|---|
| categories | list of CategoryScore or null | Each 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.
| Field | Type | What it is |
|---|---|---|
| category | string | The area: identity, access, data, devices, collaboration, threat, licensing or governance. |
| score | number | The area’s score, 0 to 100, or -1 when nothing in it could be assessed. |
| passed | integer | Checks passed. |
| failed | integer | Checks failed. |
| warned | integer | Checks partly in place. |
| notEvaluated | integer | Checks that could not be assessed, counted neither way. |
Finding
The result of one check in one assessment.
| Field | Type | What it is |
|---|---|---|
| id | string | ConfigCheckup’s id for the finding. |
| assessmentId | string | The assessment it is from. |
| tenantId | string | The tenant it is about. |
| ruleId | string | The check, such as NL-IDN-001, or CUS-… for one of the workspace’s own custom checks. |
| custom | boolean | True for one of the workspace’s own custom checks (from a template or written as an expression). Custom checks never count towards the score. |
| title | string | The check’s name. |
| category | string | The area of the tenant it belongs to. |
| severity | one of critical, high, medium, low, info | How much it matters if the check fails. |
| status | one of PASS, FAIL, WARN, NOT_APPLICABLE, ERROR | The check’s result: PASS, FAIL, WARN, NOT_APPLICABLE, or ERROR when it could not be assessed. |
| state | one of OPEN, IN_PROGRESS, RESOLVED, ACCEPTED_RISK, SUPPRESSED, PENDING_CONFIRMATION | Where 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. |
| summary | string | What was found. |
| affectedCount | integer | Accounts, devices, apps or settings affected. |
| remediation | string or null | How to fix it, in a sentence. |
| controls | any | The framework controls the check maps to. |
| firstSeenAt | date-time | When it was first seen failing, across assessments. |
| dueDate | date-time or null | When the workspace wants it fixed by, if set. |
| updatedAt | date-time | When 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.
| Field | Type | What it is |
|---|---|---|
| id | string | ConfigCheckup’s id for the report. |
| tenantId | string | The tenant it is about. |
| assessmentId | string | The assessment it is made from. |
| type | one of EXECUTIVE, TECHNICAL, BOARD, DELTA, LICENSING, REVIEW, LIFECYCLE, COPILOT | Executive, technical or board report; tenant review; device lifecycle; what changed (DELTA); licensing review; or Copilot readiness. |
| format | one of PDF, HTML, JSON, DOCX | The file’s format: PDF, HTML (saved as a PDF from the browser) or DOCX (a Word document). |
| status | one of QUEUED, RENDERING, READY, FAILED | Queued, being made, ready to download, or failed. |
| title | string | The report’s title, with the tenant’s name. |
| sizeBytes | integer or null | The file’s size once ready. |
| createdAt | date-time | When it was asked for. |
| updatedAt | date-time | When 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.
| Field | Type | What it is |
|---|---|---|
| report | object (id, title, type, tenantId, assessmentId, createdAt) | Which report this is: its id, title, type, tenant, assessment and when it was asked for. |
| model | object (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.
| Field | Type | What it is |
|---|---|---|
| id | string | ConfigCheckup’s id for the change. |
| tenantId | string | The tenant it changes. |
| findingId | string or null | The finding it was drafted from, if any. |
| ruleId | string or null | That finding’s check, such as NL-IDN-001. |
| actionType | string | The kind of change, such as tenant.updateAuthorizationPolicy. |
| title | string | What the change does, in a few words. |
| status | one of DRAFT, PENDING_APPROVAL, APPROVED, REJECTED, SCHEDULED, EXECUTING, SUCCEEDED, FAILED, ROLLED_BACK, CANCELED, EXPIRED | DRAFT 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. |
| risk | one of LOW, MEDIUM, HIGH | The 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. |
| undo | one of full, partial, none | How far it can be undone: full (rolling back puts everything back), partial (rolling back cannot put everything back; riskReasons says what) or none. |
| riskReasons | list 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. |
| blocked | string or null | Set when the change is not safe to make at this tenant: why. It cannot be sent for approval, approved or run. |
| approvalsNeeded | integer | How 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. |
| approvalsCounted | integer | Approvals given to the dry run stored now. One given to an earlier dry run that came out differently does not count. |
| dryRun | DryRun | |
| createdBy | string | The member who drafted it, or on whose behalf an API key did. |
| throughApi | boolean | Drafted through the API. |
| apiKeyId | string or null | The key it was drafted through; null for the console and automation. |
| ticketRef | string or null | The PSA or helpdesk ticket it was made for, as typed. |
| createdAt | date-time | When it was drafted. |
| updatedAt | date-time | When it last changed. |
| expiresAt | date-time or null | When 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. |
| scheduledFor | date-time or null | When 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:
| Field | Type | What it is |
|---|---|---|
| description | string | What this kind of change does. |
| rationale | string or null | Why it was drafted, such as the finding it fixes. |
| dryRunItems | list 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. |
| dryRunWarnings | list of string | What an approver must read before saying yes. |
| approvals | list of ChangeApproval | Decisions so far, oldest first. |
| runs | list of ChangeRun | Its runs, oldest first. |
| verification | one of PASSED, RECHECKING, STILL_FAILING or null | After 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.
| Field | Type | What it is |
|---|---|---|
| outcome | one of succeeded, failed, not_run | How the dry run against the live tenant went. A change whose dry run failed cannot be sent for approval until one succeeds. |
| summary | string or null | What the change would do, in a sentence; for a failed dry run, why it failed. |
| impactedCount | integer or null | Objects that would actually change; null when the dry run failed or has not run. |
| ranAt | date-time or null | When the dry run stored now was run. |
| nextStep | string or null | For a failed dry run, what to do about it. |
ChangeApproval
A decision on a change, made by a person in the console.
| Field | Type | What it is |
|---|---|---|
| name | string | Who decided. |
| decision | one of APPROVED, REJECTED | Approved or rejected. |
| at | date-time | When. |
| comment | string or null | What they wrote, if anything. |
| counts | boolean | For an approval: whether it was given to the dry run stored now, and so counts. |
ChangeRun
One run of an approved change.
| Field | Type | What it is |
|---|---|---|
| id | string | The run’s id. |
| status | one of RUNNING, SUCCEEDED, FAILED, PARTIAL, ROLLED_BACK | Running, succeeded, failed, partly done, or rolled back. |
| startedAt | date-time | |
| finishedAt | date-time or null | |
| ranBy | string or null | The approver who started it; null when it ran on approval or on its schedule. |
| succeeded | integer | Steps that succeeded. |
| failed | integer | Steps that failed. |
| error | string or null | Why it failed or stopped, when it did. |
| stoppedAt | date-time or null | When it stopped part-way, if it did; it can be resumed from the console for seven days. |
| readBack | object (confirmed, differs, unreadable, unread) or null | Each changed object read back from Microsoft after the run: confirmed, reading differently, could not be read, or not read yet. |
| steps | list of object (target, status, error) | Each object the run touched, and how it went. |
| rollback | object (status, at, by, error) or null | The 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.
| Field | Type | What it is |
|---|---|---|
| id | string | ConfigCheckup’s id for the baseline. |
| name | string | |
| description | string or null | |
| ruleIds | list of string | The checks a tenant must pass to meet it. |
| tenantTags | list of string | Tenants with any of these tags are held to it; empty for every tenant. |
| allowWarnings | boolean | Whether a check partly in place still counts as met. |
| clientCount | integer | Tenants held to it. |
| fullyCompliant | integer | Tenants meeting every check, with none unread. |
| drifted | integer | Checks, across tenants, met at the assessment before and not now. |
| clients | list of BaselineClient | Each tenant held to it, least compliant first. |
| createdAt | date-time | |
| updatedAt | date-time |
BaselineDetail
A baseline with each check’s result for each tenant.
Every field of Baseline, and:
| Field | Type | What it is |
|---|---|---|
| checks | list of BaselineCheck | Each check, those most tenants miss first. |
| results | list of BaselineResult | Each tenant’s result for each check. |
BaselineClient
One tenant’s result against a baseline.
| Field | Type | What it is |
|---|---|---|
| tenantId | string | |
| tenantName | string | |
| assessedAt | date-time or null | Its latest completed assessment; null when never assessed. |
| met | integer | Checks met (passing, or not applicable). |
| notMet | integer | Checks failing, or partly in place when the baseline does not allow that. |
| unknown | integer | Checks that could not be read, or a tenant never assessed. Never counted as met. |
| compliancePercent | integer or null | Share of the checks read that are met, 0 to 100; null when none could be read. |
| drifted | integer | Checks met at the assessment before and not now. |
BaselineCheck
One check of a baseline, across its tenants.
| Field | Type | What it is |
|---|---|---|
| ruleId | string | |
| title | string | |
| severity | one of critical, high, medium, low, info | |
| category | string | |
| met | integer | Tenants meeting it. |
| notMet | integer | Tenants not meeting it. |
| unknown | integer | Tenants where it could not be read. |
BaselineResult
One tenant’s result for one check of a baseline.
| Field | Type | What it is |
|---|---|---|
| tenantId | string | |
| ruleId | string | |
| state | one of met, excepted, not_met, unknown | Met, not met, or unknown: could not be read, or never assessed. |
| status | one of PASS, FAIL, WARN, NOT_APPLICABLE, ERROR or null | The check’s result in the tenant’s latest assessment; null when it has none. |
| drifted | boolean | Met at the assessment before and not now. |
FrameworkReadiness
A tenant’s readiness for each framework, from its latest completed assessment.
| Field | Type | What it is |
|---|---|---|
| tenantId | string | |
| assessedAt | date-time or null | The latest completed assessment it is drawn from; null before the first. |
| frameworks | list of Framework |
Framework
A tenant’s readiness for one framework. Readiness is not certification; it covers the controls Microsoft 365 can evidence.
| Field | Type | What it is |
|---|---|---|
| id | string | The framework, such as Cyber-Essentials-Plus, ISO-27001 or NIS2. |
| name | string | |
| percent | integer or null | Share of the controls that could be read that are met, counting partly met as half; null when none could be. |
| counts | object (met, partly, notMet, unknown, notEvidenced) | Controls by state. |
| controls | list of FrameworkControl or null | Each control, when one framework is asked for with ?framework=; null otherwise. |
FrameworkControl
One control of a framework, and whether the tenant meets it.
| Field | Type | What it is |
|---|---|---|
| control | string | The control’s reference, such as A.8.5 or 3.1.12. |
| group | string or null | The heading it sits under, for frameworks listed in full. |
| state | one of met, partly, not_met, unknown, not_evidenced | Met, partly met, not met, unknown (could not be read), or not evidenced by any check (your own records). |
| checks | list of object (ruleId, title, status, accepted) | The checks that evidence it. |
Licence
A licence a tenant subscribes to, and its seats.
| Field | Type | What it is |
|---|---|---|
| skuId | string | Microsoft’s id for the licence (SKU). |
| skuPartNumber | string | Microsoft’s short name, such as SPE_E3. |
| name | string | The licence’s name. |
| seats | integer or null | Seats bought; null when unlimited (a free licence). |
| assigned | integer | Seats assigned to accounts. |
| available | integer or null | Seats bought and not assigned; null when unlimited. |
| suspended | integer | Seats suspended (lapsed, not yet removed). |
| warning | integer | Seats in their grace period before suspension. |
| trial | boolean | Every subscription behind it is a trial. |
| renewsAt | date-time or null | When its earliest subscription next renews, ends or converts, when Microsoft says. |
| listPriceGbp | number or null | UK list price per seat a month, excluding VAT, when known. |
| readAt | date-time | When 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.
| Field | Type | What it is |
|---|---|---|
| deviceId | string | The device’s id in Intune, or in Entra ID for a device not in Intune. |
| name | string | |
| manufacturer | string or null | |
| model | string or null | |
| serialNumber | string or null | |
| operatingSystem | string or null | |
| osVersion | string or null | |
| ownership | string or null | company or personal, as Intune has it. |
| complianceState | string or null | compliant, noncompliant and so on, as Intune reports it. |
| encrypted | boolean or null | |
| managed | boolean or null | Managed by Intune. |
| autopilot | boolean or null | Registered with Windows Autopilot. |
| primaryUser | string or null | Its primary user’s sign-in name, where set. |
| primaryUserName | string or null | |
| lastSeenAt | date-time or null | Its latest check-in or sign-in. |
| daysSinceSeen | integer or null | |
| enrolledAt | date-time or null | |
| ageYears | number or null | Estimated hardware age in years. |
| lifecycle | one of end_of_life, approaching_end_of_life, supported, unknown | Where it is in its life, as the Devices page says. |
| action | one of replace, plan_replacement, update_os, review, none | What to do about it: replace it, plan its replacement, update its system, review it, or nothing. |
| replaceBy | date-time or null | When it needs replacing, where known. |
| readAt | date-time | When 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.
| Field | Type | What it is |
|---|---|---|
| id | string | ConfigCheckup’s id for the alert. |
| tenantId | string | The tenant it happened in. |
| kind | string | What happened, such as admin_role_added, app_consent, mfa_method_removed, defender_alert or custom_rule. |
| severity | one of high, medium | How urgent it is. |
| title | string | What happened, in a few words. |
| detail | string | What happened, in a sentence. |
| actor | string or null | Who did it, when known. |
| target | string or null | Who or what it was done to, when known. |
| occurredAt | date-time | When it happened in the tenant. |
| status | one of open, dismissed | Open, or dismissed by someone in the workspace. |
| dismissedAt | date-time or null | When it was dismissed. |
| dismissReason | string or null | Why: expected, false_positive or handled. |
| dismissedBy | string or null | Who dismissed it. |
| claimedBy | string or null | Who is looking into it (“I’m on it” in the console), if anyone. |
| createdAt | date-time | When ConfigCheckup raised it. |
| updatedAt | date-time | When 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.
| Field | Type | What it is |
|---|---|---|
| id | string | ConfigCheckup’s id for the drift check. |
| tenantId | string | |
| checkedAt | date-time | When the tenant was checked and the drift found. |
| confirmedAt | date-time or null | When a later check last found exactly this again. |
| seen | boolean | Someone has marked it seen. |
| seenAt | date-time or null | |
| regressions | list of DriftRegression | The checks that stopped passing since the tenant’s last assessment. |
DriftRegression
A check that stopped passing.
| Field | Type | What it is |
|---|---|---|
| ruleId | string | |
| title | string | |
| summary | string | |
| from | string | Its result before: pass, warn or not_applicable. |
| to | string | Its result now: warn or fail. |
AuditEvent
One entry in the workspace’s audit log.
| Field | Type | What it is |
|---|---|---|
| id | string | |
| at | date-time | When it happened. |
| action | string | What happened, as a stable code such as finding.state_changed or workspace.api_key_created. |
| sentence | string | What happened, in words, as the console’s audit log says it. |
| actorType | one of USER, API_KEY, SYSTEM | A member, an API key or ConfigCheckup itself. |
| actorId | string or null | The member’s or key’s id. |
| actorName | string or null | The member’s name, or the key as api:<its name>. |
| tenantId | string or null | The tenant it was about; null for the workspace itself. |
| targetType | string or null | |
| targetId | string or null | |
| ip | string or null | The address it came from, when it came from a person or a key. |
| metadata | any | The 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.
| Field | Type | What it is |
|---|---|---|
| title | string | The table’s name, as the file would be called. |
| columns | list of object (label, type) | Each column: its heading and the type of its values. |
| rows | list of list of any | Each 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.
| Field | Type | What it is |
|---|---|---|
| error | object (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.