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.

Version 1.0.0 · OpenAPI 3.1 specification (JSON)

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/me

Domains 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:

PlanCalls per hour
Solo300
Agency1,200
Agency Plus3,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"}
StatusCodeMeaning
400invalid_parameterA parameter has a value the API does not accept; the message names the values allowed.
401unauthorizedThe key is missing, malformed, revoked or unknown.
403plan_excludes_apiThe plan of the workspace does not include the API.
403insufficient_scopeThe key cannot start audits: create one with the audit scope.
404not_foundNot in this workspace (a domain of another workspace answers the same).
404no_auditThe domain has no audit yet.
409not_verifiedAn audit was asked for a domain that is not proved.
429rate_limitedThe hourly limit is reached: wait for Retry-After seconds.
429too_many_failuresToo many requests with a wrong key from your network: fifteen minutes’ wait.
429audit_too_soonAn audit on demand of this domain ran too recently: wait for Retry-After seconds.
500internalSomething went wrong on our side.
503unavailableThe audit could not be queued: try again.
507storage_quotaThe workspace has used the storage of its plan: delete old data or move to a larger plan.
507storage_fullOur 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
NameTypeDescription
apiVersion1
workspaceobject
workspace.namestring | nullName of the workspace.
workspace.planstringPlan of the workspace: solo, agency, agency-plus.
keyobject
key.namestringName given to the key.
key.prefixstringFirst characters of the key.
key.scopesread | audit[]read: read results; audit: also start audits.
rateLimitobject
rateLimit.limitintegerCalls allowed per clock hour, for the whole workspace.
rateLimit.remainingintegerCalls left this hour (this one counted).
rateLimit.resetAtstring (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

NameInTypeRequiredDescription
namequerystringnoKeep 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
NameTypeDescription
apiVersion1
domainsobject[]
domains[].idstring (uuid)Id of the domain.
domains[].namestringName of the domain, such as example.com.
domains[].statuspending | verified | lost | pausedpending: ownership not proved yet; verified: proved and audited; lost: the proof disappeared; paused: audits stopped.
domains[].verifiedbooleanTrue when the status is verified.
domains[].clientstring | nullClient the domain belongs to (agency plans), or null.
domains[].scoreinteger | nullScore of the last audit, out of 100; null before the first audit.
domains[].gradestring | nullGrade of the last audit, A to F.
domains[].counts{ severity: integer } | nullOpen findings of the last audit by severity.
domains[].lastAuditAtstring (date-time) | nullWhen the last audit started.
domains[].nextAuditAtstring (date-time) | nullWhen the next scheduled audit is due.
domains[].attentionboolean | nullTrue when the last audit found something new and serious.
domains[].linksobject
domains[].links.appstring (uri)The domain’s page in the application.
domains[].links.apistringThis 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

NameInTypeRequiredDescription
idpathstringyesId of the domain, or its name (example.com).
langqueryen | fr | denoLanguage 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
NameTypeDescription
apiVersion1
domainobject
domain.idstring (uuid)Id of the domain.
domain.namestringName of the domain, such as example.com.
domain.statuspending | verified | lost | pausedpending: ownership not proved yet; verified: proved and audited; lost: the proof disappeared; paused: audits stopped.
domain.verifiedbooleanTrue when the status is verified.
domain.clientstring | nullClient the domain belongs to (agency plans), or null.
domain.scoreinteger | nullScore of the last audit, out of 100; null before the first audit.
domain.gradestring | nullGrade of the last audit, A to F.
domain.counts{ severity: integer } | nullOpen findings of the last audit by severity.
domain.lastAuditAtstring (date-time) | nullWhen the last audit started.
domain.nextAuditAtstring (date-time) | nullWhen the next scheduled audit is due.
domain.attentionboolean | nullTrue when the last audit found something new and serious.
domain.linksobject
domain.links.appstring (uri)The domain’s page in the application.
domain.links.apistringThis domain in the API.
lastAuditobject | nullNull before the first audit.
lastAudit.idstring (uuid)
lastAudit.startedAtstring (date-time)
lastAudit.finishedAtstring (date-time) | null
lastAudit.triggerstringschedule or manual.
lastAudit.scannerVersionstringVersion of the scanner.
lastAudit.scoreinteger
lastAudit.gradestring
lastAudit.counts{ severity: integer }Open findings by severity (a severity with none may be left out).
lastAudit.attentionbooleanSomething new and serious.
lastAudit.previousobject | null
lastAudit.previous.idstring (uuid)Id of the audit.
lastAudit.previous.startedAtstring (date-time) | nullWhen the audit started.
lastAudit.previous.scoreintegerScore out of 100.
lastAudit.previous.gradestringGrade, A to F.
lastAudit.scoreDeltainteger | nullChange of score since the previous audit.
lastAudit.newFindingsinteger | nullFindings that were not in the previous audit.
lastAudit.fixedFindingsinteger | nullFindings of the previous audit that are gone.
doFirstobject[]The (at most) three findings to fix first.
doFirst[].fpstring | nullFingerprint: the same check on the same target keeps the same fingerprint from one audit to the next.
doFirst[].idstringId of the check, such as email.dmarc.missing.
doFirst[].severitycritical | high | medium | low | infoFrom the worst to the mildest.
doFirst[].titlestringTitle, in the language asked for.
doFirst[].targetstringHost, 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

NameInTypeRequiredDescription
idpathstringyesId of the domain, or its name (example.com).
min_severityquerycritical | high | medium | low | infonoKeep findings of this severity and worse (case does not matter). Any other value answers 400 invalid_parameter. Without it: all. (default info)
include_acceptedquery1 | trueno1 or true: also return the accepted findings, in "accepted".
limitqueryintegernoAt most this many findings (and as many accepted ones). (default 500)
langqueryen | fr | denoLanguage 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
NameTypeDescription
apiVersion1
domainobject
domain.idstring (uuid)Id of the domain.
domain.namestringName of the domain.
auditobject
audit.idstring (uuid)
audit.startedAtstring (date-time)
audit.scoreinteger
audit.gradestring
audit.counts{ severity: integer }Open findings by severity (a severity with none may be left out).
totalintegerOpen findings at or above min_severity, before limit.
findingsobject[]Open findings, worst first.
findings[].fpstring | nullFingerprint, stable from one audit to the next.
findings[].idstringId of the check.
findings[].severitycritical | high | medium | low | infoFrom the worst to the mildest.
findings[].categorystringemail, web, dns, exposure…
findings[].targetstringHost, address or record the finding is about.
findings[].titlestringTitle, in the language asked for.
findings[].detailstring | nullWhy it matters.
findings[].evidencestring | nullWhat the audit saw.
findings[].fixstring | nullHow to fix it.
findings[].referencesstring (uri)[]Standards and documentation.
findings[].recordsobject[]DNS records ready to copy that fix it, when the audit allows (always empty for an accepted finding).
findings[].records[].typestringTXT, CAA, MX… or "file" for a file to serve (MTA-STS policy).
findings[].records[].namestringName of the record (or address of the file).
findings[].records[].valuestringValue to publish.
findings[].records[].replaces?string | nullThe current value this one replaces, when there is one.
findings[].records[].note?stringWhat to check before publishing.
findings[].compliancestring[]NIS2, Cyber Essentials and ISO 27001 requirements the finding relates to.
findings[].acceptedReason?string | nullAccepted findings only: why the risk was accepted.
findings[].acceptedUntil?string | nullAccepted 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[].fpstring | nullFingerprint, stable from one audit to the next.
accepted[].idstringId of the check.
accepted[].severitycritical | high | medium | low | infoFrom the worst to the mildest.
accepted[].categorystringemail, web, dns, exposure…
accepted[].targetstringHost, address or record the finding is about.
accepted[].titlestringTitle, in the language asked for.
accepted[].detailstring | nullWhy it matters.
accepted[].evidencestring | nullWhat the audit saw.
accepted[].fixstring | nullHow to fix it.
accepted[].referencesstring (uri)[]Standards and documentation.
accepted[].recordsobject[]DNS records ready to copy that fix it, when the audit allows (always empty for an accepted finding).
accepted[].records[].typestringTXT, CAA, MX… or "file" for a file to serve (MTA-STS policy).
accepted[].records[].namestringName of the record (or address of the file).
accepted[].records[].valuestringValue to publish.
accepted[].records[].replaces?string | nullThe current value this one replaces, when there is one.
accepted[].records[].note?stringWhat to check before publishing.
accepted[].compliancestring[]NIS2, Cyber Essentials and ISO 27001 requirements the finding relates to.
accepted[].acceptedReason?string | nullAccepted findings only: why the risk was accepted.
accepted[].acceptedUntil?string | nullAccepted 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

NameInTypeRequiredDescription
idpathstringyesId of the domain, or its name (example.com).
langqueryen | fr | denoLanguage 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
NameTypeDescription
apiVersion1
domainobject
domain.idstring (uuid)Id of the domain.
domain.namestringName of the domain.
auditobject
audit.idstring (uuid)
audit.startedAtstring (date-time)
audit.scoreinteger
audit.gradestring
previousobject | nullNull at the first audit.
previous.idstring (uuid) | null
previous.startedAtstring (date-time) | null
previous.scoreinteger
previous.gradestring | null
comparisonobject | nullNull at the first audit.
comparison.scoreobject
comparison.score.frominteger
comparison.score.tointeger
comparison.score.deltainteger
comparison.attentionbooleanSomething new and serious.
comparison.worstAddedstring | nullWorst severity among the new findings.
comparison.addedobject[]
comparison.added[].fpstring | nullFingerprint: the same check on the same target keeps the same fingerprint from one audit to the next.
comparison.added[].idstringId of the check, such as email.dmarc.missing.
comparison.added[].severitycritical | high | medium | low | infoFrom the worst to the mildest.
comparison.added[].titlestringTitle, in the language asked for.
comparison.added[].targetstringHost, address or record the finding is about.
comparison.resolvedobject[]
comparison.resolved[].fpstring | nullFingerprint: the same check on the same target keeps the same fingerprint from one audit to the next.
comparison.resolved[].idstringId of the check, such as email.dmarc.missing.
comparison.resolved[].severitycritical | high | medium | low | infoFrom the worst to the mildest.
comparison.resolved[].titlestringTitle, in the language asked for.
comparison.resolved[].targetstringHost, address or record the finding is about.
comparison.severityChangedobject[]
comparison.severityChanged[].fpstring | nullFingerprint: the same check on the same target keeps the same fingerprint from one audit to the next.
comparison.severityChanged[].idstringId of the check, such as email.dmarc.missing.
comparison.severityChanged[].severitycritical | high | medium | low | infoFrom the worst to the mildest.
comparison.severityChanged[].titlestringTitle, in the language asked for.
comparison.severityChanged[].targetstringHost, address or record the finding is about.
comparison.severityChanged[].fromstringSeverity before.
comparison.severityChanged[].tostringSeverity now.
comparison.infraobject
comparison.infra.newHostsstring[]
comparison.infra.goneHostsstring[]
comparison.infra.newLookalikesstring[]
comparison.infra.newCloudstring[]
comparison.infra.portsobject[]
comparison.infra.ports[].ipstring
comparison.infra.ports[].openedinteger[]
comparison.infra.ports[].closedinteger[]
comparison.infra.certificatesobject[]Certificates that changed.
comparison.infra.certificates[].hoststring
comparison.infra.certificates[].fromstring
comparison.infra.certificates[].tostring
comparison.infra.newCertificatesobject[]New certificates seen in the transparency logs.
comparison.infra.newCertificates[].issuerstring
comparison.infra.newCertificates[].namesstring[]
comparison.infra.newCertificates[].notBeforestring

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

NameInTypeRequiredDescription
idpathstringyesId of the domain, or its name (example.com).
limitqueryintegernoAt 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
NameTypeDescription
apiVersion1
domainobject
domain.idstring (uuid)Id of the domain.
domain.namestringName of the domain.
auditsobject[]Newest first.
audits[].idstring (uuid)
audits[].startedAtstring (date-time)
audits[].triggerstring
audits[].scoreinteger
audits[].gradestring
audits[].counts{ severity: integer }Open findings by severity (a severity with none may be left out).
audits[].attentionboolean

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

NameInTypeRequiredDescription
idpathstringyesId of the domain, or its name (example.com).
formatquerysarifnosarif: 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
NameTypeDescription
apiVersion1
domainstringName of the domain.
auditobjectThe audit as stored (its raw facts left out). Kept as it always was for existing scripts; further fields may appear.
audit.idstring (uuid)
audit.domain_idstring (uuid)
audit.started_atstring (date-time)
audit.finished_at?string (date-time) | null
audit.duration_ms?integer | null
audit.scanner_versionstring
audit.scoreinteger
audit.gradestring
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 | nullChanges since the previous audit.
audit.attentionboolean
audit.triggerstring
audit.snapshot?objectSummary kept for comparisons.
audit.created_at?string (date-time)
findingsobject[]Open findings, worst first, in English.
findings[].fp?stringFingerprint.
findings[].idstringId of the check.
findings[].severitycritical | high | medium | low | infoFrom the worst to the mildest.
findings[].categorystring
findings[].targetstring
findings[].titlestring
findings[].detail?string
findings[].evidence?string
findings[].fix?string
findings[].references?string[]
findings[].acceptedReason?string
findings[].acceptedUntil?string | null
acceptedobject[]Accepted findings.
accepted[].fp?stringFingerprint.
accepted[].idstringId of the check.
accepted[].severitycritical | high | medium | low | infoFrom the worst to the mildest.
accepted[].categorystring
accepted[].targetstring
accepted[].titlestring
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

NameInTypeRequiredDescription
idpathstringyesId 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
NameTypeDescription
apiVersion1
queuedtrue
alreadyQueuedbooleanTrue when an audit of the domain was already waiting or running: this run follows it.
runobject
run.idstring (uuid)Id of the run, to follow at /api/v1/runs/{id}.
run.statusqueued | running | done | failedqueued: waiting for a scanner; running: under way (see progress); done: the audit is in; failed: the audit did not finish.
run.domainobject
run.domain.idstring (uuid)Id of the domain.
run.domain.namestringName of the domain.
run.requestedAtstring (date-time)When the audit was asked for.
run.progressobject | nullWhile running only.
run.progress.percentnumber | nullPercentage done.
run.progress.stepstring | nullCurrent step.
run.error?stringFailed runs only: what happened.
run.auditobject | nullThe audit, once done.
run.audit.idstring (uuid)
run.audit.startedAtstring (date-time)
run.audit.finishedAtstring (date-time) | null
run.audit.triggerstringmanual for an audit started on demand.
run.audit.scoreinteger
run.audit.gradestring
run.audit.counts{ severity: integer }Open findings by severity (a severity with none may be left out).
run.audit.newFindingsinteger | nullFindings that were not in the previous audit.
run.audit.fixedFindingsinteger | nullFindings of the previous audit that are gone.
run.audit.worstAddedstring | nullWorst severity among the new findings.
run.previousobject | nullThe audit before this one.
run.previous.idstring (uuid)Id of the audit.
run.previous.startedAtstring (date-time) | nullWhen the audit started.
run.previous.scoreintegerScore out of 100.
run.previous.gradestringGrade, A to F.
run.scoreDeltainteger | nullChange of score since the previous audit.
run.linksobject
run.links.selfstringThis run in the API.
run.links.appstring (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

NameInTypeRequiredDescription
idpathstring (uuid)yesId 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
NameTypeDescription
apiVersion1
runobject
run.idstring (uuid)Id of the run, to follow at /api/v1/runs/{id}.
run.statusqueued | running | done | failedqueued: waiting for a scanner; running: under way (see progress); done: the audit is in; failed: the audit did not finish.
run.domainobject
run.domain.idstring (uuid)Id of the domain.
run.domain.namestringName of the domain.
run.requestedAtstring (date-time)When the audit was asked for.
run.progressobject | nullWhile running only.
run.progress.percentnumber | nullPercentage done.
run.progress.stepstring | nullCurrent step.
run.error?stringFailed runs only: what happened.
run.auditobject | nullThe audit, once done.
run.audit.idstring (uuid)
run.audit.startedAtstring (date-time)
run.audit.finishedAtstring (date-time) | null
run.audit.triggerstringmanual for an audit started on demand.
run.audit.scoreinteger
run.audit.gradestring
run.audit.counts{ severity: integer }Open findings by severity (a severity with none may be left out).
run.audit.newFindingsinteger | nullFindings that were not in the previous audit.
run.audit.fixedFindingsinteger | nullFindings of the previous audit that are gone.
run.audit.worstAddedstring | nullWorst severity among the new findings.
run.previousobject | nullThe audit before this one.
run.previous.idstring (uuid)Id of the audit.
run.previous.startedAtstring (date-time) | nullWhen the audit started.
run.previous.scoreintegerScore out of 100.
run.previous.gradestringGrade, A to F.
run.scoreDeltainteger | nullChange of score since the previous audit.
run.linksobject
run.links.selfstringThis run in the API.
run.links.appstring (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.