# Emissar Receipt v0.1 Retrieval API, a draft for comment. Prose, normative rules and rationale: https://emissar.ai/specs/receipt/v0.1 (section 11).
# Schemas live in https://emissar.ai/specs/schemas/receipt-v0.1.schema.json and are referenced, not copied.
openapi: 3.1.0
info:
  title: Emissar Receipt Retrieval API
  version: 0.1.0-draft
  summary: Authenticated retrieval, countersigning and chain inspection for Emissar Receipts.
  description: |
    A draft for comment, published 2026-10-05. Emissar Front Door implements this API for the businesses it serves.

    Every A2A server that issues Emissar Receipts (or a host acting for it, such as an Emissar Front Door)
    serves this API at the `receiptApi` URL it declares in its Agent Card extension entry
    (`https://emissar.ai/ext/receipt/v1`). The base URL below is a placeholder.

    Only the two parties to a receipt may call these operations. The caller authenticates with the same
    security scheme it used for the A2A task (OAuth 2.0 bearer token or mutual TLS). The server maps the
    caller to a party by comparing the authenticated identity with `parties.client.authentication` (or to
    itself for `parties.server`). A server MAY also accept HTTP Message Signatures (RFC 9421) made with the
    key in `parties.client.signingKey`; OpenAPI has no security scheme type for that, so it is described
    here only.

    Receipts are not public. Anyone who holds a receipt document can verify it offline (spec section 12)
    without this API.

    Errors use RFC 9457 problem details. Problem `type` values are identifiers under
    `https://emissar.ai/ext/receipt/v1/problems/`; they are not expected to resolve.
servers:
  - url: https://freight.example.net/receipts/v0
    description: Illustrative issuer host (the receiptApi value from the demo). Not deployed.
security:
  - oauth2: []
  - mutualTLS: []
tags:
  - name: receipts
  - name: chains
paths:
  /receipts:
    get:
      tags: [receipts]
      operationId: listReceipts
      summary: List receipts where the caller is a party
      description: Newest first by issuedAt. Filters combine with AND.
      parameters:
        - name: taskId
          in: query
          schema: { type: string, minLength: 1, maxLength: 256 }
        - name: contextId
          in: query
          schema: { type: string, minLength: 1, maxLength: 256 }
        - name: issuedAfter
          in: query
          schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Timestamp" }
        - name: status
          in: query
          schema: { enum: [issued, countersigned, refused, expired] }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: cursor
          in: query
          description: The `next` value from the previous page.
          schema: { type: string, minLength: 1, maxLength: 512 }
      responses:
        "200":
          description: A page of receipt summaries.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            application/json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/ReceiptList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /receipts/{receiptId}:
    parameters:
      - $ref: "#/components/parameters/ReceiptId"
    get:
      tags: [receipts]
      operationId: getReceipt
      summary: Get a receipt document
      description: |
        Returns the current document: the payload, every signature collected so far and any anchors. The
        payload never changes after issue; signatures and anchors are only ever added, so the ETag changes
        when they are. The response carries the receipt digest in `Emissar-Receipt-Digest` so a caller can
        compare it with the digest it already holds before parsing.
      responses:
        "200":
          description: The receipt document.
          headers:
            ETag: { schema: { type: string } }
            Cache-Control: { $ref: "#/components/headers/NoStore" }
            Emissar-Receipt-Digest:
              description: The receipt digest (spec section 6.3) as an ni URI.
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Digest" }
          content:
            application/emissar-receipt+json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/ReceiptDocument" }
        "304": { description: Not modified (If-None-Match matched). }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotAParty" }
        "404": { $ref: "#/components/responses/NotFound" }
        "410": { $ref: "#/components/responses/Deleted" }
  /receipts/{receiptId}/signatures:
    parameters:
      - $ref: "#/components/parameters/ReceiptId"
    post:
      tags: [receipts]
      operationId: countersignReceipt
      summary: Submit the client's countersignature
      description: |
        The client posts one signature entry (spec section 7.5). The server checks that the protected
        header names role `client` and the kid in `parties.client.signingKey`, that the key's RFC 7638
        thumbprint equals `parties.client.signingKey.jkt`, that the signature verifies over the stored
        payload, and that the countersign window is open. It then appends the entry and returns the
        updated document. Repeating an identical request returns 200 with the same document.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Signature" }
      responses:
        "200":
          description: Countersignature accepted; the updated receipt document.
          content:
            application/emissar-receipt+json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/ReceiptDocument" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotAParty" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: A different client signature or a refusal is already recorded.
          content:
            application/problem+json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
        "410":
          description: The countersign window closed (timestamps.countersignBy has passed).
          content:
            application/problem+json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
        "422":
          description: The signature, header, key or thumbprint check failed.
          content:
            application/problem+json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
  /receipts/{receiptId}/refusals:
    parameters:
      - $ref: "#/components/parameters/ReceiptId"
    post:
      tags: [receipts]
      operationId: refuseReceipt
      summary: Record the client's signed refusal
      description: |
        The client states that it will not countersign and why (spec section 7.6). The server verifies the
        refusal signature with the client key, stores the refusal with the receipt and returns it from
        getReceiptRefusal. A refusal closes the countersign window.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Refusal" }
      responses:
        "201": { description: Refusal recorded. }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotAParty" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: The receipt is already countersigned or refused.
          content:
            application/problem+json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
        "422":
          description: The refusal does not verify or names a different receipt digest.
          content:
            application/problem+json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
    get:
      tags: [receipts]
      operationId: getReceiptRefusal
      summary: Get the recorded refusal, if any
      responses:
        "200":
          description: The signed refusal.
          content:
            application/json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Refusal" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotAParty" }
        "404": { $ref: "#/components/responses/NotFound" }
  /receipts/{receiptId}/openings:
    parameters:
      - $ref: "#/components/parameters/ReceiptId"
    get:
      tags: [receipts]
      operationId: getOpenings
      summary: Get the salts that open the receipt's commitments
      description: |
        Parties only. After content retention ends the server deletes the salts (spec section 13) and
        returns 410; the receipt itself stays retrievable.
      responses:
        "200":
          description: The openings.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            application/json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Openings" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotAParty" }
        "404": { $ref: "#/components/responses/NotFound" }
        "410": { $ref: "#/components/responses/Deleted" }
  /chains/{kid}:
    parameters:
      - $ref: "#/components/parameters/Kid"
    get:
      tags: [chains]
      operationId: getChainHead
      summary: Get the latest entry of one of the server's signing-key chains
      description: |
        Returns the entry for the highest seq signed with this key. Callers that hold a receipt signed
        with the key can compare heads to detect a rewritten or forked chain (spec section 8.3). Any
        authenticated counterparty may call it; it reveals only digests, counts and times.
      responses:
        "200":
          description: The latest chain entry.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            application/json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/ChainEntry" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "404": { $ref: "#/components/responses/NotFound" }
  /chains/{kid}/entries/{seq}:
    parameters:
      - $ref: "#/components/parameters/Kid"
      - name: seq
        in: path
        required: true
        schema: { type: integer, minimum: 1, maximum: 9007199254740991 }
    get:
      tags: [chains]
      operationId: getChainEntry
      summary: Get one chain entry by sequence number
      description: |
        Chain entries outlive receipt documents: after a document is deleted the entry remains with
        `documentDeleted: true` so the chain still verifies (spec section 13).
      responses:
        "200":
          description: The chain entry.
          content:
            application/json:
              schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/ChainEntry" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "404": { $ref: "#/components/responses/NotFound" }
