ISAO — International Standards Accreditation Organization home pageVerify a certificate

Menu

Verification API

Tender portals, banks and other systems can check a certificate against the ISAO public register and get the same answer as the record page, as JSON.

Request

GET https://isao.org.uk/api/v1/verify/{code}

{code} is the verification code (XXXX-XXXX-XXXX, any case, hyphens optional) or the certificate number. Certificate numbers often contain /: encode it as %2F, or write the number with hyphens (ISAO-BAR-CB-YYYY-NNNN finds ISAO-BAR/CB/YYYY/NNNN). Add a revision suffix to ask for one revision (…%20R1); without it you get the current revision. No authentication or API key is needed. The API does not search by name.

curl -s https://isao.org.uk/api/v1/verify/XXXX-XXXX-XXXX
curl -s https://isao.org.uk/api/v1/verify/ISAO-BAR%2FCB%2FYYYY%2FNNNN

Response

JSON in UTF-8. The example below uses placeholder values. Fields may be added over time; existing fields keep their names and meaning.

{
  "status": "valid",
  "statusLine": "Valid certificate — valid until 1 June 2029",
  "publicCode": "XXXX-XXXX-XXXX",
  "number": "ISAO-QMS-YY-NNNNN",
  "revision": 0,
  "type": "REGISTRATION",
  "typeLabel": "Certificate of registration",
  "holder": {
    "name": "Example Components Ltd",
    "city": "Leeds",
    "country": "United Kingdom",
    "countryCode": "GB",
    "grade": null
  },
  "standards": [
    {
      "code": "ISO 9001:2015",
      "title": "Quality management systems"
    }
  ],
  "scope": "Design and manufacture of machined components.",
  "sectorCodes": [
    "17"
  ],
  "sites": [
    {
      "name": "Head office and works",
      "city": "Leeds",
      "country": "United Kingdom"
    }
  ],
  "dates": {
    "issue": "2026-06-02",
    "originalIssue": "2026-06-02",
    "validFrom": "2026-06-02",
    "expiry": "2029-06-01"
  },
  "mainCertificate": null,
  "statusDetail": {
    "since": null,
    "until": "2029-06-01",
    "reasonCategory": null,
    "reason": null
  },
  "replacedBy": null,
  "surveillance": "on_schedule",
  "issuer": {
    "kind": "isao",
    "name": "International Standards Accreditation Organization",
    "nameOnCertificate": null,
    "accreditationNumber": null,
    "accreditationStatus": null,
    "coveredOnIssueDate": true
  },
  "accreditation": null,
  "signature": {
    "state": "verified",
    "keyId": "isao-2026-01",
    "keyProblem": null,
    "value": "(86 base64url characters)",
    "contentHash": "(64 hexadecimal characters)",
    "shortForm": "XXXX XXXX XXXX XXXX",
    "payload": {
      "type": "REGISTRATION",
      "number": "ISAO-QMS-YY-NNNNN",
      "revision": 0,
      "holderName": "Example Components Ltd",
      "standards": [
        "ISO 9001:2015"
      ],
      "scopeText": "Design and manufacture of machined components.",
      "issueDate": "2026-06-02",
      "expiryDate": "2029-06-01",
      "issuer": "International Standards Accreditation Organization",
      "publicCode": "XXXX-XXXX-XXXX",
      "payloadVersion": 2,
      "validFrom": "2026-06-02",
      "auditReportNo": "AR/2026/0001",
      "placeOfIssue": "London",
      "issuingOffice": null,
      "mainCertificateNumber": null,
      "recognitionMarks": []
    },
    "payloadVersion": 2,
    "keysUrl": "https://isao.org.uk/.well-known/isao-signing-keys.json"
  },
  "notices": [],
  "verifyUrl": "https://isao.org.uk/v/XXXX-XXXX-XXXX",
  "checkedAt": "2026-09-29T10:15:00.000Z"
}
Response fields
FieldMeaning
statusvalid, not_yet_valid, suspended, withdrawn, expired, superseded or not_covered (see Statuses).
statusLineThe status as one plain-English line, as shown on the record page.
publicCodeThe verification code printed on the certificate.
number, revisionCertificate number as printed (with R1, R2 … for revised certificates) and the revision as a number.
type, typeLabelACCREDITATION, REGISTRATION, AUDITOR, TRAINING or CUSTOM, and its label.
holderThe holder as certified: name, city and country (code and name) as printed on the certificate, not later edits to the register. Registered auditors: name and grade only.
standardsStandards by number, with the titles printed on the certificate, in the order printed.
scope, sectorCodes, sitesScope of certification, IAF sector codes (1 to 39, numeric order), and the certified sites with city and country, as printed.
datesissue, originalIssue (initial certification), validFrom and expiry as YYYY-MM-DD (UTC). The expiry day is included in the validity.
mainCertificateFor a site or sub-certificate: the main certificate it is valid together with (number, code, record address), with its status today and the date behind it; otherwise null. A sub-certificate never reads better than its main certificate.
statusDetailsince (date of suspension, withdrawal or expiry), until (valid-until or planned end of a suspension), reasonCategory and reason. A certificate that is not yet valid starts on dates.validFrom.
replacedByFor superseded certificates: the current version's code and record address.
surveillanceon_schedule, overdue, complete or none.
issuerWho issued the certificate. kind isao: ISAO issued it itself (every new certificate of registration); accreditationNumber and accreditationStatus are null and coveredOnIssueDate is true, as no accreditation chain applies. kind body: a certification body issued it; its current name, the name printed on the certificate when different (nameOnCertificate), its ISAO accreditation number and status today (past its expiry date it reads expired), and whether that accreditation covered the certificate on every day from its first day of validity to its issue date (coveredOnIssueDate).
accreditationCertificates of accreditation only: the accreditation the certificate records (number), its status today (active, suspended, withdrawn or expired; past its expiry date it reads expired) and since (the date of that suspension, withdrawal or expiry); otherwise null. The certificate keeps its own status; notices says when that accreditation is suspended, withdrawn or expired.
signaturestate (verified, mismatch, unsigned, unknown_key), keyId, keyProblem (unpublished, retired or revoked, with unknown_key), the Ed25519 signature (value), contentHash, shortForm as printed, the signed payload with its payloadVersion (1, 2 or 3) and keysUrl.
noticesNotices about the certificate, for example a suspension of the issuing body.
verifyUrl, checkedAtThe record page for people, and the time of the check (UTC).

