openapi: 3.1.0

info:
  title: Audrie Public API
  version: 1.0.0-draft
  summary: Verify document authenticity — anonymously, or as your organisation.
  description: |
    Verify that a document was cryptographically authenticated and anchored to a
    public ledger. You send a SHA-256 hash and receive a verdict. Audrie never
    receives your document, and holds hashes rather than content.

    Almost everything here verifies. The one operation that creates anything is
    `POST /v1/records`, which authenticates a document **as an organisation** —
    signed with the organisation's own key, with no individual accountable for
    the signature. It needs its own entitlement and its own scope.

    That is deliberately not the same thing as a person signing. Authenticating
    a document as a NAMED INDIVIDUAL requires a signer who has completed
    identity verification, authorising the operation with WebAuthn, and that
    cannot be done through an API key. A verifier can tell the two apart:
    organisation-signed records answer `verified_org` rather than `verified`.

    - Introduction: <https://audrie.io/docs/introduction>
    - Quickstart: <https://audrie.io/docs/quickstart>
    - Disclosure tiers: <https://audrie.io/docs/disclosure-tiers>
    - Errors and rate limits: <https://audrie.io/docs/errors>
    - Pagination: <https://audrie.io/docs/pagination>
    - Verifying without Audrie: <https://audrie.io/docs/independent-verification>
  contact:
    name: Audrie Platform
    url: https://audrie.io
  license:
    name: Proprietary
    identifier: LicenseRef-Audrie-Proprietary

servers:
  - url: https://api.audrie.io
    description: Production
  - url: https://api.dev.audrie.io
    description: Development

# Anonymous or credentialed. The empty object is the anonymous alternative.
security:
  - {}
  - apiKey: []

tags:
  - name: Verifications
    description: Verify a document hash, and read back what your organisation has verified.
  - name: Records
    description: Authenticate a document as your organisation.

