AUDRIEDOCS
ReferenceVerifications

Verify a document hash

POST
/v1/verifications

Submits a document hash and returns a verdict — Audrie's answer to "does this document check out, and what do you know about it".

Both cryptographic checks are recomputed server-side for your request. Nothing in the request body is trusted beyond the identifiers used to look records up: a client that lies about a hash can only produce a verdict that fails its own verification.

Works with or without a credential. See Disclosure tiers.

What you get back

verdict is a single value you can branch on. It is derived from the four checks in matches[].checksPerformed plus revocation state, and it fails closed: anything Audrie cannot fully substantiate is never reported as verified.

Every determinate outcome — no_record included — returns 200 with a verdict. A hash Audrie holds no record of is information, not a failed request.

mode says what the verdict was computed over. Read the field rather than hardcoding a value.

Two modes

partial is the default: omit mode, or send partial explicitly, and Audrie looks up every anchored record registered against originalDocHash. This is the mode described above — one hash, zero or more records.

full re-verifies ONE exact certified PDF you already hold. Send mode: full together with recordId (the record id embedded in that PDF) and signedDocHash (prefixed sha256:). signedDocHash is the SHA-256 of the exact bytes you are holding with each signer's signature value replaced by the same number of ASCII 0 characters. A signer's signature value is the hex text of signers.<alg>.sig in the embedded certificate, without its quotes; Audrie's seal signature (serverSeals.<alg>.sig) is hashed unchanged. It is not the sha256sum of the file: a signer's signature cannot cover a hash that includes itself. Audrie's verifier computes it when it reads a certified PDF; sha256sum cannot, because each signer's signature value has to be located before it is blanked. originalDocHash stays the plain sha256sum of the original file. recordId and signedDocHash are required with full and rejected with partial — a request naming both a mode and fields that mode does not use is refused with 400, not silently corrected.

A full response carries at most one match — the exact record you named — and matches[] is empty for every collapsed outcome below.

A recordId that does not exist, one that names a document Audrie did not embed a certificate in, and a recordId whose hashes do not match the ones you sent all answer the same way: 200 with verdict: no_record and an empty matches[], identical in every field but verifiedAt, which records when the request was answered and carries no information about which cause applied. Response timing is not part of that guarantee either. recordId is not a secret — it appears in the Hedera messages an authenticated document anchors to — so treating any of these three causes differently in the response body would let anyone holding a recordId alone learn whether it exists and what kind of record it is, with no proof of possessing the document at all. Do not build logic that expects to tell these three apart from the response; a well-formed recordId reused across requests returns a body identical in every field but verifiedAt (which records when the request was answered and carries no information about which cause applied), regardless of which cause applies.

A malformed recordId — one that is not a UUID — is the one exception, and answers 400: it is a syntax error you can detect yourself before sending the request, not a fact about whether a record exists.

