API reference
Read the scores, findings and changes of your domains, and start audits, from your scripts, your CI, your automation tools and your AI assistants. From the Solo plan.
Getting started
The API answers at https://app.perimetry.cloud. Create a key in Settings › API keys (owners and admins), then send it in the Authorization header of every request, never in the address:
Authorization: Bearer pc_…A key that only reads is enough for everything but starting audits, which needs a key with the audit scope. How to create and revoke keys: API keys.
Check a key:
curl -H "Authorization: Bearer $PERIMETRY_API_KEY" https://app.perimetry.cloud/api/v1/meDomains by id or by name
Wherever a path takes {id}, give the id of the domain or its name (example.com). A domain of another workspace answers 404, like a missing one.
Versions
Every answer carries Perimetry-Api-Version: 1. Version 1 only ever gains fields: nothing is renamed or removed. Ignore the fields you do not know.
The specification
The same description, in OpenAPI 3.1, for Postman, Insomnia, code generators and automation tools. Public, without a key: https://app.perimetry.cloud/api/v1/openapi.json. This page is built from it.
Without code
Connectors for n8n, Zapier and Make are being prepared on top of this API. Until they are published, the HTTP request module of each tool works with the examples below; for an alert as soon as an audit finishes, add the webhook address of your scenario as a Webhook channel on the Alerts page: it receives the summary of each audit in JSON.
Limits
Calls are counted for the whole workspace, per clock hour:
| Plan | Calls per hour |
|---|---|
| Solo | 300 |
| Agency | 1,200 |
| Agency Plus | 3,600 |
Every authenticated answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time of the next hour). Beyond the limit, the API answers 429 with Retry-After. After sixty requests with a wrong key in fifteen minutes from one network, the next ones are refused: wait fifteen minutes.
Audits on demand follow the rule of the Audit now button (one an hour per domain on Solo) and, through the API, one every 15 minutes per domain.
Errors
Errors answer in JSON with a sentence and a stable code:
{"error": "invalid API key", "code": "unauthorized"}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter | A parameter has a value the API does not accept; the message names the values allowed. |
| 401 | unauthorized | The key is missing, malformed, revoked or unknown. |
| 403 | plan_excludes_api | The plan of the workspace does not include the API. |
| 403 | insufficient_scope | The key cannot start audits: create one with the audit scope. |
| 404 | not_found | Not in this workspace (a domain of another workspace answers the same). |
| 404 | no_audit | The domain has no audit yet. |
| 409 | not_verified | An audit was asked for a domain that is not proved. |
| 429 | rate_limited | The hourly limit is reached: wait for Retry-After seconds. |
| 429 | too_many_failures | Too many requests with a wrong key from your network: fifteen minutes’ wait. |
| 429 | audit_too_soon | An audit on demand of this domain ran too recently: wait for Retry-After seconds. |
| 500 | internal | Something went wrong on our side. |
| 503 | unavailable | The audit could not be queued: try again. |
| 507 | storage_quota | The workspace has used the storage of its plan: delete old data or move to a larger plan. |
| 507 | storage_full | Our storage is full for the moment: try again later. |
Account
The key and its workspace.
GET /api/v1/me
The key, its workspace and its hourly limit
The workspace and plan of the key, what it may do (its scopes), and what is left of the hourly limit. A good first call to check a key.
Example
curl -H "Authorization: Bearer $PERIMETRY_API_KEY" \
"https://app.perimetry.cloud/api/v1/me"Answer 200
{
"apiVersion": 1,
"workspace": {
"name": "Acme",
"plan": "agency"
},
"key": {
"name": "GitHub Actions",
"prefix": "pc_3kQx",
"scopes": [
"read",
"audit"
]
},
"rateLimit": {
"limit": 1200,
"remaining": 1187,
"resetAt": "2026-11-12T10:00:00.000Z"
}
}Fields of the answer
| Name | Type | Description |
|---|---|---|
apiVersion | 1 | |
workspace | object | |
workspace.name | string | null | Name of the workspace. |
workspace.plan | string | Plan of the workspace: solo, agency, agency-plus. |
key | object | |
key.name | string | Name given to the key. |
key.prefix | string | First characters of the key. |
key.scopes | read | audit[] | read: read results; audit: also start audits. |
rateLimit | object | |
rateLimit.limit | integer | Calls allowed per clock hour, for the whole workspace. |
rateLimit.remaining | integer | Calls left this hour (this one counted). |
rateLimit.resetAt | string (date-time) | When the next hour starts. |
Errors
- 401 The key is missing, malformed, revoked or unknown (code unauthorized).
- 403 The plan of the workspace does not include the API (code plan_excludes_api). X-RateLimit-Limit and X-RateLimit-Remaining are 0.
- 429 The hourly limit is reached (rate_limited), or too many requests came with a wrong key (too_many_failures). Wait for Retry-After seconds.
- 500 Something went wrong on our side (code internal).
Domains
The domains of the workspace.
GET /api/v1/domains
List the domains
Every domain of the workspace with its status, client, score, grade, open findings by severity, the dates of the last and next audits and a link to its page.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
name | query | string | no | Keep only the domain of this name (example.com). |
Example
curl -H "Authorization: Bearer $PERIMETRY_API_KEY" \
"https://app.perimetry.cloud/api/v1/domains"Answer 200
{
"apiVersion": 1,
"domains": [
{
"id": "3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10",
"name": "example.com",
"status": "verified",
"verified": true,
"client": null,
"score": 72,
"grade": "C",
"counts": {
"critical": 0,
"high": 2,
"medium": 3,
"low": 4,
"info": 6
},
"lastAuditAt": "2026-11-12T03:14:07.000Z",
"nextAuditAt": "2026-11-19T03:14:07.000Z",
"attention": true,
"links": {
"app": "https://app.perimetry.cloud/domains/3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10",
"api": "/api/v1/domains/3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10"
}
}
]
}Fields of the answer
| Name | Type | Description |
|---|---|---|
apiVersion | 1 | |
domains | object[] | |
domains[].id | string (uuid) | Id of the domain. |
domains[].name | string | Name of the domain, such as example.com. |
domains[].status | pending | verified | lost | paused | pending: ownership not proved yet; verified: proved and audited; lost: the proof disappeared; paused: audits stopped. |
domains[].verified | boolean | True when the status is verified. |
domains[].client | string | null | Client the domain belongs to (agency plans), or null. |
domains[].score | integer | null | Score of the last audit, out of 100; null before the first audit. |
domains[].grade | string | null | Grade of the last audit, A to F. |
domains[].counts | { severity: integer } | null | Open findings of the last audit by severity. |
domains[].lastAuditAt | string (date-time) | null | When the last audit started. |
domains[].nextAuditAt | string (date-time) | null | When the next scheduled audit is due. |
domains[].attention | boolean | null | True when the last audit found something new and serious. |
domains[].links | object | |
domains[].links.app | string (uri) | The domain’s page in the application. |
domains[].links.api | string | This domain in the API. |
Errors
- 401 The key is missing, malformed, revoked or unknown (code unauthorized).
- 403 The plan of the workspace does not include the API (code plan_excludes_api). X-RateLimit-Limit and X-RateLimit-Remaining are 0.
- 429 The hourly limit is reached (rate_limited), or too many requests came with a wrong key (too_many_failures). Wait for Retry-After seconds.
- 500 Something went wrong on our side (code internal).
GET /api/v1/domains/{id}
One domain and its last audit
The domain, its last audit with the change of score since the previous one, the numbers of new and fixed findings, and the three findings to fix first.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | Id of the domain, or its name (example.com). |
lang | query | en | fr | de | no | Language of the titles, explanations and fixes of the findings (case does not matter). Any other value is English. (default en) |
Example
curl -H "Authorization: Bearer $PERIMETRY_API_KEY" \
"https://app.perimetry.cloud/api/v1/domains/example.com"Answer 200
{
"apiVersion": 1,
"domain": {
"id": "3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10",
"name": "example.com",
"status": "verified",
"verified": true,
"client": null,
"score": 72,
"grade": "C",
"counts": {
"critical": 0,
"high": 2,
"medium": 3,
"low": 4,
"info": 6
},
"lastAuditAt": "2026-11-12T03:14:07.000Z",
"nextAuditAt": "2026-11-19T03:14:07.000Z",
"attention": true,
"links": {
"app": "https://app.perimetry.cloud/domains/3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10",
"api": "/api/v1/domains/3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10"
}
},
"lastAudit": {
"id": "8b1d5e2f-7c3a-4f6e-b2d9-0a4c6e8f1a23",
"startedAt": "2026-11-12T03:14:07.000Z",
"finishedAt": "2026-11-12T03:16:52.000Z",
"trigger": "schedule",
"scannerVersion": "0.13.1",
"score": 72,
"grade": "C",
"counts": {
"critical": 0,
"high": 2,
"medium": 3,
"low": 4,
"info": 6
},
"attention": true,
"previous": {
"id": "c4e7a1b9-2d5f-4a8c-9e3b-6f1a2d4c8e57",
"startedAt": "2026-11-05T03:14:07.000Z",
"score": 80,
"grade": "B"
},
"scoreDelta": -8,
"newFindings": 2,
"fixedFindings": 1
},
"doFirst": [
{
"fp": "5d0e3b7c1a9f4e26",
"id": "email.dmarc.p_none",
"severity": "high",
"title": "DMARC policy only monitors (p=none)",
"target": "example.com"
}
]
}Fields of the answer
| Name | Type | Description |
|---|---|---|
apiVersion | 1 | |
domain | object | |
domain.id | string (uuid) | Id of the domain. |
domain.name | string | Name of the domain, such as example.com. |
domain.status | pending | verified | lost | paused | pending: ownership not proved yet; verified: proved and audited; lost: the proof disappeared; paused: audits stopped. |
domain.verified | boolean | True when the status is verified. |
domain.client | string | null | Client the domain belongs to (agency plans), or null. |
domain.score | integer | null | Score of the last audit, out of 100; null before the first audit. |
domain.grade | string | null | Grade of the last audit, A to F. |
domain.counts | { severity: integer } | null | Open findings of the last audit by severity. |
domain.lastAuditAt | string (date-time) | null | When the last audit started. |
domain.nextAuditAt | string (date-time) | null | When the next scheduled audit is due. |
domain.attention | boolean | null | True when the last audit found something new and serious. |
domain.links | object | |
domain.links.app | string (uri) | The domain’s page in the application. |
domain.links.api | string | This domain in the API. |
lastAudit | object | null | Null before the first audit. |
lastAudit.id | string (uuid) | |
lastAudit.startedAt | string (date-time) | |
lastAudit.finishedAt | string (date-time) | null | |
lastAudit.trigger | string | schedule or manual. |
lastAudit.scannerVersion | string | Version of the scanner. |
lastAudit.score | integer | |
lastAudit.grade | string | |
lastAudit.counts | { severity: integer } | Open findings by severity (a severity with none may be left out). |
lastAudit.attention | boolean | Something new and serious. |
lastAudit.previous | object | null | |
lastAudit.previous.id | string (uuid) | Id of the audit. |
lastAudit.previous.startedAt | string (date-time) | null | When the audit started. |
lastAudit.previous.score | integer | Score out of 100. |
lastAudit.previous.grade | string | Grade, A to F. |
lastAudit.scoreDelta | integer | null | Change of score since the previous audit. |
lastAudit.newFindings | integer | null | Findings that were not in the previous audit. |
lastAudit.fixedFindings | integer | null | Findings of the previous audit that are gone. |
doFirst | object[] | The (at most) three findings to fix first. |
doFirst[].fp | string | null | Fingerprint: the same check on the same target keeps the same fingerprint from one audit to the next. |
doFirst[].id | string | Id of the check, such as email.dmarc.missing. |
doFirst[].severity | critical | high | medium | low | info | From the worst to the mildest. |
doFirst[].title | string | Title, in the language asked for. |
doFirst[].target | string | Host, address or record the finding is about. |
Errors
- 401 The key is missing, malformed, revoked or unknown (code unauthorized).
- 403 The plan of the workspace does not include the API (code plan_excludes_api). X-RateLimit-Limit and X-RateLimit-Remaining are 0.
- 404 Not in this workspace (code not_found).
- 429 The hourly limit is reached (rate_limited), or too many requests came with a wrong key (too_many_failures). Wait for Retry-After seconds.
- 500 Something went wrong on our side (code internal).
Findings
What the latest audit found.
GET /api/v1/domains/{id}/findings
Open findings of the latest audit
The open findings of the latest audit, worst first, each with its fix, the DNS records ready to copy and the compliance requirements it relates to. Accepted findings on request.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | Id of the domain, or its name (example.com). |
min_severity | query | critical | high | medium | low | info | no | Keep findings of this severity and worse (case does not matter). Any other value answers 400 invalid_parameter. Without it: all. (default info) |
include_accepted | query | 1 | true | no | 1 or true: also return the accepted findings, in "accepted". |
limit | query | integer | no | At most this many findings (and as many accepted ones). (default 500) |
lang | query | en | fr | de | no | Language of the titles, explanations and fixes of the findings (case does not matter). Any other value is English. (default en) |
Example
curl -H "Authorization: Bearer $PERIMETRY_API_KEY" \
"https://app.perimetry.cloud/api/v1/domains/example.com/findings?min_severity=high&lang=fr"Answer 200
{
"apiVersion": 1,
"domain": {
"id": "3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10",
"name": "example.com"
},
"audit": {
"id": "8b1d5e2f-7c3a-4f6e-b2d9-0a4c6e8f1a23",
"startedAt": "2026-11-12T03:14:07.000Z",
"score": 72,
"grade": "C",
"counts": {
"critical": 0,
"high": 2,
"medium": 3,
"low": 4,
"info": 6
}
},
"total": 1,
"findings": [
{
"fp": "5d0e3b7c1a9f4e26",
"id": "email.dmarc.p_none",
"severity": "high",
"category": "email",
"target": "example.com",
"title": "DMARC policy only monitors (p=none)",
"detail": "Receivers are told to deliver mail that fails DMARC anyway: anyone can still send mail in the domain’s name.",
"evidence": "v=DMARC1; p=none; rua=mailto:dmarc@example.com",
"fix": "Move the policy to quarantine, then reject, once the reports show every sender is aligned.",
"references": [
"https://www.rfc-editor.org/rfc/rfc7489"
],
"records": [
{
"type": "TXT",
"name": "_dmarc.example.com",
"value": "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com",
"replaces": "v=DMARC1; p=none; rua=mailto:dmarc@example.com",
"note": "One step stricter: mail that fails DMARC goes to spam."
}
],
"compliance": [
"NIS2 Art. 21(2)(j)",
"ISO 27001 A.8.21"
]
}
]
}Fields of the answer
| Name | Type | Description |
|---|---|---|
apiVersion | 1 | |
domain | object | |
domain.id | string (uuid) | Id of the domain. |
domain.name | string | Name of the domain. |
audit | object | |
audit.id | string (uuid) | |
audit.startedAt | string (date-time) | |
audit.score | integer | |
audit.grade | string | |
audit.counts | { severity: integer } | Open findings by severity (a severity with none may be left out). |
total | integer | Open findings at or above min_severity, before limit. |
findings | object[] | Open findings, worst first. |
findings[].fp | string | null | Fingerprint, stable from one audit to the next. |
findings[].id | string | Id of the check. |
findings[].severity | critical | high | medium | low | info | From the worst to the mildest. |
findings[].category | string | email, web, dns, exposure… |
findings[].target | string | Host, address or record the finding is about. |
findings[].title | string | Title, in the language asked for. |
findings[].detail | string | null | Why it matters. |
findings[].evidence | string | null | What the audit saw. |
findings[].fix | string | null | How to fix it. |
findings[].references | string (uri)[] | Standards and documentation. |
findings[].records | object[] | DNS records ready to copy that fix it, when the audit allows (always empty for an accepted finding). |
findings[].records[].type | string | TXT, CAA, MX… or "file" for a file to serve (MTA-STS policy). |
findings[].records[].name | string | Name of the record (or address of the file). |
findings[].records[].value | string | Value to publish. |
findings[].records[].replaces? | string | null | The current value this one replaces, when there is one. |
findings[].records[].note? | string | What to check before publishing. |
findings[].compliance | string[] | NIS2, Cyber Essentials and ISO 27001 requirements the finding relates to. |
findings[].acceptedReason? | string | null | Accepted findings only: why the risk was accepted. |
findings[].acceptedUntil? | string | null | Accepted findings only: until when, or null for no end date. |
accepted? | object[] | Accepted findings (with include_accepted only), with the reason and end date. |
accepted[].fp | string | null | Fingerprint, stable from one audit to the next. |
accepted[].id | string | Id of the check. |
accepted[].severity | critical | high | medium | low | info | From the worst to the mildest. |
accepted[].category | string | email, web, dns, exposure… |
accepted[].target | string | Host, address or record the finding is about. |
accepted[].title | string | Title, in the language asked for. |
accepted[].detail | string | null | Why it matters. |
accepted[].evidence | string | null | What the audit saw. |
accepted[].fix | string | null | How to fix it. |
accepted[].references | string (uri)[] | Standards and documentation. |
accepted[].records | object[] | DNS records ready to copy that fix it, when the audit allows (always empty for an accepted finding). |
accepted[].records[].type | string | TXT, CAA, MX… or "file" for a file to serve (MTA-STS policy). |
accepted[].records[].name | string | Name of the record (or address of the file). |
accepted[].records[].value | string | Value to publish. |
accepted[].records[].replaces? | string | null | The current value this one replaces, when there is one. |
accepted[].records[].note? | string | What to check before publishing. |
accepted[].compliance | string[] | NIS2, Cyber Essentials and ISO 27001 requirements the finding relates to. |
accepted[].acceptedReason? | string | null | Accepted findings only: why the risk was accepted. |
accepted[].acceptedUntil? | string | null | Accepted findings only: until when, or null for no end date. |
Errors
- 400 A query parameter has a value the API does not accept (code invalid_parameter); the message names the parameter and the values allowed.
- 401 The key is missing, malformed, revoked or unknown (code unauthorized).
- 403 The plan of the workspace does not include the API (code plan_excludes_api). X-RateLimit-Limit and X-RateLimit-Remaining are 0.
- 404 Not in this workspace (not_found), or the domain has no audit yet (no_audit).
- 429 The hourly limit is reached (rate_limited), or too many requests came with a wrong key (too_many_failures). Wait for Retry-After seconds.
- 500 Something went wrong on our side (code internal).
Audits
History, comparison, latest audit, and audits on demand.
GET /api/v1/domains/{id}/compare
What changed since the previous audit
Score from and to, findings added, fixed or changed in severity, and the changes of the infrastructure: hosts, ports, certificates, look-alike domains, cloud services.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | Id of the domain, or its name (example.com). |
lang | query | en | fr | de | no | Language of the titles, explanations and fixes of the findings (case does not matter). Any other value is English. (default en) |
Example
curl -H "Authorization: Bearer $PERIMETRY_API_KEY" \
"https://app.perimetry.cloud/api/v1/domains/example.com/compare"Answer 200
{
"apiVersion": 1,
"domain": {
"id": "3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10",
"name": "example.com"
},
"audit": {
"id": "8b1d5e2f-7c3a-4f6e-b2d9-0a4c6e8f1a23",
"startedAt": "2026-11-12T03:14:07.000Z",
"score": 72,
"grade": "C"
},
"previous": {
"id": "c4e7a1b9-2d5f-4a8c-9e3b-6f1a2d4c8e57",
"startedAt": "2026-11-05T03:14:07.000Z",
"score": 80,
"grade": "B"
},
"comparison": {
"score": {
"from": 80,
"to": 72,
"delta": -8
},
"attention": true,
"worstAdded": "high",
"added": [
{
"fp": "5d0e3b7c1a9f4e26",
"id": "email.dmarc.p_none",
"severity": "high",
"title": "DMARC policy only monitors (p=none)",
"target": "example.com"
}
],
"resolved": [],
"severityChanged": [],
"infra": {
"newHosts": [
"staging.example.com"
],
"goneHosts": [],
"newLookalikes": [],
"newCloud": [],
"ports": [
{
"ip": "192.0.2.10",
"opened": [
8080
],
"closed": []
}
],
"certificates": [],
"newCertificates": [
{
"issuer": "Let's Encrypt",
"names": [
"staging.example.com"
],
"notBefore": "2026-11-10T00:00:00.000Z"
}
]
}
}
}Fields of the answer
| Name | Type | Description |
|---|---|---|
apiVersion | 1 | |
domain | object | |
domain.id | string (uuid) | Id of the domain. |
domain.name | string | Name of the domain. |
audit | object | |
audit.id | string (uuid) | |
audit.startedAt | string (date-time) | |
audit.score | integer | |
audit.grade | string | |
previous | object | null | Null at the first audit. |
previous.id | string (uuid) | null | |
previous.startedAt | string (date-time) | null | |
previous.score | integer | |
previous.grade | string | null | |
comparison | object | null | Null at the first audit. |
comparison.score | object | |
comparison.score.from | integer | |
comparison.score.to | integer | |
comparison.score.delta | integer | |
comparison.attention | boolean | Something new and serious. |
comparison.worstAdded | string | null | Worst severity among the new findings. |
comparison.added | object[] | |
comparison.added[].fp | string | null | Fingerprint: the same check on the same target keeps the same fingerprint from one audit to the next. |
comparison.added[].id | string | Id of the check, such as email.dmarc.missing. |
comparison.added[].severity | critical | high | medium | low | info | From the worst to the mildest. |
comparison.added[].title | string | Title, in the language asked for. |
comparison.added[].target | string | Host, address or record the finding is about. |
comparison.resolved | object[] | |
comparison.resolved[].fp | string | null | Fingerprint: the same check on the same target keeps the same fingerprint from one audit to the next. |
comparison.resolved[].id | string | Id of the check, such as email.dmarc.missing. |
comparison.resolved[].severity | critical | high | medium | low | info | From the worst to the mildest. |
comparison.resolved[].title | string | Title, in the language asked for. |
comparison.resolved[].target | string | Host, address or record the finding is about. |
comparison.severityChanged | object[] | |
comparison.severityChanged[].fp | string | null | Fingerprint: the same check on the same target keeps the same fingerprint from one audit to the next. |
comparison.severityChanged[].id | string | Id of the check, such as email.dmarc.missing. |
comparison.severityChanged[].severity | critical | high | medium | low | info | From the worst to the mildest. |
comparison.severityChanged[].title | string | Title, in the language asked for. |
comparison.severityChanged[].target | string | Host, address or record the finding is about. |
comparison.severityChanged[].from | string | Severity before. |
comparison.severityChanged[].to | string | Severity now. |
comparison.infra | object | |
comparison.infra.newHosts | string[] | |
comparison.infra.goneHosts | string[] | |
comparison.infra.newLookalikes | string[] | |
comparison.infra.newCloud | string[] | |
comparison.infra.ports | object[] | |
comparison.infra.ports[].ip | string | |
comparison.infra.ports[].opened | integer[] | |
comparison.infra.ports[].closed | integer[] | |
comparison.infra.certificates | object[] | Certificates that changed. |
comparison.infra.certificates[].host | string | |
comparison.infra.certificates[].from | string | |
comparison.infra.certificates[].to | string | |
comparison.infra.newCertificates | object[] | New certificates seen in the transparency logs. |
comparison.infra.newCertificates[].issuer | string | |
comparison.infra.newCertificates[].names | string[] | |
comparison.infra.newCertificates[].notBefore | string |
Errors
- 401 The key is missing, malformed, revoked or unknown (code unauthorized).
- 403 The plan of the workspace does not include the API (code plan_excludes_api). X-RateLimit-Limit and X-RateLimit-Remaining are 0.
- 404 Not in this workspace (not_found), or the domain has no audit yet (no_audit).
- 429 The hourly limit is reached (rate_limited), or too many requests came with a wrong key (too_many_failures). Wait for Retry-After seconds.
- 500 Something went wrong on our side (code internal).
GET /api/v1/domains/{id}/audits
History of audits
The audits of the domain, newest first: date, what started it, score, grade, findings by severity.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | Id of the domain, or its name (example.com). |
limit | query | integer | no | At most this many audits. (default 30) |
Example
curl -H "Authorization: Bearer $PERIMETRY_API_KEY" \
"https://app.perimetry.cloud/api/v1/domains/example.com/audits?limit=10"Answer 200
{
"apiVersion": 1,
"domain": {
"id": "3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10",
"name": "example.com"
},
"audits": [
{
"id": "8b1d5e2f-7c3a-4f6e-b2d9-0a4c6e8f1a23",
"startedAt": "2026-11-12T03:14:07.000Z",
"trigger": "schedule",
"score": 72,
"grade": "C",
"counts": {
"high": 2,
"medium": 3,
"low": 4,
"info": 6
},
"attention": true
}
]
}Fields of the answer
| Name | Type | Description |
|---|---|---|
apiVersion | 1 | |
domain | object | |
domain.id | string (uuid) | Id of the domain. |
domain.name | string | Name of the domain. |
audits | object[] | Newest first. |
audits[].id | string (uuid) | |
audits[].startedAt | string (date-time) | |
audits[].trigger | string | |
audits[].score | integer | |
audits[].grade | string | |
audits[].counts | { severity: integer } | Open findings by severity (a severity with none may be left out). |
audits[].attention | boolean |
Errors
- 401 The key is missing, malformed, revoked or unknown (code unauthorized).
- 403 The plan of the workspace does not include the API (code plan_excludes_api). X-RateLimit-Limit and X-RateLimit-Remaining are 0.
- 404 Not in this workspace (code not_found).
- 429 The hourly limit is reached (rate_limited), or too many requests came with a wrong key (too_many_failures). Wait for Retry-After seconds.
- 500 Something went wrong on our side (code internal).
GET /api/v1/domains/{id}/latest
The latest audit in full, or as SARIF
The latest audit as stored, with its findings and the accepted ones, in English; or, with format=sarif, a SARIF 2.1.0 document for code-scanning tools (served as application/sarif+json).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | Id of the domain, or its name (example.com). |
format | query | sarif | no | sarif: answer in SARIF 2.1.0 instead. |
Example
curl -H "Authorization: Bearer $PERIMETRY_API_KEY" \
"https://app.perimetry.cloud/api/v1/domains/example.com/latest?format=sarif"Answer 200
The audit (JSON), or SARIF (application/sarif+json) with format=sarif.
{
"apiVersion": 1,
"domain": "example.com",
"audit": {
"id": "8b1d5e2f-7c3a-4f6e-b2d9-0a4c6e8f1a23",
"domain_id": "3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10",
"started_at": "2026-11-12T03:14:07.000Z",
"finished_at": "2026-11-12T03:16:52.000Z",
"duration_ms": 165000,
"scanner_version": "0.13.1",
"score": 72,
"grade": "C",
"counts": {
"high": 2,
"medium": 3,
"low": 4,
"info": 6
},
"snapshot": {},
"errors": [],
"diff": null,
"attention": true,
"trigger": "schedule",
"created_at": "2026-11-12T03:16:52.000Z"
},
"findings": [
{
"fp": "5d0e3b7c1a9f4e26",
"id": "email.dmarc.p_none",
"severity": "high",
"category": "email",
"target": "example.com",
"title": "DMARC policy only monitors (p=none)",
"fix": "Move the policy to quarantine, then reject."
}
],
"accepted": []
}Fields of the answer
| Name | Type | Description |
|---|---|---|
apiVersion | 1 | |
domain | string | Name of the domain. |
audit | object | The audit as stored (its raw facts left out). Kept as it always was for existing scripts; further fields may appear. |
audit.id | string (uuid) | |
audit.domain_id | string (uuid) | |
audit.started_at | string (date-time) | |
audit.finished_at? | string (date-time) | null | |
audit.duration_ms? | integer | null | |
audit.scanner_version | string | |
audit.score | integer | |
audit.grade | string | |
audit.counts | { severity: integer } | Open findings by severity (a severity with none may be left out). |
audit.errors? | object[] | Checks that could not run. |
audit.diff? | object | null | Changes since the previous audit. |
audit.attention | boolean | |
audit.trigger | string | |
audit.snapshot? | object | Summary kept for comparisons. |
audit.created_at? | string (date-time) | |
findings | object[] | Open findings, worst first, in English. |
findings[].fp? | string | Fingerprint. |
findings[].id | string | Id of the check. |
findings[].severity | critical | high | medium | low | info | From the worst to the mildest. |
findings[].category | string | |
findings[].target | string | |
findings[].title | string | |
findings[].detail? | string | |
findings[].evidence? | string | |
findings[].fix? | string | |
findings[].references? | string[] | |
findings[].acceptedReason? | string | |
findings[].acceptedUntil? | string | null | |
accepted | object[] | Accepted findings. |
accepted[].fp? | string | Fingerprint. |
accepted[].id | string | Id of the check. |
accepted[].severity | critical | high | medium | low | info | From the worst to the mildest. |
accepted[].category | string | |
accepted[].target | string | |
accepted[].title | string | |
accepted[].detail? | string | |
accepted[].evidence? | string | |
accepted[].fix? | string | |
accepted[].references? | string[] | |
accepted[].acceptedReason? | string | |
accepted[].acceptedUntil? | string | null |
Errors
- 401 The key is missing, malformed, revoked or unknown (code unauthorized).
- 403 The plan of the workspace does not include the API (code plan_excludes_api). X-RateLimit-Limit and X-RateLimit-Remaining are 0.
- 404 Not in this workspace (not_found), or the domain has no audit yet (no_audit).
- 429 The hourly limit is reached (rate_limited), or too many requests came with a wrong key (too_many_failures). Wait for Retry-After seconds.
- 500 Something went wrong on our side (code internal).
POST /api/v1/domains/{id}/audit
Start an audit
Starts an audit of a proved domain; the key needs the audit scope. Answers 202 with a run to follow at /api/v1/runs/{id}. While an audit of the domain is already waiting or running, the new run follows that one (alreadyQueued). Same limit as the Audit now button (one an hour per domain on Solo), and through the API one every 15 minutes per domain. No request body.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | Id of the domain, or its name (example.com). |
Example
curl -X POST -H "Authorization: Bearer $PERIMETRY_API_KEY" \
"https://app.perimetry.cloud/api/v1/domains/example.com/audit"Answer 202
Accepted: the audit is queued (or already was).
{
"apiVersion": 1,
"queued": true,
"alreadyQueued": false,
"run": {
"id": "6f1c9d2e-4b7a-4c3e-8f5d-1a2b3c4d5e6f",
"status": "queued",
"domain": {
"id": "3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10",
"name": "example.com"
},
"requestedAt": "2026-11-12T09:00:02.000Z",
"progress": null,
"audit": null,
"previous": {
"id": "c4e7a1b9-2d5f-4a8c-9e3b-6f1a2d4c8e57",
"startedAt": "2026-11-05T03:14:07.000Z",
"score": 72,
"grade": "C"
},
"scoreDelta": null,
"links": {
"self": "/api/v1/runs/6f1c9d2e-4b7a-4c3e-8f5d-1a2b3c4d5e6f",
"app": "https://app.perimetry.cloud/domains/3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10"
}
}
}Fields of the answer
| Name | Type | Description |
|---|---|---|
apiVersion | 1 | |
queued | true | |
alreadyQueued | boolean | True when an audit of the domain was already waiting or running: this run follows it. |
run | object | |
run.id | string (uuid) | Id of the run, to follow at /api/v1/runs/{id}. |
run.status | queued | running | done | failed | queued: waiting for a scanner; running: under way (see progress); done: the audit is in; failed: the audit did not finish. |
run.domain | object | |
run.domain.id | string (uuid) | Id of the domain. |
run.domain.name | string | Name of the domain. |
run.requestedAt | string (date-time) | When the audit was asked for. |
run.progress | object | null | While running only. |
run.progress.percent | number | null | Percentage done. |
run.progress.step | string | null | Current step. |
run.error? | string | Failed runs only: what happened. |
run.audit | object | null | The audit, once done. |
run.audit.id | string (uuid) | |
run.audit.startedAt | string (date-time) | |
run.audit.finishedAt | string (date-time) | null | |
run.audit.trigger | string | manual for an audit started on demand. |
run.audit.score | integer | |
run.audit.grade | string | |
run.audit.counts | { severity: integer } | Open findings by severity (a severity with none may be left out). |
run.audit.newFindings | integer | null | Findings that were not in the previous audit. |
run.audit.fixedFindings | integer | null | Findings of the previous audit that are gone. |
run.audit.worstAdded | string | null | Worst severity among the new findings. |
run.previous | object | null | The audit before this one. |
run.previous.id | string (uuid) | Id of the audit. |
run.previous.startedAt | string (date-time) | null | When the audit started. |
run.previous.score | integer | Score out of 100. |
run.previous.grade | string | Grade, A to F. |
run.scoreDelta | integer | null | Change of score since the previous audit. |
run.links | object | |
run.links.self | string | This run in the API. |
run.links.app | string (uri) | The domain’s page in the application. |
Errors
- 401 The key is missing, malformed, revoked or unknown (code unauthorized).
- 403 The plan does not include the API (plan_excludes_api), or the key cannot start audits (insufficient_scope).
- 404 Not in this workspace (code not_found).
- 409 The domain is not proved (code not_verified).
- 429 The hourly limit is reached (rate_limited), too many requests came with a wrong key (too_many_failures), or an audit on demand of this domain ran too recently (audit_too_soon). Wait for Retry-After seconds.
- 500 Something went wrong on our side (code internal).
- 503 The audit could not be queued; try again (code unavailable).
- 507 The workspace has used the storage of its plan (storage_quota), or the service is full for the moment (storage_full): no audit on demand until there is room. Delete old data or move to a larger plan; everything already stored stays readable.
GET /api/v1/runs/{id}
Where an audit started through the API stands
queued, running (with its progress), done (new score, previous score, change) or failed. Poll every 10 to 30 seconds; an audit usually takes a few minutes.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Id of the run, from POST /api/v1/domains/{id}/audit. |
Example
curl -H "Authorization: Bearer $PERIMETRY_API_KEY" \
"https://app.perimetry.cloud/api/v1/runs/6f1c9d2e-4b7a-4c3e-8f5d-1a2b3c4d5e6f"Answer 200
{
"apiVersion": 1,
"run": {
"id": "6f1c9d2e-4b7a-4c3e-8f5d-1a2b3c4d5e6f",
"status": "done",
"domain": {
"id": "3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10",
"name": "example.com"
},
"requestedAt": "2026-11-12T09:00:02.000Z",
"progress": null,
"audit": {
"id": "8b1d5e2f-7c3a-4f6e-b2d9-0a4c6e8f1a23",
"startedAt": "2026-11-12T09:00:05.000Z",
"finishedAt": "2026-11-12T09:02:41.000Z",
"trigger": "manual",
"score": 78,
"grade": "C",
"counts": {
"critical": 0,
"high": 1,
"medium": 3,
"low": 4,
"info": 6
},
"newFindings": 0,
"fixedFindings": 1,
"worstAdded": null
},
"previous": {
"id": "c4e7a1b9-2d5f-4a8c-9e3b-6f1a2d4c8e57",
"startedAt": "2026-11-05T03:14:07.000Z",
"score": 72,
"grade": "C"
},
"scoreDelta": 6,
"links": {
"self": "/api/v1/runs/6f1c9d2e-4b7a-4c3e-8f5d-1a2b3c4d5e6f",
"app": "https://app.perimetry.cloud/domains/3f9c2a64-5b1e-4c0a-9a57-2f6d8e1b4c10"
}
}
}Fields of the answer
| Name | Type | Description |
|---|---|---|
apiVersion | 1 | |
run | object | |
run.id | string (uuid) | Id of the run, to follow at /api/v1/runs/{id}. |
run.status | queued | running | done | failed | queued: waiting for a scanner; running: under way (see progress); done: the audit is in; failed: the audit did not finish. |
run.domain | object | |
run.domain.id | string (uuid) | Id of the domain. |
run.domain.name | string | Name of the domain. |
run.requestedAt | string (date-time) | When the audit was asked for. |
run.progress | object | null | While running only. |
run.progress.percent | number | null | Percentage done. |
run.progress.step | string | null | Current step. |
run.error? | string | Failed runs only: what happened. |
run.audit | object | null | The audit, once done. |
run.audit.id | string (uuid) | |
run.audit.startedAt | string (date-time) | |
run.audit.finishedAt | string (date-time) | null | |
run.audit.trigger | string | manual for an audit started on demand. |
run.audit.score | integer | |
run.audit.grade | string | |
run.audit.counts | { severity: integer } | Open findings by severity (a severity with none may be left out). |
run.audit.newFindings | integer | null | Findings that were not in the previous audit. |
run.audit.fixedFindings | integer | null | Findings of the previous audit that are gone. |
run.audit.worstAdded | string | null | Worst severity among the new findings. |
run.previous | object | null | The audit before this one. |
run.previous.id | string (uuid) | Id of the audit. |
run.previous.startedAt | string (date-time) | null | When the audit started. |
run.previous.score | integer | Score out of 100. |
run.previous.grade | string | Grade, A to F. |
run.scoreDelta | integer | null | Change of score since the previous audit. |
run.links | object | |
run.links.self | string | This run in the API. |
run.links.app | string (uri) | The domain’s page in the application. |
Errors
- 401 The key is missing, malformed, revoked or unknown (code unauthorized).
- 403 The plan of the workspace does not include the API (code plan_excludes_api). X-RateLimit-Limit and X-RateLimit-Remaining are 0.
- 404 Not in this workspace (code not_found).
- 429 The hourly limit is reached (rate_limited), or too many requests came with a wrong key (too_many_failures). Wait for Retry-After seconds.
- 500 Something went wrong on our side (code internal).
Specification
This document.
GET /api/v1/openapi.json
This specification
The OpenAPI 3.1 description of the API. Public: no key, not counted against the hourly limit.
No key needed.
Example
curl \
"https://app.perimetry.cloud/api/v1/openapi.json"Answer 200
The OpenAPI document.