paths:
  /v1/verifications:
    post:
      operationId: createVerification
      tags: [Verifications]
      summary: Verify a document hash
      security:
        - {}
        - apiKey: []
      description: |
        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` is `partial` on every response. Read the field rather than
        hardcoding the value.

        ## One hash, several records

        You do not 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` is not `sha256:` followed by 64
        lowercase hex characters, `reference` is longer than 128 characters or
        contains a control character, or the body is not a JSON object.
        `error.details` names the field.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerificationRequest'
            examples:
              minimal:
                summary: Anonymous — hash only
                value:
                  originalDocHash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
              withReference:
                summary: Credentialed, tagged for reconciliation
                value:
                  originalDocHash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
                  reference: matter-2026-114
      responses:
        '200':
          description: |
            A determinate verdict. Read `verdict`; `no_record` and
            `pending_anchor` arrive here rather than as errors.
          headers:
            X-RateLimit-Limit:      { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining:  { $ref: '#/components/headers/X-RateLimit-Remaining' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Verification'
              examples:
                anonymousVerified:
                  summary: verified — anonymous tier
                  value:
                    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
                credentialedVerified:
                  summary: verified — organisation tier
                  value:
                    verificationId: 3c0d9a2e-77b1-4f0a-8a11-d1e2f3a4b5c6
                    verdict: verified
                    disclosureTier: organisation
                    assuranceLevel: audrie.registry.v1
                    mode: partial
                    environment: live
                    originalDocHash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
                    verifiedAt: '2026-08-21T04:11:09Z'
                    reference: matter-2026-114
                    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
                credentialedVerifiedOrg:
                  summary: verified_org — organisation tier, the signing organisation named
                  value:
                    verificationId: 9b1e4f6a-2c7d-4e83-b0a5-6f1d2c3b4a59
                    verdict: verified_org
                    disclosureTier: organisation
                    assuranceLevel: audrie.registry.v1
                    mode: partial
                    environment: live
                    originalDocHash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
                    verifiedAt: '2026-08-21T04:11:09Z'
                    reference: matter-2026-114
                    matches:
                      - recordId: 2a9c6e41-7f3b-4d18-8e5c-0b1a2c3d4e5f
                        kind: record_only
                        status: active
                        recordIssuedAt: '2026-05-02T22:14:51Z'
                        signerType: org
                        signerStanding: good_standing
                        signerOrg: Northwind Conveyancing Pty Ltd
                        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
                revoked:
                  summary: revoked — read the reason before concluding what it means
                  value:
                    verdict: revoked
                    disclosureTier: public
                    assuranceLevel: audrie.registry.v1
                    mode: partial
                    originalDocHash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
                    verifiedAt: '2026-08-21T04:12:40Z'
                    matches:
                      - recordId: 8f7d2c11-2b3e-4a5f-9c8d-1e2f3a4b5c6d
                        kind: pdf_embedded
                        status: revoked
                        recordIssuedAt: '2026-05-02T22:14:51Z'
                        signerType: user
                        signerStanding: good_standing
                        revocation:
                          reason: SUPERSEDED
                          revokedAt: '2026-07-14T09:02:11Z'
                        checksPerformed:
                          hashBinding: pass
                          userSignature: pass
                          serverSeal: pass
                          anchorStatus: anchored
                noRecord:
                  summary: no_record — a 200, not a 404
                  value:
                    verdict: no_record
                    disclosureTier: public
                    assuranceLevel: audrie.registry.v1
                    mode: partial
                    originalDocHash: sha256:0000000000000000000000000000000000000000000000000000000000000000
                    verifiedAt: '2026-08-21T04:13:02Z'
                    matches: []
                pendingAnchor:
                  summary: pending_anchor — registered, awaiting Hedera finality
                  value:
                    verdict: pending_anchor
                    disclosureTier: public
                    assuranceLevel: audrie.registry.v1
                    mode: partial
                    originalDocHash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
                    verifiedAt: '2026-08-21T04:13:44Z'
                    matches: []
                    retryAfterSeconds: 5
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

    get:
      operationId: listVerifications
      tags: [Verifications]
      summary: List your organisation's verifications
      security:
        - apiKey: []
      description: |
        Returns your organisation's verification log, newest first.

        Scoped to the calling key's organisation and environment: a `test` key
        never sees `live` verifications. Anonymous verifications are attributable
        to no one and are not logged, so a credential is required.

        Rows carry the verdict as it was recorded. They carry no `matches`: to
        get matches for a hash, `POST` it, which is the operation that computes
        them.

        Cursor-paginated. See [Pagination](https://audrie.io/docs/pagination).

        ## Errors

        Every query parameter is rejected rather than ignored or clamped. An
        unparseable `from`, an out-of-range `limit` or a cursor that does not
        decode all answer `400` with the offending field in `error.details` — a
        silently different page is worse than a refusal on a log you reconcile
        against.
      parameters:
        - name: verdict
          in: query
          description: Return only verifications that recorded this verdict.
          schema: { $ref: '#/components/schemas/Verdict' }
        - name: originalDocHash
          in: query
          description: Return only verifications of this document hash.
          schema: { $ref: '#/components/schemas/DocHash' }
        - name: from
          in: query
          description: |
            Return only verifications at or after this instant. RFC 3339
            timestamp.
          schema: { type: string, format: date-time }
          example: '2026-08-01T00:00:00Z'
        - name: to
          in: query
          description: |
            Return only verifications at or before this instant. RFC 3339
            timestamp.
          schema: { type: string, format: date-time }
          example: '2026-08-31T23:59:59Z'
        - name: limit
          in: query
          description: |
            Rows per page. An integer between 1 and 100. Defaults to 25.
            A value outside that range is refused, not clamped.
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
        - name: after
          in: query
          description: |
            Opaque cursor from a previous page's `pagination.nextCursor`. Pass it
            back verbatim; do not construct or modify one.
          schema: { type: string }
      responses:
        '200':
          description: |
            A page of verifications, newest first, with a cursor for the next
            page in `pagination`.
          headers:
            X-RateLimit-Limit:      { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining:  { $ref: '#/components/headers/X-RateLimit-Remaining' }
          content:
            application/json:
              schema:
                type: object
                description: One page of the reconciliation log.
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    description: The page's rows, newest first.
                    items: { $ref: '#/components/schemas/VerificationSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /v1/verifications/{verificationId}:
    get:
      operationId: getVerification
      tags: [Verifications]
      summary: Read one verification back
      security:
        - apiKey: []
      description: |
        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.
      parameters:
        - name: verificationId
          in: path
          required: true
          description: The `verificationId` returned when the verification was performed. A UUID.
          schema: { type: string, format: uuid }
          example: 3c0d9a2e-77b1-4f0a-8a11-d1e2f3a4b5c6
      responses:
        '200':
          description: The recorded verification, with `matches` empty.
          headers:
            X-RateLimit-Limit:      { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining:  { $ref: '#/components/headers/X-RateLimit-Remaining' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Verification' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /v1/records:
    post:
      operationId: createRecord
      tags: [Records]
      summary: Authenticate a document as your organisation
      security:
        - apiKey: []
      description: |
        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 cannot reach this operation.** There is
        > no sandbox ledger: this endpoint signs with your organisation's
        > production key and anchors a permanent, public Hedera message that
        > **cannot be withdrawn**, so a key labelled `test` is never granted
        > `records:write` at all and is refused here with `403`. Mint a `live`
        > credential when you are ready to create records that are real.

        ## 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. | 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".
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordRequest'
            examples:
              minimal:
                summary: Hash only
                value:
                  originalDocHash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
              withFilename:
                summary: With a filename for your own reconciliation
                value:
                  originalDocHash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
                  filename: deed-of-variation-2026-114.pdf
      responses:
        '201':
          description: |
            The document was authenticated and submitted to Hedera. Read
            `anchorState`; finality follows within seconds.
          headers:
            X-RateLimit-Limit:      { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining:  { $ref: '#/components/headers/X-RateLimit-Remaining' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Record' }
              example:
                recordId: 9a1b7c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d
                originalDocHash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
                authenticatedAt: '2026-09-14T03:21:44Z'
                anchorState: accepted
                verifyUrl: https://app.audrie.io/verify/9a1b7c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d
                anchor:
                  topicId: '0.0.4915623'
                  transactionId: 0.0.1234@1746223981.000000000
                  hashscanUrl: https://hashscan.io/mainnet/transaction/0.0.1234@1746223981.000000000
                retryAfterSeconds: 5
        '200':
          description: |
            Your organisation already holds an active record for this hash. It is
            returned unchanged and **nothing was created**. The body is the same
            shape as the `201`.

            **A repeat call updates nothing** — a different `filename` in the
            body is ignored, not applied. If a filename you have already
            published needs changing, re-sending will not do it: the record is
            anchored, and correcting it is a support request.
          headers:
            X-RateLimit-Limit:      { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining:  { $ref: '#/components/headers/X-RateLimit-Remaining' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Record' }
              example:
                recordId: 9a1b7c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d
                originalDocHash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
                authenticatedAt: '2026-09-14T03:21:44Z'
                anchorState: anchored
                verifyUrl: https://app.audrie.io/verify/9a1b7c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d
                anchor:
                  topicId: '0.0.4915623'
                  transactionId: 0.0.1234@1746223981.000000000
                  hashscanUrl: https://hashscan.io/mainnet/transaction/0.0.1234@1746223981.000000000
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '402': { $ref: '#/components/responses/RecordsAddonRequired' }
        '403': { $ref: '#/components/responses/RecordsForbidden' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/RecordsUnavailable' }

  /v1/records/{recordId}/revoke:
    post:
      operationId: revokeRecord
      tags: [Records]
      summary: Revoke a record your organisation authenticated
      security:
        - apiKey: []
      description: |
        Marks a record your organisation authenticated as no longer in good
        standing, and anchors that decision to Hedera.

        **Revocation is permanent.** It cannot be undone, by you or by us. The
        record is not removed — a Hedera message cannot be withdrawn — and
        verification continues to report it, with a `revoked` verdict and the
        reason you gave.

        ## The credential must carry `records:revoke`

        This scope is granted only when you ask for it as you mint a credential.
        A credential that can authenticate documents cannot revoke them unless
        you requested both, and a credential minted without it never gains the
        scope — mint a new one.

        Ask for it on the credentials you intend to revoke with, and not on the
        ones your batch jobs carry. A credential holding this scope can revoke
        **any** record your organisation authenticated, including records
        created by a different credential. That is what makes it the right tool
        after a credential is lost: mint a new credential with `records:revoke`
        and clean up with it.

        **A credential granted `records:revoke` also carries `records:write`.**
        There is no revoke-only credential, so a key you mint to clean up after
        an incident can also authenticate documents as your organisation —
        protect it as a signing key, and revoke it when the cleanup is done.

        A `test`-prefixed credential is never granted this scope.

        ## What you can revoke

        Records your **organisation** authenticated through this API. Documents
        a person authenticated in the Audrie app are not revocable here — only
        that signer can revoke those, from the app.

        Revoking is not a way to correct a mistake in the document itself. A
        corrected document is a new document with a new hash: authenticate the
        new one, and revoke the old one as `SUPERSEDED`.

        ## Errors

        A `403` has two causes, and they read differently. A credential without
        `records:revoke` is told so, and the message names the scope. Otherwise
        the record is not one your organisation can revoke — and that answer
        deliberately does not distinguish a record belonging to another
        organisation from one a person authenticated in the Audrie app.

        A `409` has two causes and does not distinguish them. **Do not retry
        blindly**: if the record is already revoked, retrying never succeeds,
        because revocation is permanent. If its anchor has not yet reached
        consensus, retry once `POST /v1/verifications` reports `verified_org`.
        That call is also how you tell the two apart — an already-revoked record
        reports `revoked`.
      parameters:
        - name: recordId
          in: path
          required: true
          description: The `recordId` returned when the document was authenticated. UUID.
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RevokeRecordRequest'
            examples:
              superseded:
                summary: A newer document replaces this one
                value:
                  reason: SUPERSEDED
              error:
                summary: The document was wrong
                value:
                  reason: ERROR_IN_DOCUMENT
      responses:
        '200':
          description: |
            The record is revoked and the decision has been submitted to Hedera.
            Verification reports `revoked` for this record from now on.
          headers:
            X-RateLimit-Limit:      { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining:  { $ref: '#/components/headers/X-RateLimit-Remaining' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RevokeRecordResponse' }
              example:
                recordId: 9a1b7c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '402': { $ref: '#/components/responses/RecordsAddonRequired' }
        '403': { $ref: '#/components/responses/RecordsForbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/RecordNotRevocable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/RecordsUnavailable' }

components:

  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: aud_{live|test}_<keyId:16><secret:32><crc:6>
      description: |
        `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.

        Two scopes exist. `verifications:read` covers every verification
        operation and is on every key. `records:write` covers
        `POST /v1/records` and is granted only on a **`live`** credential whose
        organisation holds the organisation-issued authentication add-on **at the
        moment the key is minted** — a `test` key never receives it, because
        there is no sandbox ledger and the records it would create would be
        permanent — 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. Buying the add-on later means minting a new
        key; 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>

  headers:
    X-RateLimit-Limit:
      description: |
        Requests permitted in the current window, for the tightest limit that
        applies to this request.
      schema: { type: integer }
    X-RateLimit-Remaining:
      description: |
        Requests left in the current window for the tightest limit that applies.
        A `429` is still possible under load, so treat this as a budget rather
        than a guarantee.
      schema: { type: integer }
    Retry-After:
      description: Seconds to wait before retrying.
      schema: { type: integer }

  schemas:

    DocHash:
      type: string
      description: |
        A SHA-256 digest — `sha256:` followed by 64 lowercase hex characters.
        Uppercase hex and a bare digest with no prefix are both refused.
      pattern: '^sha256:[0-9a-f]{64}$'
      examples:
        - sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

    RecordRequest:
      type: object
      description: |
        What to authenticate. It names no organisation and no signer: both come
        from the API credential you present.

        **There is no `orgId` or `credentialId` field, and there never will be.**
        Sending one is not an error — unknown properties are ignored — but it has
        no effect whatsoever. Tenancy is taken from the credential and from
        nothing else, so a key can only ever write records for its own
        organisation.
      required: [originalDocHash]
      properties:
        originalDocHash:
          $ref: '#/components/schemas/DocHash'
        filename:
          type: string
          maxLength: 500
          description: |
            Optional label stored with the record. At most 500 characters.
            Control characters are refused, and so are invisible formatting
            characters generally — for example bidirectional overrides and
            isolates, the soft hyphen, the zero-width space and the byte-order
            mark — because this text is rendered on a public page and those
            characters can make a name display as something it is not.

            **Two exceptions:** the zero-width joiner and non-joiner are
            permitted, because Persian and several Indic scripts require them to
            spell words correctly and emoji sequences use them. Accented and CJK
            filenames are fine; the rule is about invisible characters, not
            about alphabet.

            **It is not private. Do not put confidential text in it.** It is
            shown on the record's verification page — the `verifyUrl` this
            operation returns — which needs no credential, so anyone who has that
            link or the `recordId` can read it. A client name, a matter
            reference or a deal codename in a filename is disclosed to whoever
            you share the link with, and to anyone they share it with.

            It is metadata and nothing more: it is not the document, it is not
            hashed, and it does not appear in the signature or on the ledger. It
            is NOT returned by the verification endpoints — `matches[]` carries
            no filename at any disclosure tier.
          examples:
            - deed-of-variation-2026-114.pdf

    AnchorState:
      type: string
      description: |
        How far a record has got towards Hedera finality. Treat any value you do
        not recognise as not yet final.

        | Value | Meaning |
        |---|---|
        | `pending` | The record exists and its ledger message has not been confirmed as submitted. |
        | `accepted` | The ledger accepted the message and consensus is seconds away. |
        | `anchored` | Consensus reached. The record is final and permanent. |

        **This field is Audrie's own record of progress, not the ledger's**, and
        so is the verdict you get back from verifying the hash — `checksPerformed`
        says which of its fields are cryptographic recomputations and which
        report stored state, and the anchor status is stored state.

        `pending` and `accepted` plainly mean "not yet". To confirm `anchored`
        against the ledger itself, resolve the match's `anchor.mirrorNodeUrl`
        against a public Hedera mirror node — that is the one answer that does
        not rest on anything Audrie wrote down.

        `POST /v1/records`'s *Anchoring is asynchronous, and it can fail* section
        is keyed on the VERIFICATION verdict rather than on these values, and
        includes the case that does not resolve by waiting.

        This is a different question from `ChecksPerformed.anchorStatus`, which
        reports whether a check passed. This reports progress.
      enum: [pending, accepted, anchored]

    RevokeRecordRequest:
      type: object
      required: [reason]
      properties:
        reason:
          type: string
          description: |
            Why the record is being revoked. It is recorded in the ledger
            message and reported to anyone who verifies the document, so choose
            the one that is true.

            The reason decides how a verifier should read the revocation.
            Revoking as `SUPERSEDED` does not suggest the original was
            improperly authenticated; `KEY_COMPROMISE` does.

            | Value | Meaning | What a verifier should conclude |
            |---|---|---|
            | `SUPERSEDED` | A newer document replaces this one. | The document was validly authenticated. Look for the replacement. |
            | `ERROR_IN_DOCUMENT` | The document contains an error. | The document was validly authenticated, and its contents are wrong. |
            | `KEY_COMPROMISE` | The signing key may be in someone else's hands. | Treat the authentication itself as untrustworthy. |
            | `PRIVILEGE_WITHDRAWN` | The authority to authenticate this document was withdrawn. | The signer was not entitled to authenticate it. |
            | `AFFILIATION_CHANGED` | The signer's relationship to the organisation changed. | The document was validly authenticated at the time. |
            | `CESSATION_OF_OPERATION` | The organisation or service has stopped operating. | The document was validly authenticated at the time. |
            | `CERTIFICATE_HOLD` | Temporarily withdrawn pending a decision. | Do not rely on it for now. |
            | `UNSPECIFIED` | No reason given. | Nothing beyond the fact of revocation. |
            | `OTHER` | A reason outside this list. | Nothing beyond the fact of revocation. |
          enum:
            - SUPERSEDED
            - ERROR_IN_DOCUMENT
            - KEY_COMPROMISE
            - PRIVILEGE_WITHDRAWN
            - AFFILIATION_CHANGED
            - CESSATION_OF_OPERATION
            - CERTIFICATE_HOLD
            - UNSPECIFIED
            - OTHER
          example: SUPERSEDED

    RevokeRecordResponse:
      type: object
      description: The outcome of a revocation.
      required: [recordId]
      properties:
        recordId:
          type: string
          format: uuid
          description: The record that was revoked. UUID.

    Record:
      type: object
      description: |
        A document authenticated by your organisation.

        It carries no `kind` and no `signerType`: every record this operation
        produces is a `record_only` record signed by the organisation's system
        key. Those fields are published per match on the verification
        endpoints, where they vary.
      required:
        - recordId
        - originalDocHash
        - authenticatedAt
        - anchorState
        - verifyUrl
        - anchor
      properties:
        recordId:
          type: string
          format: uuid
          description: |
            Audrie's identifier for this record. Stable and permanent; store it.
          examples:
            - 9a1b7c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d
        originalDocHash:
          $ref: '#/components/schemas/DocHash'
        authenticatedAt:
          type: string
          format: date-time
          description: |
            When the record was authenticated, RFC 3339 UTC. This is the signed
            time, not the ledger's consensus time — the two differ by seconds
            and the consensus time is published on the verification endpoints.
          examples:
            - '2026-09-14T03:21:44Z'
        anchorState:
          $ref: '#/components/schemas/AnchorState'
        verifyUrl:
          type: string
          format: uri
          description: |
            A human-readable verification page for this record. It needs no
            credential, so treat it as public: anyone you send it to, and anyone
            they send it to, can open it.

            **It shows the `filename` you supplied.** See the warning on
            `filename` in the request schema before putting anything
            confidential there.
          examples:
            - https://app.audrie.io/verify/9a1b7c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d
        anchor:
          type: object
          description: |
            Where the record is committed on Hedera.

            There is no `mirrorNodeUrl` here, unlike on a verification match: at
            the moment this response is written the message may not have reached
            consensus, so a mirror node would answer `404` for it. `topicId` and
            `transactionId` are the coordinates; `POST /v1/verifications`
            publishes the resolvable URL once there is something to resolve.
          required: [topicId, transactionId, hashscanUrl]
          properties:
            topicId:
              type: string
              description: The Hedera topic this record is anchored to — your organisation's own.
              examples: ['0.0.4915623']
            transactionId:
              type: string
              description: The Hedera transaction id carrying the record's ledger message.
              examples: ['0.0.1234@1746223981.000000000']
            hashscanUrl:
              type: string
              format: uri
              description: |
                The transaction on HashScan, a public Hedera explorer Audrie does
                not operate. For a person to read; it resolves once consensus is
                reached.
              examples:
                - https://hashscan.io/mainnet/transaction/0.0.1234@1746223981.000000000
        retryAfterSeconds:
          type: integer
          description: |
            Seconds to wait before verifying, present while `anchorState` is not
            `anchored`. Absent once the record is final.
          examples: [5]

    DisclosureTier:
      type: string
      description: |
        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`.

        | 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](https://audrie.io/docs/disclosure-tiers) has the full
        comparison.
      enum: [public, organisation]

    Verdict:
      type: string
      description: |
        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.

        | 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. | Read `revocation.reason`. 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. |

        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.
      enum:
        - verified
        - verified_org
        - revoked
        - signature_invalid
        - pending_anchor
        - no_record

    AssuranceLevel:
      type: string
      description: |
        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.
      enum: [audrie.registry.v1]

    SignerStanding:
      type: string
      description: |
        How the record's signer is doing. A second axis, orthogonal to the
        record's own `status`: a record can be `active` while its signer is
        `identity_revoked`, and that matters.

        Recomputed at request time, so it can differ from a snapshot taken when a
        certificate was issued. Anything other than `good_standing` is a reason
        to look closer before relying on the document.

        | Value | Meaning | What to do |
        |---|---|---|
        | `good_standing` | No signer-standing issue applies to this record. | Nothing. |
        | `affiliation_ended` | The signer's membership of the record's organisation has ended. | The signature stands; the signer no longer speaks for that organisation. |
        | `under_review` | The signer's account is suspended pending review. | Treat the document as provisional and re-verify later. |
        | `account_closed` | The signer's account has been deactivated. | The signature stands; there is no live signer to contact. |
        | `identity_revoked` | The signer's identity verification has failed, expired, or been withdrawn. | Do not rely on the identity behind this record. |
        | `indeterminate` | Standing could not be established. | Treat as not established. It is never a synonym for `good_standing`. |

        Where more than one applies, the most severe is reported, in the order
        above.
      enum:
        - good_standing
        - account_closed
        - affiliation_ended
        - identity_revoked
        - under_review
        - indeterminate

    ChecksPerformed:
      type: object
      description: |
        The four checks run against a matched record. All four must pass for a
        `verified` verdict. Two are cryptographic re-computations performed for
        your request; two report how the record was selected and what its stored
        state is.

        | Check | Kind |
        |---|---|
        | `hashBinding` | Guaranteed by selection |
        | `userSignature` | Recomputed for this request |
        | `serverSeal` | Recomputed for this request |
        | `anchorStatus` | Reported from stored state |
      required: [hashBinding, userSignature, serverSeal, anchorStatus]
      properties:
        hashBinding:
          type: string
          description: |
            The submitted hash binds to this record. `pass` or `fail`.

            Guaranteed by selection: the record was retrieved **by** the hash you
            submitted, so a returned match is bound to it by construction. The
            field is present so the check is explicit rather than implied.
          enum: [pass, fail]
        userSignature:
          type: string
          description: |
            The signer's stored ML-DSA-65 (FIPS 204) signature re-verifies
            against the public key held on the record. `pass` or `fail`.

            Recomputed for this request. This is the check that catches a forged
            **signer**.
          enum: [pass, fail]
        serverSeal:
          type: string
          description: |
            Audrie's own post-quantum seal over the record re-verifies, and the
            sealed fields cross-bind to the stored row. `pass` or `fail`.

            Recomputed for this request. This is the check that catches a
            modified **record** — an altered hash, timestamp or signer id in the
            row itself — which `userSignature` cannot see.
          enum: [pass, fail]
        anchorStatus:
          type: string
          description: |
            The record's Hedera anchor state. Only `anchored` counts toward a
            `verified` verdict.

            Reports Audrie's record of the anchoring outcome. To confirm the
            anchor independently, resolve `matches[].anchor.mirrorNodeUrl`
            against a public Hedera mirror node we do not operate.

            | Value | Meaning |
            |---|---|
            | `anchored` | Committed to Hedera and final. |
            | `pending` | Submitted, awaiting consensus. |
            | `failed` | Not committed. The record is not independently checkable. |
          enum: [anchored, pending, failed]

    Anchor:
      type: object
      description: |
        Where this record is committed on Hedera Consensus Service. Checkable
        without Audrie — resolve `mirrorNodeUrl` against a public mirror node we
        do not operate. The procedure is at
        <https://audrie.io/docs/independent-verification>.

        The committed message carries the document's SHA-256 and nothing else
        about it: no document content, no filename, and no identity. That is
        what makes the commitment checkable by someone who holds the same
        document and does not trust us.
      required: [topicId, transactionId, sequenceNumber, consensusAt, mirrorNodeUrl]
      properties:
        topicId:
          type: string
          description: The Hedera Consensus Service topic this record was submitted to.
          examples: ['0.0.4915623']
        transactionId:
          type: string
          description: The Hedera transaction id of the submission.
          examples: ['0.0.1234@1746223981.000000000']
        sequenceNumber:
          type: integer
          format: int64
          description: |
            Position of this record's message in the topic. A 64-bit integer.
            Use it with `topicId` to fetch the committed message from a mirror
            node.
        consensusAt:
          type: string
          format: date-time
          description: |
            Hedera consensus timestamp — the authoritative "when". RFC 3339, UTC,
            to nanosecond precision. It is assigned by network consensus, never
            by a server clock, and Audrie cannot backdate it.
        mirrorNodeUrl:
          type: string
          format: uri
          description: |
            Absolute URL that resolves this transaction on a public Hedera mirror
            node. Fetch it directly, or substitute a mirror node you prefer —
            `topicId` and `sequenceNumber` are all you need.

    Revocation:
      type: object
      description: |
        Why and when this record was revoked. Present only on a record that has
        been explicitly revoked; `reason` and `revokedAt` are both present when
        the object is.

        `reason` is reported faithfully and is not interpreted for you. Read it
        before concluding what a revocation means: a document that was superseded
        was still validly signed at the time, which is a different fact from a
        key compromise.
      required: [reason, revokedAt]
      properties:
        reason:
          type: string
          description: |
            Why the record was revoked. These mirror the RFC 5280 §5.3.1
            CRLReason set, plus `ERROR_IN_DOCUMENT` and `OTHER`. Values are never
            renamed or removed.

            | Value | Meaning | Was it validly signed at the time? |
            |---|---|---|
            | `SUPERSEDED` | A later document replaces this one. | Yes. |
            | `ERROR_IN_DOCUMENT` | The document contained a mistake. | Yes. |
            | `KEY_COMPROMISE` | The signing key is believed compromised. | No — treat the signature as unreliable. |
            | `PRIVILEGE_WITHDRAWN` | The signer's authority to sign this was withdrawn. | Not for this purpose. |
            | `AFFILIATION_CHANGED` | The signer's organisational affiliation changed. | Yes. |
            | `CESSATION_OF_OPERATION` | The signer or organisation stopped operating. | Yes. |
            | `CERTIFICATE_HOLD` | Revoked provisionally, pending a decision. | Undetermined — re-verify later. |
            | `UNSPECIFIED` | No reason was given. | Undetermined. |
            | `OTHER` | A reason outside this set. | Undetermined. |
          enum:
            - SUPERSEDED
            - ERROR_IN_DOCUMENT
            - KEY_COMPROMISE
            - PRIVILEGE_WITHDRAWN
            - AFFILIATION_CHANGED
            - CESSATION_OF_OPERATION
            - CERTIFICATE_HOLD
            - UNSPECIFIED
            - OTHER
        revokedAt:
          type: string
          format: date-time
          description: When the record was revoked. RFC 3339 timestamp, UTC.

    Match:
      type: object
      description: |
        One record matching the submitted hash.

        Matches are pseudonymous by default: they carry the record, its checks
        and its anchor, and no identity beyond the hint the signer chose to
        publish. At the `organisation` tier a match signed by an organisation
        also carries that organisation's registered name in `signerOrg`.
      required: [recordId, kind, status, recordIssuedAt, signerType, checksPerformed]
      properties:
        recordId:
          type: string
          format: uuid
          description: Stable id for this record. A UUID.
        kind:
          type: string
          description: |
            What was produced when this record was authenticated.

            | Value | Meaning |
            |---|---|
            | `pdf_embedded` | A PDF carrying an embedded Audrie certificate page. |
            | `record_only` | A hash was authenticated with no artifact returned. |
          enum: [pdf_embedded, record_only]
        status:
          type: string
          description: |
            The record's own state, independent of its signer's standing.

            | Value | Meaning |
            |---|---|
            | `active` | The record stands. |
            | `revoked` | The record has been revoked. Read `revocation`, which is present when a reason was recorded. |
          enum: [active, revoked]
        recordIssuedAt:
          type: string
          format: date-time
          description: |
            The record's Hedera consensus timestamp — when the network agreed
            this record existed, not when a server thought so. RFC 3339, UTC.
        signerType:
          type: string
          description: |
            Who this record's signature attributes to. Always present.

            | Value | Meaning | What to do |
            |---|---|---|
            | `user` | A person signed, in sole control of their signing key. | Read `signerStanding` and `signerHint` to judge that person. |
            | `org` | An organisation's system key signed. No individual is named or accountable for this signature. | Accept only if a signature made under an organisation's authority satisfies your requirement. At the `organisation` tier, read `signerOrg` for which organisation. |

            Read this per record rather than inferring attribution from the
            top-level `verdict`. The verdict reduces across every match and a
            person's signature outranks an organisation's, so a hash carrying
            both answers `verified` and the organisation attribution appears
            only here.
          enum: [user, org]
        signerStanding: { $ref: '#/components/schemas/SignerStanding' }
        signerHint:
          type: string
          maxLength: 255
          description: |
            A free-text label the signer chose to publish alongside the record —
            a firm name, for instance. Up to 255 characters. Present only when
            the signer published one.

            Opt-in and self-asserted: Audrie does not validate it. Treat it as a
            display hint, never as proof of identity. It is not `signerOrg`, and
            neither is derived from the other.
        signerOrg:
          type: string
          maxLength: 255
          description: |
            The registered name of the organisation whose system key signed this
            record. Up to 255 characters. **`organisation` tier only**, and only
            on a match whose `signerType` is `org`.

            Audrie asserts this value from the organisation's validated
            registration; the signer does not choose it. That is the difference
            from `signerHint`.

            Present while the organisation's registration remains validated.
            Absent on every match below the `organisation` tier. Absent on a
            match whose `signerType` is `user`, at every tier: a person's record
            names no organisation, even when that person belongs to one. Absent
            when the organisation's validation is pending or has been withdrawn
            — read `signerStanding` — and absent when `signerStanding` is
            `indeterminate`.

            A registration can belong to a sole trader, so this value can be a
            person's legal name. It is disclosed to validated organisations only;
            handle it as personal data where that applies.
        revocation: { $ref: '#/components/schemas/Revocation' }
        checksPerformed: { $ref: '#/components/schemas/ChecksPerformed' }
        anchor: { $ref: '#/components/schemas/Anchor' }

    VerificationRequest:
      type: object
      description: The document hash to verify, and an optional identifier of your own.
      required: [originalDocHash]
      properties:
        originalDocHash:
          allOf: [{ $ref: '#/components/schemas/DocHash' }]
          description: |
            SHA-256 of the original document, before any Audrie certificate page
            was attached. Compute it yourself — `sha256sum contract.pdf` — and
            prefix it with `sha256:`.
        reference:
          type: string
          maxLength: 128
          description: |
            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.

    Verification:
      type: object
      description: |
        A verdict, and the records behind it. Which fields are populated depends
        on `disclosureTier`.
      required: [verdict, disclosureTier, assuranceLevel, mode, originalDocHash, verifiedAt, matches]
      properties:
        verificationId:
          type: string
          format: uuid
          description: |
            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.
        verdict: { $ref: '#/components/schemas/Verdict' }
        disclosureTier: { $ref: '#/components/schemas/DisclosureTier' }
        assuranceLevel: { $ref: '#/components/schemas/AssuranceLevel' }
        mode:
          type: string
          description: |
            What the verdict was computed over. `partial` means it was computed
            from the document hash you submitted. Read the field rather than
            hardcoding the value.
          enum: [partial]
        environment:
          type: string
          description: |
            The 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.
          enum: [live, test]
        originalDocHash: { $ref: '#/components/schemas/DocHash' }
        verifiedAt:
          type: string
          format: date-time
          description: When Audrie computed this verdict. RFC 3339 timestamp, UTC.
        reference:
          type: string
          description: |
            Echoed verbatim from the request. **`organisation` tier only.**
        matches:
          type: array
          description: |
            Anchored records matching the hash, ordered by anchor time, earliest
            first. Empty for `no_record` and `pending_anchor`.

            Returns at most 25 records, and `verdict` is computed over the
            records returned. A hash reaches 25 only when many parties
            independently authenticate identical bytes; if that is your use case,
            contact us.
          items: { $ref: '#/components/schemas/Match' }
        retryAfterSeconds:
          type: integer
          description: |
            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.

    VerificationSummary:
      type: object
      description: |
        One row of the reconciliation log — the verdict as it was recorded.
        `verdict` is the field to reconcile on.

        Rows carry no matches: matches are derived from the document hash rather
        than stored. To get matches for a hash, `POST` it.
      required: [verificationId, verdict, assuranceLevel, originalDocHash, verifiedAt]
      properties:
        verificationId:
          type: string
          format: uuid
          description: Stable id for this verdict. A UUID. Pass it to read the verdict back.
        verdict: { $ref: '#/components/schemas/Verdict' }
        assuranceLevel: { $ref: '#/components/schemas/AssuranceLevel' }
        mode:
          type: string
          description: What the verdict was computed over. See `mode` on a verification.
          enum: [partial]
        environment:
          type: string
          description: The environment of the key that produced this verdict — `live` or `test`.
          enum: [live, test]
        originalDocHash: { $ref: '#/components/schemas/DocHash' }
        verifiedAt:
          type: string
          format: date-time
          description: When Audrie computed this verdict. RFC 3339 timestamp, UTC.
        reference:
          type: string
          description: |
            The identifier you sent with the verification. Present when you sent
            one.

    Pagination:
      type: object
      description: |
        Where you are in the log. See
        [Pagination](https://audrie.io/docs/pagination).
      required: [hasMore]
      properties:
        nextCursor:
          type: string
          description: |
            Pass verbatim as `after` to fetch the next page. Absent on the last
            page. Opaque — do not parse or construct one.
        hasMore:
          type: boolean
          description: |
            Whether another page follows. Stop paging when this is `false`; do
            not stop on an empty `data` array alone.

    Error:
      type: object
      description: |
        The canonical error envelope, identical across every Audrie API. Branch on
        `error.code`, never on `error.message`. See
        [Errors](https://audrie.io/docs/errors).
      required: [error]
      properties:
        error:
          type: object
          description: The error.
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable, machine-readable identifier for the failure. Branch on this.
              examples: [VALIDATION_FAILED]
            message:
              type: string
              description: Human-readable explanation. Written for a person and changes without notice.
            details:
              type: object
              additionalProperties: { type: string }
              description: |
                One message per offending field, keyed by field name. Present on
                a validation failure.

  responses:

    RecordNotRevocable:
      description: |
        The record is not in a revocable state: already revoked, or not yet at
        consensus. These are not distinguished. Retrying helps only in the
        second case — see the operation's **Errors** note.

        `error.code` is `CONFLICT`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

    ValidationFailed:
      description: |
        The request is malformed. Read `error.details` for the offending field
        and fix the request.

        `error.code` is `VALIDATION_FAILED`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: VALIDATION_FAILED
              message: Validation failed
              details:
                originalDocHash: must match sha256:<64 hex chars>

    Unauthenticated:
      description: |
        The credential you sent is not usable, or the operation requires one and
        you sent none. Which of several causes made a supplied credential
        unusable is not distinguished.

        A credential that is present and does not work is refused rather than
        quietly downgraded to the `public` tier.

        `error.code` is `UNAUTHENTICATED`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: UNAUTHENTICATED
              message: Invalid API credential

    PaymentRequired:
      description: |
        API access is not enabled for this organisation. On
        `POST /v1/verifications` you can drop the `Authorization` header and keep
        verifying at the `public` tier; the two read operations require a
        credential.

        `error.code` is `PAYMENT_REQUIRED`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: PAYMENT_REQUIRED
              message: API access is not enabled for this organisation

    Forbidden:
      description: |
        The credential is valid but not permitted to perform this call. Read
        `error.message`: it names which of the two causes applied.

        **Call this API from your server.** An organisation-wide key in frontend
        JavaScript is readable by every visitor to your site, and a key that has
        been served to a browser should be treated as leaked and revoked. See
        [API keys](https://audrie.io/docs/authentication).

        While your organisation is being validated, `POST /v1/verifications`
        succeeds at the `public` tier and the two read operations return this
        status. Your verifications are recorded throughout and appear in your log
        once validation completes.

        `error.code` is `FORBIDDEN`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            scope:
              summary: The key lacks the required scope
              value:
                error:
                  code: FORBIDDEN
                  message: Credential lacks the verifications:read scope
            browser:
              summary: The request came from a browser
              value:
                error:
                  code: FORBIDDEN
                  message: API credentials must not be used from a browser

    RecordsAddonRequired:
      description: |
        An entitlement is missing. **Two causes reach this status and they are
        NOT machine-distinguishable in this version** — both carry
        `PAYMENT_REQUIRED`, and `error.message` changes without notice, so do not
        branch on it. The two are:

        - *"Organisation-issued document authentication is not enabled"* — the
          add-on. This is a **separate entitlement from API access**: the same
          credential can verify successfully and be refused here.
        - *"API access is not enabled for this organisation"* — the base API
          entitlement, which every operation on this API requires.

        Neither is about the credential. Do not rotate the key; both are billing
        changes, and checking your subscription tells you which applies.

        `error.code` is `PAYMENT_REQUIRED`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: PAYMENT_REQUIRED
              message: Organisation-issued document authentication is not enabled for this organisation

    RecordsForbidden:
      description: |
        The credential is valid but not permitted to authenticate documents.
        **Branch on `error.code`** — the two causes need different responses.

        `ORG_NOT_VALIDATED` means your organisation has not finished validation.
        Validation is a review carried out by Audrie, so the action is to wait,
        not to change anything. Your key is fine, and verification keeps working
        throughout. This code is specific to your ORGANISATION's validation and
        is distinct from any code about an individual's identity check.

        `FORBIDDEN` means one of: the credential lacks the `records:write` scope —
        which is what you will normally see for a **`test`** credential, since
        that scope is never granted to one — the request carried an `Origin`
        header (call this API from your server, never from a browser), or your
        organisation is not in a state that permits authenticating documents.
        Read `error.message`.

        See [Errors](https://audrie.io/docs/errors), and the `apiKey` security
        scheme for how `records:write` is granted at mint.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            notValidated:
              summary: The organisation is still being validated
              value:
                error:
                  code: ORG_NOT_VALIDATED
                  message: Your organisation is still being validated. Document authentication is available once validation completes; verification works throughout.
            scope:
              summary: The key cannot write records
              value:
                error:
                  code: FORBIDDEN
                  message: Credential lacks the records:write scope

    RecordsUnavailable:
      description: |
        Audrie could not complete the authentication. **Three causes reach this
        status and they are NOT machine-distinguishable** — all carry
        `SERVICE_UNAVAILABLE`, and `error.message` changes without notice, so do
        not branch on it. They do not want the same response, which is why all
        three are described:

        - **The ledger was briefly unavailable.** Retry with backoff. **This is
          the one place on this API where a retry may find work already done:**
          a record may have been created before the failure, and a retry returns
          it with `200` rather than creating a second. See the operation's
          Idempotency section.
        - **Your organisation has no signing key.** **Retrying will not help** —
          this status is reserved for a missing key, not for a momentary failure
          reaching one, which answers `500` instead. No record was created: the
          key is resolved before anything is written.
        - **Your organisation is not provisioned for anchoring.** **Retrying will
          not help.** No record was created, for the same reason.

        Only the first is worth persistent retrying, and only the first can have
        created anything. **Retry a few times with backoff, then stop and report
        it** rather than looping: two of the three do not clear by themselves,
        and neither of those left anything to reconcile against.

        None of the three is a statement about your credential or your
        entitlement — all are fine. Never rotate an API credential in response to
        this: where the message mentions a signing key, that is your
        organisation's document-signing key, not the credential you sent.

        `error.code` is `SERVICE_UNAVAILABLE`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: SERVICE_UNAVAILABLE
              message: The ledger is temporarily unavailable. Retry; if a record was already created for this document it is returned rather than duplicated.

    NotFound:
      description: |
        No such resource for this organisation and environment. Check the id;
        see the operation's own **Errors** note for what this status does and
        does not distinguish.

        `error.code` is `NOT_FOUND`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: NOT_FOUND
              message: Verification not found

    PayloadTooLarge:
      description: |
        The request body is too large. The largest legal body is a 71-character
        hash and a 128-character `reference`, so this means something other than
        a verification request was sent.

        `error.code` is `PAYLOAD_TOO_LARGE`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: PAYLOAD_TOO_LARGE
              message: Request body too large

    RateLimited:
      description: |
        Rate limit exceeded. Wait `Retry-After` seconds and retry.

        `error.code` is `RATE_LIMITED`. See
        [Errors](https://audrie.io/docs/errors) for the limits and the headers
        that report your remaining budget.
      headers:
        Retry-After:            { $ref: '#/components/headers/Retry-After' }
        X-RateLimit-Limit:      { $ref: '#/components/headers/X-RateLimit-Limit' }
        X-RateLimit-Remaining:  { $ref: '#/components/headers/X-RateLimit-Remaining' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: RATE_LIMITED
              message: Too many requests, please retry later

    InternalError:
      description: |
        Something failed on our side. Retry with backoff.

        Retrying is safe on every verification operation: none of them modifies a
        record. A retried credentialed verification is recorded again in your log
        under a new `verificationId`, so reconcile on `verificationId` rather
        than assuming one row per attempt.

        Retrying `POST /v1/records` is also safe — the document hash is its
        idempotency key.

        `error.code` is `INTERNAL_ERROR`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: INTERNAL_ERROR
              message: An internal error occurred

    ServiceUnavailable:
      description: |
        Audrie could not answer this request. Retry with backoff; no record is
        modified.

        This is not a statement about your credential. It is still valid, and
        nothing about your organisation has changed. A `401` means stop and fix
        your credential; a `503` means retry. Never rotate a key in response to
        this.

        `error.code` is `SERVICE_UNAVAILABLE`. See
        [Errors](https://audrie.io/docs/errors).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error:
              code: SERVICE_UNAVAILABLE
              message: Service temporarily unavailable