Statuses

Certificate statuses
statusMeaning
validValid today. For a certificate issued by a certification body, that body's accreditation also covered the standard on every day from the first day of validity to the issue date, and has not been withdrawn.
not_yet_validIssued, but its validity has not started yet: it is valid from dates.validFrom (statusLine gives the date). Do not rely on it before then.
suspendedTemporarily not valid. statusDetail gives the date, reason category and any planned end.
withdrawnPermanently not valid since statusDetail.since.
expiredThe validity period has ended.
supersededReplaced by a newer version: see replacedBy.
not_coveredNot valid under ISAO accreditation: on some day from the first day of validity to the issue date the issuing body was not accredited for the standard (or its accreditation was suspended, withdrawn or expired that day), or its accreditation has been withdrawn since. notices gives the day and the reason.
not_foundHTTP 404. No matching record was found. Check the code or number and try again.

Only valid means the certificate can be relied on today. Read notices as well: when an issuing body is suspended, or its accreditation has passed its expiry date, its certificates keep their status and carry a notice. So does a certificate of accreditation whose accreditation is suspended, withdrawn or expired (accreditation gives that state). A site or sub-certificate takes its main certificate's status when that is worse.

HTTP status codes

HTTP status codes
CodeMeaning
200A certificate was found. The body is the record above, whatever its status.
400The path is neither a verification code nor a certificate number. The API does not search by name.
404No certificate matches: {"status": "not_found", "statusLine": …, "checkedAt": …}.
429The lookup allowance for your IP address is used up. Wait for Retry-After seconds.
503The register cannot be checked at the moment. Try again later.

Errors have the form {"error": "invalid_query" | "rate_limited" | "unavailable", "message": "…"}.

Rate limits, CORS and caching

  • 30 lookups per 10 minutes for each IP address, shared with the verification pages of this site. IPv6 addresses count per /64 network, so addresses in one /64 share an allowance. The allowance refills evenly over that time.
  • Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (seconds until the allowance is full again) and RateLimit-Policy (30;w=600). When it is used up, the API answers 429 with Retry-After in seconds.
  • CORS is open (Access-Control-Allow-Origin: *), so browsers can call the API from any site. No cookies or credentials are used.
  • Responses are never cached (Cache-Control: no-store): a suspension or withdrawal shows at once. Do not cache results for longer than you need them.
  • If your integration needs a higher limit, contact us.

Checking the digital signature yourself

ISAO signs every issued certificate with Ed25519. The API already re-verifies the signature on each call (signature.state), but you can check it independently:

  1. Fetch the public keys from /.well-known/isao-signing-keys.json and pick the key whose id equals signature.keyId. Only the keys listed there verify ISAO signatures. A retired key verifies only signatures made before its retirement; a revoked key verifies none (signature.keyProblem then says retired or revoked).
  2. Serialise signature.payload as canonical JSON (RFC 8785: keys sorted, no whitespace) and encode it as UTF-8.
  3. Verify signature.value (base64url, 64 bytes) over those bytes with the key (base64url, 32 bytes). The SHA-256 of the same bytes, in hex, equals signature.contentHash.
  4. Compare the payload with the certificate you were given: holder name, standards, scope, dates, issuer and verification code. Version 2 payloads ("payloadVersion": 2) also cover the valid-from date, audit report number, place of issue, issuing office, main certificate number and recognition marks. Version 3 payloads ("payloadVersion": 3) also cover the initial certification date, the IAF sector codes, the holder's trading names and address, the sites, the issuer name as printed, the issuing body's accreditation number as recorded and, on a certificate of accreditation, the schedule; documentDigest covers the rest of the printed document, so any change to it reads as a mismatch.

signature.payload is given in the version the certificate was signed with: signature.payloadVersion is 3 for certificates signed since the certificate-integrity release (autumn 2026), 2 for those signed from September 2026 until then, and 1 for earlier ones, whose payload has no payloadVersion field. In version 3, issuer is the name printed on the certificate and signedAt the signing time. Every version is described at /.well-known/isao-signing-keys.json.

import { createPublicKey, verify } from "node:crypto";

const base = "https://isao.org.uk";
const record = await (await fetch(`${base}/api/v1/verify/XXXX-XXXX-XXXX`)).json();
const { keys } = await (await fetch(`${base}/.well-known/isao-signing-keys.json`)).json();
const key = keys.find((k) => k.id === record.signature.keyId);

// RFC 8785 canonical JSON (sorted keys, no whitespace) for the payload's strings, numbers and arrays.
const canonical = (v) =>
  Array.isArray(v) ? `[${v.map(canonical).join(",")}]`
  : v !== null && typeof v === "object"
    ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`
    : JSON.stringify(v);

const publicKey = createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: key.publicKey }, format: "jwk" });
const valid = verify(
  null,
  Buffer.from(canonical(record.signature.payload), "utf8"),
  publicKey,
  Buffer.from(record.signature.value, "base64url"),
);
console.log(valid ? "Signature verified" : "Signature does not match");