components:
  securitySchemes:
    oauth2:
      type: oauth2
      description: The same OAuth 2.0 authorization server and client registration the client uses for A2A.
      flows:
        clientCredentials:
          tokenUrl: https://freight.example.net/oauth/token
          scopes:
            receipts.read: Read receipts, openings and chain entries where the caller is a party
            receipts.sign: Submit countersignatures and refusals
    mutualTLS:
      type: mutualTLS
      description: TLS client certificate; the certificate subject is compared with parties.client.authentication.subject.
  parameters:
    ReceiptId:
      name: receiptId
      in: path
      required: true
      schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Uuid" }
    Kid:
      name: kid
      in: path
      required: true
      description: Percent-encoded key ID.
      schema: { type: string, minLength: 1, maxLength: 256 }
  headers:
    NoStore:
      description: Receipts and openings are private to the parties; shared caches must not keep them.
      schema: { type: string, const: "private, no-store" }
  responses:
    BadRequest:
      description: The request is malformed or fails schema validation.
      content:
        application/problem+json:
          schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
    Unauthenticated:
      description: Missing or invalid credentials.
      content:
        application/problem+json:
          schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
    NotAParty:
      description: |
        The caller is authenticated but is not a party to this receipt. Servers MAY return 404 instead,
        so that callers cannot learn which receipt IDs exist.
      content:
        application/problem+json:
          schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
    NotFound:
      description: No such receipt, chain or entry (or the caller may not know it exists).
      content:
        application/problem+json:
          schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
    Deleted:
      description: The resource existed and was deleted under the issuer's retention policy.
      content:
        application/problem+json:
          schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
    TooManyRequests:
      description: Rate limited; honor Retry-After.
      headers:
        Retry-After: { schema: { type: integer, minimum: 1 } }
      content:
        application/problem+json:
          schema: { $ref: "../schemas/receipt-v0.1.schema.json#/$defs/Problem" }
