Read one verification back
Returns one verification as it was recorded. Scoped to the calling key's organisation and environment.
matches is always [] here. Matches are derived from the document hash
rather than stored, so this operation answers "what did we tell you, and
when". To get current matches, POST the hash again, which is the
operation that computes them.
Errors
A 404 means the id does not exist, belongs to another organisation, or
belongs to another environment. These are not distinguished, and a
malformed id answers the same way. Existence is not disclosed.
apiKeyAuthorizationBearer <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
verificationId*stringThe verificationId returned when the verification was performed. A UUID.
uuidThe recorded verification, with matches empty.
application/json- response
A verdict, and the records behind it. Which fields are populated depends
on disclosureTier.
verificationId?stringStable 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.
uuidverdict*VerdictAudrie'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.
| Value | Meaning | What to do |
|---|---|---|
verified | An 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_org | An 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. |
revoked | A 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_invalid | A record matches and its stored signature failed re-verification. | Escalate. Do not retry. |
pending_anchor | The hash is registered and awaiting Hedera finality. | Retry after retryAfterSeconds. Finality is 3–5 seconds. |
no_record | Audrie holds no record of this document. | Treat as unknown, not as an error. |
sandbox | The 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.
"verified""verified_org""revoked""signature_invalid""pending_anchor""no_record""sandbox"disclosureTier*DisclosureTierHow 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.
| Value | Meaning |
|---|---|
public | Served 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. |
organisation | Served 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.
"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.
"audrie.registry.v1"mode*stringWhat the verdict was computed over — see mode on the request.
| Value | Meaning |
|---|---|
partial | Computed from every anchored record matching the document hash you submitted. |
full | Computed 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.
"partial""full"environment?stringThe environment of the key that produced this verdict, taken from its
prefix. organisation tier only.
| Value | Meaning |
|---|---|
live | Produced by an aud_live_ key. |
test | Produced 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).
"live""test"originalDocHash*DocHashA SHA-256 digest — sha256: followed by 64 lowercase hex characters.
Uppercase hex and a bare digest with no prefix are both refused.
^sha256:[0-9a-f]{64}$verifiedAt*stringWhen Audrie computed this verdict. RFC 3339 timestamp, UTC.
date-timereference?stringEchoed 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?booleanWhether 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?integerSeconds 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 GET 'https://api.audrie.io/v1/verifications/3c0d9a2e-77b1-4f0a-8a11-d1e2f3a4b5c6'{ "verificationId": "72d12273-0723-448e-a9f6-f7957128efa5", "verdict": "verified", "disclosureTier": "public", "assuranceLevel": "audrie.registry.v1", "mode": "partial", "environment": "live", "originalDocHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "verifiedAt": "2019-08-24T14:15:22Z", "reference": "string", "matches": [ { "recordId": "0470cfd4-dcf3-4d8c-ac28-58c6deeff25f", "kind": "pdf_embedded", "status": "active", "recordIssuedAt": "2019-08-24T14:15:22Z", "signerType": "user", "signerStanding": "good_standing", "signerHint": "string", "signerOrg": "string", "revocation": { "reason": "SUPERSEDED", "revokedAt": "2019-08-24T14:15:22Z" }, "checksPerformed": { "hashBinding": "pass", "userSignature": "pass", "serverSeal": "pass", "anchorStatus": "anchored" }, "anchor": { "topicId": "0.0.4915623", "transactionId": "0.0.1234@1746223981.000000000", "sequenceNumber": 0, "consensusAt": "2019-08-24T14:15:22Z", "mirrorNodeUrl": "http://example.com" } } ], "matchesTruncated": true, "retryAfterSeconds": 0}Verify a document hash POST
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](https://audrie.io/docs/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.
Authenticate a document as your organisation POST
Authenticates a document **as the calling organisation**, signing its hash with the organisation's own post-quantum key and anchoring the result to Hedera. This is the only operation on this API that creates anything. Send a hash. Audrie never receives the document. > **A `test`-prefixed credential creates a sandbox record.** It signs > with your organisation's sandbox key — separate from your production > key — and anchors to Hedera's **test** network, never mainnet. A > sandbox record verifies only as `sandbox`, never `verified_org`, > carries no legal weight, and we delete our sandbox record of it after > 30 days; its hash remains on the Hedera test network, which we do not > control. > > A hash you authenticate with a `test` key answers `sandbox` to > **any** caller who submits it, anonymous callers included, for as > long as the sandbox record exists — do not authenticate a hash of a > document whose existence is confidential. Mint a `live` credential > when you are ready to create records that carry legal weight. ## What this is, and what it is not The signature attributes to an **organisation**, not to a person. No individual is accountable for it: there is no identity-verified signer and no WebAuthn ceremony behind an API call. A verifier sees that difference — these records answer `verified_org` rather than `verified`, carry `signerType: org` on the match, and at the `organisation` tier carry your organisation's registered name in `signerOrg`. Use it where an organisation is the right signer: bulk authentication of documents your organisation issues. Where a named person must be accountable, have that person authenticate in the portal instead. ## Idempotency Retrying is safe, and you do not need an idempotency key: the document hash is the key. Your organisation can hold **one active record per document hash**. A repeat call for a hash you already hold returns that record with `200` and creates nothing; a first call returns `201`. The body is identical either way, because after a timeout you cannot know which call you made — branch on the status only if you care. A record whose anchor ultimately **failed** stops counting, so a later call for that hash creates a new record rather than returning the failed one forever. ## Anchoring is asynchronous, and it can fail On a `201`, `anchorState` is `pending` or `accepted`, and reaches `anchored` at Hedera finality — typically 3–5 seconds. On a `200` retry it is whatever the existing record has already reached, which may be `anchored`. Branch over all three values, not two. Poll `POST /v1/verifications` with the same hash rather than this operation. **Read the verdict, not the clock, and expect three outcomes.** Until the record reaches the ledger, verification answers `no_record` — it reports only records that have been accepted for anchoring, so a record you have just created is briefly invisible there. Then: | The verdict becomes | Meaning | What to do | |---|---|---| | `pending_anchor` | On the ledger, awaiting consensus. | Wait `retryAfterSeconds` and poll again. | | `verified_org` | Anchored. Done. A record created with a `test` key reads `sandbox` here instead. | Nothing. | | still `no_record` after ~3 minutes | **The anchor failed** before the message was submitted. The record is abandoned and stops being active. | Call `POST /v1/records` again for this hash. It will create a new record rather than returning the abandoned one. | | stuck at `pending_anchor` for many minutes | The message reached the ledger but consensus has not been reconciled. **This one does not self-resolve by retrying**, and re-authenticating will not help — the record is still active, so a repeat call returns it unchanged. | Report it with the `recordId`. | **An anchored record cannot be withdrawn.** A Hedera message is permanent. Revocation marks a record as no longer in good standing rather than removing it — see `POST /v1/records/{recordId}/revoke`. ## Errors A `400` means `originalDocHash` is not `sha256:` followed by 64 **lowercase** hex characters, `filename` is longer than 500 characters or contains a control or invisible formatting character, or the body is not a JSON object. `error.details` names the field. Uppercase hex is refused rather than normalised: the hash is used verbatim inside the signature and the ledger message, so two spellings would be two separate permanent records for one document. The `402` and `403` each have more than one cause. **Branch on `error.code`**, and read the response descriptions below — they say which causes exist and which of them mean "wait" rather than "fix something".