# Verify a Receipt (/docs/guides/verify-a-receipt)



A <Term name="Receipt" /> is the artifact you hand an auditor, a customer, or a
counterparty when they ask *what exactly happened, and what did it cost*. This
guide is the counterparty's side: given a receipt, prove it is authentic —
**offline**, with nothing but the receipt bytes and Ductor's published public
keys.

<ReceiptSpecimen />

## What you are verifying [#what-you-are-verifying]

The signed unit is a canonical byte string — a versioned, deterministic
serialization of the receipt's envelope. Three design choices make offline
verification actually mean something:

<FactGrid>
  <Fact label="Everything that matters is inside the signature">
    The algorithm and signing-key id are bound into the signed bytes under a domain-separation constant, so a signature can never be replayed onto different claims, a different scheme, or a different system's payloads.
  </Fact>

  <Fact label="Lifecycle state is outside it">
    Whether a receipt has settled is derived from immutable facts, never written into signed content — so a receipt settling later cannot disturb a signature you already verified.
  </Fact>

  <Fact label="Stored bytes are the truth">
    Verification runs against the exact canonical bytes that were signed, persisted alongside the receipt. You never re-serialize and hope your JSON matches.
  </Fact>
</FactGrid>

## The procedure [#the-procedure]

The whole path, and the two places a forged or altered receipt is caught:

```mermaid
flowchart TD
  R["Receipt<br/>envelope + signed bytes"] --> K{"key_id known<br/>in JWKS?"}
  K -->|no| F1["FAIL<br/>unknown key"]
  K -->|yes| W{"key valid at<br/>issued_at?"}
  W -->|no| F2["FAIL<br/>outside window"]
  W -->|yes| H["Recompute payload hash<br/>over stored canonical bytes"]
  H --> V{"Ed25519 verify<br/>detached signature"}
  V -->|"bytes altered"| F3["FAIL<br/>tampered"]
  V -->|match| OK["AUTHENTIC<br/>claims are as signed"]
```

Every failure edge is terminal: there is no fallback key, no re-serialization
attempt, and no partial pass.

<Steps>
  <Step>
    **Fetch the published keys.** Ductor serves its receipt-signing public keys as
    a standard JWKS document — Ed25519 keys (`OKP`/`EdDSA`), each with a key id and
    a validity window:

    ```bash
    curl -s https://api.ductor.io/.well-known/work-receipts/jwks.json
    ```

    Cache it. Everything after this step is offline.
  </Step>

  <Step>
    **Select the key.** The receipt's `key_id` names the key that signed it. An
    unknown key id fails verification — no fallback, no "try them all".
  </Step>

  <Step>
    **Check the window against issuance.** The key must have been valid *when the
    receipt was issued* — `issued_at` inside `[not_before, expires_at)`. Retiring a
    key never invalidates the receipts it legitimately signed; a revoked key
    disappears from the JWKS entirely.
  </Step>

  <Step>
    **Recompute the hash, then verify the signature.** SHA-256 over the canonical
    bytes must equal the receipt's `payload_hash` — the independent tamper check —
    and the Ed25519 signature must verify over those same bytes with the selected
    public key.

    ```go
    sum := sha256.Sum256(canonical)
    if hex.EncodeToString(sum[:]) != receipt.PayloadHash {
        return Tampered
    }
    ok := ed25519.Verify(publicKey, canonical, signature)
    ```
  </Step>
</Steps>

The verdict is structured, not boolean — each check answers separately:

<TypeTable
  type="{
  key_found: { type: &#x22;bool&#x22;, description: &#x22;The receipt's key_id exists in the key set.&#x22; },
  within_window: { type: &#x22;bool&#x22;, description: &#x22;The key was valid at issuance time.&#x22; },
  hash_match: { type: &#x22;bool&#x22;, description: &#x22;The canonical bytes hash to payload_hash — the tamper check.&#x22; },
  signature_valid: { type: &#x22;bool&#x22;, description: &#x22;Ed25519 verifies over the canonical bytes.&#x22; },
}"
/>

A hosted second opinion exists — `VerifyWorkReceipt` on the API, and the
governed `work_receipt.verify` tool for agents — running exactly this
procedure. It is a convenience, not a dependency.

## Reading what you verified [#reading-what-you-verified]

A verified receipt is a set of claims, each anchored to deeper evidence:

* **`worker`** identifies who did the work — including, for an agent, the
  pinned definition (id, version, hash) it executed under. For non-assignment
  outcomes — a rejection, a drop — `worker` is explicitly `null`, and that is a
  *valid* receipt: proof of what didn't happen is still proof.
* **`route`** carries the decision id and a digest of its explanation — the
  snapshotted *why*, not a live reconstruction.
* **`tools`** carries the content hash of the exposure manifest the agent acted
  under, joining the per-invocation receipts from the governed tool surface.
* **`cost`** is exact: `int64` micro-units, with `total` equal to the sum of
  its breakdown by construction.
* **`settlement`** references the journal transactions that moved the money —
  arriving at issuance when settlement already committed, or later as a
  **signed amendment** that never mutates the original.

Amendments chain: a claw-back, a late settlement, each is a new signed receipt
referencing its predecessor, and the chain reads newest-last with the original
intact. Verify each link the same way.

<Fact label="Shared receipts">
  A receipt shared outside the tenant is a distinct, re-signed **redacted**
  object — issued under a revocable, expiring grant, resolved by an opaque
  high-entropy token. It verifies on its own signature. The full receipt is
  never public, and a redacted receipt is never a blanked copy of it.
</Fact>

## Where to go deeper [#where-to-go-deeper]

<Cards>
  <Card title="Receipt" href="/docs/primitives/receipt">
    The primitive — what a receipt is and why it exists.
  </Card>

  <Card title="Agent tool security" href="/docs/ai/agent-tool-security">
    The signed manifests and exposure receipts a receipt joins.
  </Card>

  <Card title="The settlement journal" href="/docs/billing/settlement-journal">
    The balanced money movement a receipt's settlement section references.
  </Card>
</Cards>