pending_anchor is answered honestly. It is reachable only once originalDocHash and signedDocHash are both confirmed against the named record, so by the time you see it you already hold a document whose bytes (apart from each signer's signature value) and record id, we have just told you, are correct — the reply adds nothing an attacker without that document could not already infer.

One hash, several records

partial mode does not let you pick the record. One hash can legitimately have several records — a re-signing, or several parties authenticating the same document — so the response returns every anchored match, ordered by anchor time, earliest first. The timeline is the answer.

verdict asserts that some valid anchored authentication exists, not that a particular party made it. Read each match's own status, signerStanding and signerHint before attributing a document to anyone.

Signer standing is live

signerStanding is recomputed at request time. A certificate issued earlier carries a frozen snapshot; this endpoint does not. The two can legitimately disagree, and the live value is the current answer.

Errors

A 400 here means originalDocHash or signedDocHash is not sha256: followed by 64 lowercase hex characters, reference is longer than 128 characters or contains a control character, mode is not partial or full, recordId/signedDocHash are sent with partial or missing with full, recordId is not a UUID, or the body is not a JSON object. error.details names the field.

Authorization

apiKey
headerAuthorizationBearer <aud_{live|test}_<keyId:16><secret:32><crc:6>>

Authorization: Bearer aud_live_…

Optional on POST /v1/verifications, which answers without any credential. Required to list or read verifications back.

Send exactly one Authorization header. A credential that is present and does not work is refused rather than downgraded to the anonymous tier.

Issue a key in the portal under Settings → Organisation → API credentials; it is shown once, at creation.

Three scopes exist. verifications:read covers every verification operation and is on every key. records:write covers POST /v1/records, and records:revoke covers POST /v1/records/{recordId}/revoke. Both are granted to a live or a test credential alike, on an organisation that holds the organisation-issued authentication add-on at the moment the key is minted; records:revoke is granted only when you ask for it. Scopes are fixed then and never widen afterwards, because a credential's recorded scopes are what an auditor reads to know what that key could do on a given date. A test key's records:write and records:revoke reach sandbox records only: signed with the organisation's sandbox key, anchored on Hedera's test network, and never a live record. A key minted without a scope never gains it; mint a new one. Buying the add-on later means minting a new key too; the old one keeps verifying.

Scopes, rotation, revocation, the live and test prefixes and what to do about a leaked key: https://audrie.io/docs/authentication

Request Body

application/json
  1. body

The document hash to verify, and an optional identifier of your own. Add mode: full to re-verify one exact certified PDF by record id instead — see mode.

originalDocHash*string

SHA-256 of the original document, before any Audrie certificate page was attached. Compute it yourself — sha256sum contract.pdf — and prefix it with sha256:.

Match^sha256:[0-9a-f]{64}$
mode?string

Which verdict to compute. Defaults to partial.

ValueMeaning
partialLook up every anchored record registered against originalDocHash. recordId and signedDocHash are rejected with this mode.
fullRe-verify one exact certified PDF, named by recordId. Requires recordId and signedDocHash.

Omitting mode is identical to sending partial.

Value in"partial""full"
recordId?string

The record id embedded in the certified PDF you are verifying — required with mode: full, rejected with partial. A malformed value (not a UUID) is rejected with 400; a well-formed one that does not exist returns the same response as a hash mismatch — see full mode in this operation's description.

Formatuuid
signedDocHash?string

SHA-256 of the exact certified PDF bytes you are holding, with the hex text of each signer's signature value (signers.<alg>.sig in the embedded certificate, without its quotes) replaced by the same number of ASCII 0 characters. Audrie's seal signature (serverSeals.<alg>.sig) is hashed unchanged. This is not the sha256sum of the file, and it is not computed the way originalDocHash is. Audrie's verifier computes it when it reads a certified PDF. A sha256sum of the file, or a hash with the seal signature blanked as well, is well formed once prefixed sha256: and answers no_record. Required with mode: full, rejected with partial.

Match^sha256:[0-9a-f]{64}$
reference?string

Your own identifier — a matter number, case id, or batch tag. Up to 128 characters, any script. Control characters are refused.

Stored and returned verbatim, filterable when listing, never interpreted. Do not put personal data here.

Ignored on anonymous requests, which are not logged. Stored but not echoed back while your organisation is being validated, because those responses are public tier.

Lengthlength <= 128

Response Body

A determinate verdict. Read verdict; no_record and pending_anchor arrive here rather than as errors.

application/json
  1. response

A verdict, and the records behind it. Which fields are populated depends on disclosureTier.

verificationId?string

Stable id for this verdict — use it to read the verdict back and to reconcile. A UUID. organisation tier only.

An anonymous verification is attributable to no one and is not retained, so it has none. A credentialed request from an organisation being validated is served at the public tier and so carries none either — but it is attributed to your organisation and retained, and appears in your log once validation completes.

Formatuuid
verdict*Verdict

Audrie's answer about the submitted document. It fails closed: anything we cannot fully substantiate is never verified. Treat any value you do not recognise as not verified.

ValueMeaningWhat to do
verifiedAn anchored record matches, every check passed, and the signature attributes to a person.Read each match's signerStanding and signerHint before accepting. The verdict is about the document, not about who authenticated it.
verified_orgAn anchored record matches and every check passed, and the signature attributes to an organisation's system key rather than to a person.Accept only if a signature made under an organisation's authority, with no named person accountable for it, satisfies your requirement. Read matches[].signerType to see which records are which, and at the organisation tier matches[].signerOrg for which organisation.
revokedA record matches and is no longer in good standing, or Audrie holds a revocation for this hash beyond the 25 records matches returns.Read revocation.reason when a revoked record appears in matches — it may not. A superseded document was still validly signed at the time.
signature_invalidA record matches and its stored signature failed re-verification.Escalate. Do not retry.
pending_anchorThe hash is registered and awaiting Hedera finality.Retry after retryAfterSeconds. Finality is 3–5 seconds.
no_recordAudrie holds no record of this document.Treat as unknown, not as an error.
sandboxThe only record matching this hash was authenticated with a test credential, anchored on Hedera's test network rather than the network a genuine record uses.Treat exactly like no_record for any trust decision. This is never an authentication: it names no signer, and any organisation holding a test credential can produce it for any hash you send.

A document authenticated moments ago answers no_record for the short window before its record is submitted to Hedera, then pending_anchor until finality. If you verify immediately after authenticating, retry rather than concluding the record was lost.

Where several records match one hash, the verdict is a single reduction across all of them. This is the precedence, and the first value any match produces is the answer:

signature_invalid → verified → verified_org → revoked → pending_anchor

So one tampered record is enough to answer signature_invalid, and one healthy anchored authentication is enough to answer verified or verified_org. Between those two, a match signed by a person outranks one signed by an organisation's system key: a hash carrying both answers verified, and verified_org means every healthy match was signed by an organisation.

The scalar therefore asserts only that SOME match produced the winning value. A verified or verified_org answer does not mean every match is healthy — a revoked record for the same hash is invisible in it — and it attributes the document to nobody. Read each match's own status and signerType rather than inferring either from the scalar.

sandbox sits outside that reduction, because a live record for a hash always answers first. An anonymous caller or a live credential sees sandbox only when Audrie's check for a live record finds none already accepted for anchoring against this hash. A record you have just authenticated — before Hedera accepts it — or one whose submission failed, is not visible to that check, so a sandbox record for the same hash can answer sandbox in that window. In full mode, this check is keyed by the recordId you send, not by the hash. The answer carries matches: [] and matchesTruncated: false — no anchor, signer or revocation detail, whatever state the sandbox record is actually in. A test credential sees only sandbox records: reading its OWN organisation's sandbox record for a hash, it gets sandbox in place of what would otherwise be verified or verified_org, in full match detail, with revoked, signature_invalid and pending_anchor reported as themselves so it can exercise every branch. Another organisation's sandbox record reads to a test credential exactly as it reads to an anonymous caller — collapsed to sandbox, with no detail.

A sandbox record does not last: we delete our sandbox record of it after 30 days; its hash remains on the Hedera test network, which we do not control.

Value in"verified""verified_org""revoked""signature_invalid""pending_anchor""no_record""sandbox"
disclosureTier*DisclosureTier

How much this response was permitted to disclose. Read it rather than inferring it from whether you sent a key. Treat any value you do not recognise as public.

ValueMeaning
publicServed without a credential, or with a credential on an organisation that is still being validated. Carries the verdict, every check, the anchor proof, revocation state and signer standing.
organisationServed with a credential on a validated organisation. Adds verificationId, environment, your reference and matches[].signerOrg on organisation-signed matches, and admits you to the reconciliation log.

Disclosure tiers has the full comparison.

Value in"public""organisation"
assuranceLevel*"audrie.registry.v1"

How strong this verdict is. Always present.

audrie.registry.v1 means the verdict was recomputed server-side from the anchored registry, with no certificate artifact issued. Read the field explicitly rather than hardcoding the value, and never treat its absence as a default.

Value in"audrie.registry.v1"
mode*string

What the verdict was computed over — see mode on the request.

ValueMeaning
partialComputed from every anchored record matching the document hash you submitted.
fullComputed by re-verifying one exact certified PDF, named by recordId in the request.

GET /v1/verifications/{verificationId} and the list operation return the mode recorded for that verification, not just POST /v1/verifications's immediate response.

Value in"partial""full"
environment?string

The environment of the key that produced this verdict, taken from its prefix. organisation tier only.

ValueMeaning
liveProduced by an aud_live_ key.
testProduced by an aud_test_ key.

The two are isolated, not ranked: a test key never sees live verifications and neither is a permission level. A verification recorded under test is checked only against sandbox records: your own organisation's in full detail, and another organisation's collapsed to the sandbox verdict (see Verdict).

Value in"live""test"
originalDocHash*DocHash

A SHA-256 digest — sha256: followed by 64 lowercase hex characters. Uppercase hex and a bare digest with no prefix are both refused.

Match^sha256:[0-9a-f]{64}$
verifiedAt*string

When Audrie computed this verdict. RFC 3339 timestamp, UTC.

Formatdate-time
reference?string

Echoed verbatim from the request. organisation tier only.

matches*array<>

Anchored records matching the hash, ordered by anchor time, earliest first. Empty for no_record and pending_anchor, and also for revoked when the revocation lies beyond the 25-record window described below.

mode: partial returns at most 25 records, and verdict is computed over the records returned, plus — for revoked only — a check that also covers records beyond these 25. A hash reaches 25 only when many parties independently authenticate identical bytes; if that is your use case, contact us.

verdict can be revoked even when no revoked record appears here: Audrie checks for a revocation on this hash beyond the 25 records returned. That beyond-window revocation reaches verdict only when none of the returned matches already yields verified, verified_org or signature_invalid — those outrank it. When one does, the revocation surfaces nowhere in the response; matchesTruncated: true is the only signal that the list may be hiding one. Read matchesTruncated to see whether this list itself may be incomplete.

mode: full returns at most one record — the one named by recordId in the request. no_record here means the id, kind and hashes were not all confirmed together; see full mode in the operation's description for why those causes are not distinguished.

matchesTruncated?boolean

Whether the underlying match lookup reached the 25-record limit and may hold more records than matches shows. false means the lookup is complete. It does not say how many further records exist, only that there may be more — and it can be true while matches is empty: a revocation beyond the 25-record window can turn verdict revoked without any of the returned records being the revoked one. Always false on mode: full, which resolves exactly one record and has no 25-record window to reach.

Present only on POST /v1/verifications, which recomputes matches and can answer definitively. Reading a recorded verdict back — GET /v1/verifications/{verificationId} and the list operation — never recomputes matches, so both omit this field rather than assert a stale answer.

retryAfterSeconds?integer

Seconds to wait before retrying. Present on a pending_anchor verdict from POST /v1/verifications, and on no other verdict. Reading a recorded pending_anchor verdict back does not carry it.

curl -X POST 'https://api.audrie.io/v1/verifications' \  -H 'Content-Type: application/json' \  -d '{  "originalDocHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}'
Example Response
{  "verdict": "verified",  "disclosureTier": "public",  "assuranceLevel": "audrie.registry.v1",  "mode": "partial",  "originalDocHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",  "verifiedAt": "2026-08-21T04:11:09Z",  "matches": [    {      "recordId": "8f7d2c11-2b3e-4a5f-9c8d-1e2f3a4b5c6d",      "kind": "pdf_embedded",      "status": "active",      "recordIssuedAt": "2026-05-02T22:14:51Z",      "signerType": "user",      "signerStanding": "good_standing",      "signerHint": "Wren & Associates LLP",      "checksPerformed": {        "hashBinding": "pass",        "userSignature": "pass",        "serverSeal": "pass",        "anchorStatus": "anchored"      },      "anchor": {        "topicId": "0.0.4915623",        "transactionId": "0.0.1234@1746223981.000000000",        "sequenceNumber": 8814,        "consensusAt": "2026-05-02T22:14:51.221Z",        "mirrorNodeUrl": "https://mainnet-public.mirrornode.hedera.com/api/v1/transactions/0.0.1234-1746223981-000000000"      }    }  ]}