openapi: 3.1.0
info:
  title: Zceipt API
  version: "2026-10-14"
  summary: Prove one shielded Zcash payment, reveal nothing else.
  description: |
    Self-hostable REST API for Zcash payment receipts (ZIP 311 for Orchard and Ironwood).

    - **Verify** any receipt, a batch of receipts, or a receipt against a payee, an amount and a challenge.
    - **Proof requests** ask a payer to prove a payment. Each one comes with a ZIP-321 payment URI, a verifier link and the CLI command that answers it.
    - **Webhooks** are signed `request.fulfilled` / `request.created` / `receipt.rejected` events, retried with backoff.

    Authentication works like Stripe's. `rcpt_test_…` keys serve testnet (or a local regtest chain) and `rcpt_live_…` keys serve mainnet. Objects never cross modes.
    Send `Authorization: Bearer <key>`. Creating `POST`s accept an `Idempotency-Key` header. A replay returns the original response with `Idempotent-Replayed: true`.
    Every error has the same shape and a stable `code`. `GET /v1/errors` lists them all.
servers:
  - url: http://localhost:8723
    description: zceipt serve (local)
security:
  - bearer: []
tags:
  - name: Verification
  - name: Proof requests
  - name: Sign in with Zcash
  - name: Claims
  - name: Webhooks
  - name: Meta
paths:
  /v1/verify:
    post:
      tags: [Verification]
      operationId: verify
      summary: Verify a receipt or a batch
      description: |
        Verifies the receipt's zero-knowledge proof and signature against the mined transaction, and opens the disclosed outputs.
        If `receipt` is a `zip311b1…` batch, returns a `batch_verification`. Optional checks: `expect_address` and `min_zatoshis`, and `challenge`.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [receipt]
              properties:
                receipt: { type: string, description: "A zip311r1… receipt, a zip311b1… batch, or a link containing one." }
                expect_address: { type: string, description: "Fail with wrong_payee unless a disclosed output paid this unified address." }
                min_zatoshis: { type: integer, minimum: 0, description: "With expect_address: fail with insufficient_amount unless at least this much was paid." }
                challenge: { type: string, description: "Fail with challenge_mismatch unless the receipt signs challenge:<this>." }
            example: { receipt: "zip311r1WlJDVAEB…", expect_address: "utest1…", min_zatoshis: 1000000 }
      responses:
        "200":
          description: Valid
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/Verification"
                  - $ref: "#/components/schemas/BatchVerification"
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "422": { $ref: "#/components/responses/Error" }
        "502": { $ref: "#/components/responses/Error" }
  /v1/sandbox/fixtures:
    get:
      tags: [Meta]
      operationId: listSandboxFixtures
      summary: List the sandbox receipts (zip311_test_*) and their outcomes
      security: []
      responses:
        "200": { description: List of fixture names }
  /v1/deliveries:
    get:
      tags: [Webhooks]
      operationId: listDeliveries
      summary: Webhook delivery attempts (status, attempts, last HTTP status, last error)
      responses:
        "200": { description: List of deliveries }
  /v1/requests/{id}/qr.svg:
    get:
      tags: [Proof requests]
      operationId: requestQr
      summary: The request's ZIP-321 payment URI as an SVG QR code
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: SVG image, content: { image/svg+xml: {} } }
        "404": { $ref: "#/components/responses/Error" }
  /v1/payment_links/check:
    post:
      tags: [Proof requests]
      operationId: checkPaymentLink
      summary: Lint a ZIP-321 payment link before printing it
      description: |
        Parses a `zcash:` URI and reports, per payment, the network, address kind and receivers, amount, memo and whether a
        receipt can prove a payment to it, plus the QR version it needs. `findings` lists errors (wrong network, unknown
        required params, invalid URI), warnings (transparent, TEX or Sapling-only recipients, missing amount, dense QR) and tips.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [uri]
              properties:
                uri: { type: string }
                network: { type: string, enum: [mainnet, testnet, regtest], description: "Default: the key's network." }
      responses:
        "200": { description: The check; `ok` is false if any finding is an error }
        "400": { $ref: "#/components/responses/Error" }
  /v1/requests/{id}/attestations:
    post:
      tags: [Proof requests]
      operationId: createAttestation
      summary: Sign a minimal business fact about a fulfilled request
      description: |
        Returns an Ed25519 SD-JWT stating `paid_at_least_zatoshis`, `paid_on_or_before` (a day), `network` and `final`.
        The txid, exact amount, exact time, request id and label are selectively disclosable (`disclosures`).
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                at_least_zatoshis: { type: integer, minimum: 0 }
                before: { type: string, format: date }
                disclose: { type: array, items: { type: string, enum: [txid, amount_zatoshis, paid_at, request, label] } }
                audience: { type: string, maxLength: 200, description: "Who the attestation is for (aud). A verifier that names its audience refuses one made for another, or for nobody." }
                valid_for_days: { type: integer, minimum: 1, maximum: 3650, description: "Days until it expires (exp). Default: no expiry." }
      responses:
        "200": { description: Attestation }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
  /v1/attestations/verify:
    post:
      tags: [Proof requests]
      operationId: verifyAttestation
      summary: Verify an attestation this server issued
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [attestation], properties: { attestation: { type: string } } }
      responses:
        "200": { description: Valid, with the stated and revealed claims }
        "422": { $ref: "#/components/responses/Error" }
  /.well-known/jwks.json:
    get:
      tags: [Meta]
      operationId: jwks
      summary: Public keys for attestations
      security: []
      responses:
        "200": { description: JWK set (Ed25519) }
  /v1/requests/{id}/review:
    post:
      operationId: reviewDeposit
      summary: Accept or reject a deposit held for review (deposit desks)
      description: "A request created with source_of_funds whose deposit's funding failed screening (a blocked address, or funding the payer didn't show) is held_for_review. accept credits it (status fulfilled, request.fulfilled); reject sets rejected (request.rejected), after which it can be refunded to the payer's signed refund address."
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision: { type: string, enum: [accept, reject] }
                note: { type: string, maxLength: 500 }
      responses:
        "200": { description: The request after review }
        "409": { description: "not_held_for_review" }
  /v1/requests/{id}/refunds:
    post:
      tags: [Proof requests]
      operationId: createRefund
      summary: Refund a payment to the payer's signed refund address
      description: |
        Only works if the payer signed `refund-to:<address>` into their receipt (`zceipt create --refund-to`).
        Returns a ZIP-321 `payment_uri` to that address and a `challenge` for the refund receipt. Emits `refund.created`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                amount_zatoshis: { type: integer, minimum: 1, description: "Default: what is left of the payment." }
                reason: { type: string, maxLength: 200 }
                payment_txid: { type: string, description: "Which payment to refund. Default: the latest one with a refund address." }
      responses:
        "201": { description: Refund created }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
  /v1/refunds/{id}:
    get:
      tags: [Proof requests]
      operationId: getRefund
      summary: Retrieve a refund
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Refund }
        "404": { $ref: "#/components/responses/Error" }
  /v1/refunds/{id}/receipts:
    post:
      tags: [Proof requests]
      operationId: submitRefundReceipt
      summary: Settle a refund with a receipt for the refund payment
      description: The receipt must sign `challenge:<refund challenge>` and pay the refund address at least `amount_zatoshis`. Emits `refund.succeeded`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [receipt], properties: { receipt: { type: string } } }
      responses:
        "200": { description: Refund succeeded }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "422": { $ref: "#/components/responses/Error" }
  /v1/test_clocks:
    post:
      tags: [Meta]
      operationId: createTestClock
      summary: "Sandbox: create a test clock"
      description: A frozen time and block height that test-mode requests can run on (`test_clock` on create). Test keys only.
      requestBody:
        content:
          application/json:
            schema: { type: object, properties: { frozen_time: { type: integer }, height: { type: integer, minimum: 1 } } }
      responses:
        "201": { description: Test clock }
        "400": { $ref: "#/components/responses/Error" }
  /v1/test_clocks/{id}:
    get:
      tags: [Meta]
      operationId: getTestClock
      summary: "Sandbox: retrieve a test clock"
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Test clock }
        "404": { $ref: "#/components/responses/Error" }
  /v1/test_clocks/{id}/advance:
    post:
      tags: [Meta]
      operationId: advanceTestClock
      summary: "Sandbox: move a test clock and fire what became due"
      description: Moves time and height, then fires `request.expired` and `request.final` for requests on the clock. The response lists them in `fired`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, properties: { seconds: { type: integer, minimum: 0 }, blocks: { type: integer, minimum: 0 } } }
      responses:
        "200": { description: Test clock with fired events }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
  /v1/test_clocks/{id}/reorg:
    post:
      tags: [Meta]
      operationId: reorgTestClock
      summary: "Sandbox: roll a test clock's chain back"
      description: Payments mined in the dropped blocks (and not yet final) fire `request.payment_reorged` and lose their confirmations.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        content:
          application/json:
            schema: { type: object, properties: { depth: { type: integer, minimum: 1, maximum: 100 } } }
      responses:
        "200": { description: Test clock with fired events }
        "404": { $ref: "#/components/responses/Error" }
  /v1/sign_in/challenges:
    post:
      tags: [Sign in with Zcash]
      operationId: createSignInChallenge
      summary: Issue a single-use sign-in challenge
      description: |
        Returns a nonce bound to `domain` that expires after 5 minutes. The user's wallet answers with
        `zceipt sign-in --domain <domain> --nonce <nonce>`, which proves control of a shielded address
        with a Halo 2 proof and a RedPallas signature. No transaction and no fee.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                domain: { type: string, description: "Host name the sign-in is for. Default: this API's public host." }
            example: { domain: "kemis-coffee.test" }
      responses:
        "200":
          description: Challenge
          content:
            application/json:
              example: { object: sign_in_challenge, id: sic_016fcd1628de976b20b5eaeb, livemode: false, coin_type: 1, domain: kemis-coffee.test, nonce: 3586f998c75075f243d65a88, expires_at: 1792014583, status: pending, wallet_command: "zceipt sign-in --domain kemis-coffee.test --nonce 3586f998c75075f243d65a88" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
  /v1/sign_in/verify:
    post:
      tags: [Sign in with Zcash]
      operationId: verifySignIn
      summary: Verify a zsig1… sign-in and spend its challenge
      description: |
        Checks the proof and signature against the challenge's domain and nonce, then marks the challenge used.
        A sign-in made for another domain (phishing) or replayed is refused.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sign_in]
              properties:
                sign_in: { type: string, description: "The zsig1… text from the wallet." }
      responses:
        "200":
          description: Signed in
          content:
            application/json:
              example: { object: sign_in, verified: true, livemode: false, challenge: sic_016fcd1628de976b20b5eaeb, domain: kemis-coffee.test, issued_at: 1792014283, address: "utest1lrnzetdp2cuvg0hvnapcmy0emmhguv358a4flvzxv6j00yfar7qwyrwk22mrr285943d8k4k523pshzujke90w0g3zq9z4ehnsljs2x6" }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "422": { $ref: "#/components/responses/Error" }
  /v1/claims/verify:
    post:
      tags: [Claims]
      operationId: verifyClaim
      summary: Verify a zip311t1… claim or payer tag and return its tag
      description: |
        A claim shows a wallet held at least its floor at the snapshot block `height`; keep one claim per tag.
        A payer tag (no `height`) is on a payment's real spend; payments with different tags were made by different wallets.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [claim, domain]
              properties:
                claim: { type: string, description: "The zip311t1… text." }
                domain: { type: string, description: "Your airdrop's or round's name; a claim for another is refused." }
                height: { type: integer, description: "The snapshot block. Required for a claim, omitted for a payer tag." }
                list: { type: array, items: { type: string, description: "A note commitment, 64 hex digits" }, description: "For a claim against your own published list instead of the chain (the claimant's note is on it; its nullifier stays private). Give min_floor_zatoshis; omit height." }
                min_floor_zatoshis: { type: integer, minimum: 1, description: "The least a claim's note must have held. Required for a claim: the claimant picks the floor it shows, so a dust note proves \"at least 1 zatoshi\" unless you set this." }
      responses:
        "200":
          description: Valid
          content:
            application/json:
              example: { valid: true, kind: claim, network: test, pool: Ironwood, domain: airdrop-1, message: "", tag: f1771da7bc17af4c1393e2edafccbd1d34bd5a638ef50ddffca2f003a56cc936, floor_zatoshis: 10000000, floor_zec: "0.10000000", snapshot_height: 568 }
        "400": { $ref: "#/components/responses/Error" }
        "422": { $ref: "#/components/responses/Error" }
  /v1/requests:
    post:
      tags: [Proof requests]
      operationId: createRequest
      summary: Create a proof request
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [payee]
              properties:
                payee: { type: string, description: "Your unified address. The payment must reach it." }
                amount_zatoshis: { type: integer, minimum: 0, description: "Minimum amount the receipt must show paid to payee." }
                label: { type: string, maxLength: 200 }
                metadata: { type: object, additionalProperties: true, description: "Your own key/values, e.g. order_id." }
                expires_in: { type: integer, minimum: 1, description: "Seconds until the request expires." }
                public_min_contributions: { type: integer, minimum: 1, maximum: 1000, default: 3, description: "Campaigns: contributions needed before the public page shows a total" }
                source_of_funds:
                  type: object
                  description: "Deposit desks (single requests): only accept a receipt that proves where every spent note came from (zceipt create --prove-source; zceipt pay adds it), and screen the transparent addresses that funded it against blocked_addresses. Cleared: fulfilled. Otherwise: held_for_review until POST /v1/requests/{id}/review."
                  properties:
                    required: { const: true }
                    blocked_addresses: { type: array, items: { type: string }, maxItems: 10000, description: "Transparent addresses of this network; never shown publicly" }
                kind:
                  type: string
                  enum: [single, partial, campaign]
                  default: single
                  description: "single: one receipt must cover the amount. partial: receipts add up until the amount is reached. campaign: open-ended; every valid receipt counts and a public page shows the verified total (never the donors)."
            example: { payee: "utest1…", amount_zatoshis: 1000000, label: "Order #42", metadata: { order_id: "42" }, expires_in: 86400 }
      responses:
        "201":
          description: Created
          content: { application/json: { schema: { $ref: "#/components/schemas/ProofRequest" } } }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
    get:
      tags: [Proof requests]
      operationId: listRequests
      summary: List proof requests, newest first
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [open, fulfilled, held_for_review, rejected, expired] } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
        - { name: starting_after, in: query, schema: { type: string }, description: "Cursor: the last id of the previous page." }
      responses:
        "200":
          description: A page
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/ProofRequest" } }
                  has_more: { type: boolean }
  /v1/requests/{id}:
    get:
      tags: [Proof requests]
      operationId: getRequest
      summary: Retrieve a proof request
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ProofRequest" } } } }
        "404": { $ref: "#/components/responses/Error" }
  /v1/requests/{id}/receipts:
    post:
      tags: [Proof requests]
      operationId: submitReceipt
      summary: Submit the receipt that answers a request
      description: |
        Verifies the receipt, checks that it signs the request's challenge, paid the payee, and paid at least `amount_zatoshis`.
        On success the request becomes `fulfilled` and `request.fulfilled` is sent. On a verification failure `receipt.rejected` is sent and the request stays open.
        If the request has `source_of_funds`, a receipt without a source proof for every spent note is refused (422 `source_of_funds_required`); a deposit whose funding fails screening becomes `held_for_review` (`request.held_for_review`).
      parameters:
        - { $ref: "#/components/parameters/Id" }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [receipt], properties: { receipt: { type: string } } }
      responses:
        "200": { description: Fulfilled, content: { application/json: { schema: { $ref: "#/components/schemas/ProofRequest" } } } }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "422": { $ref: "#/components/responses/Error" }
  /v1/public/requests/{id}:
    get:
      tags: [Proof requests]
      operationId: publicGetRequest
      summary: Payer view of a request (no API key; needs the client secret)
      security: []
      parameters: [{ $ref: "#/components/parameters/Id" }, { name: secret, in: query, required: true, schema: { type: string } }]
      responses: { "200": { description: "Status, amounts, payment URI. Never metadata." }, "404": { $ref: "#/components/responses/Error" } }
  /v1/public/requests/{id}/receipts:
    post:
      tags: [Proof requests]
      operationId: publicSubmitReceipt
      summary: Payer submits a receipt from the hosted page (client secret; none needed for campaigns)
      security: []
      parameters: [{ $ref: "#/components/parameters/Id" }]
      requestBody: { content: { application/json: { schema: { type: object, required: [receipt], properties: { receipt: { type: string }, client_secret: { type: string } } } } } }
      responses: { "200": { description: Accepted }, "409": { $ref: "#/components/responses/Error" }, "422": { $ref: "#/components/responses/Error" } }
  /v1/public/campaigns/{id}:
    get:
      tags: [Proof requests]
      operationId: publicCampaign
      summary: Public campaign tally (verified total and payments, never donors)
      security: []
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses: { "200": { description: OK } }
  /v1/webhook_endpoints:
    post:
      tags: [Webhooks]
      operationId: createWebhookEndpoint
      summary: Register a webhook endpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                events: { type: array, items: { type: string, enum: ["*", request.created, request.payment_received, request.fulfilled, request.held_for_review, request.rejected, request.expired, receipt.rejected] }, default: ["*"] }
      responses:
        "201":
          description: Created. `secret` is only returned here.
          content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } }
    get:
      tags: [Webhooks]
      operationId: listWebhookEndpoints
      summary: List webhook endpoints
      responses: { "200": { description: OK } }
  /v1/webhook_endpoints/{id}:
    delete:
      tags: [Webhooks]
      operationId: deleteWebhookEndpoint
      summary: Delete a webhook endpoint
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses: { "200": { description: Deleted }, "404": { $ref: "#/components/responses/Error" } }
  /v1/events:
    get:
      tags: [Webhooks]
      operationId: listEvents
      summary: List events, newest first
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
      responses: { "200": { description: OK } }
  /v1/events/{id}/resend:
    post:
      tags: [Webhooks]
      operationId: resendEvent
      summary: Re-deliver an event to every subscribed endpoint
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses: { "200": { description: Queued }, "404": { $ref: "#/components/responses/Error" } }
  /v1/errors:
    get:
      tags: [Meta]
      operationId: listErrors
      summary: Every error code, its HTTP status and meaning
      security: []
      responses: { "200": { description: OK } }
  /v1/health:
    get:
      tags: [Meta]
      operationId: health
      summary: Liveness and configured networks
      security: []
      responses: { "200": { description: OK } }
webhooks:
  request.fulfilled:
    post:
      summary: A valid receipt fulfilled a proof request
      description: |
        Header `Zceipt-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`.
        Verify it, reject stale timestamps, and deduplicate on the event `id`, because deliveries can repeat.
      requestBody:
        content: { application/json: { schema: { $ref: "#/components/schemas/Event" } } }
      responses: { "200": { description: Return any 2xx to acknowledge } }
components:
  securitySchemes:
    bearer: { type: http, scheme: bearer, description: "rcpt_test_… or rcpt_live_…" }
  parameters:
    Id: { name: id, in: path, required: true, schema: { type: string } }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 255 }
      description: Retrying with the same key returns the first response instead of acting twice.
  responses:
    Error:
      description: Error
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          required: [type, code, message]
          properties:
            type: { type: string, enum: [invalid_request_error, authentication_error, verification_error, api_error] }
            code: { type: string, examples: [challenge_mismatch, transaction_not_mined, wrong_payee] }
            message: { type: string }
            param: { type: [string, "null"] }
            doc_url: { type: string }
    Output:
      type: object
      properties:
        action: { type: integer }
        recipient: { type: string, description: "Orchard-only unified address of the recipient" }
        zatoshis: { type: integer }
        memo: { type: [string, "null"] }
        memo_withheld: { type: boolean, description: "Present and true when the sender disclosed this output without its memo (receipt format v5)" }
    Verification:
      type: object
      properties:
        object: { const: verification }
        valid: { const: true }
        livemode: { type: boolean }
        network: { type: string, enum: [mainnet, testnet, regtest] }
        pool: { type: string, enum: [orchard, ironwood, orchard_to_ironwood, ironwood_to_orchard], description: "The spends' pool, or spends_to_outputs for a cross-pool (format v3) receipt." }
        txid: { type: string }
        mined_height: { type: integer }
        message: { type: string }
        challenge: { type: [string, "null"] }
        batch: { type: [object, "null"] }
        sender_addresses: { type: array, items: { type: string }, description: "Addresses the sender chose to prove; usually empty" }
        sources:
          type: array
          description: "Where spent notes came from, for spends whose source the sender proved (receipt format v4); omitted when none"
          items:
            type: object
            properties:
              spend_action: { type: integer }
              source_txid: { type: string }
              source_action: { type: integer }
              source_height: { type: integer }
              zatoshis: { type: [integer, "null"], description: "The spent note's exact value, if the sender revealed it" }
              transparent_inputs:
                type: array
                description: "The source transaction's public transparent inputs (who funded it in the clear)"
                items:
                  type: object
                  properties:
                    prevout_txid: { type: string }
                    prevout_index: { type: integer }
                    address: { type: [string, "null"] }
                    zatoshis: { type: [integer, "null"] }
        outputs: { type: array, items: { $ref: "#/components/schemas/Output" } }
        paid_to_expected: { type: integer }
        verdict: { type: string, enum: [valid, valid_provisional], description: "valid_provisional until the transaction has 10 confirmations" }
        checks:
          type: object
          description: "One level per property (ZIP 311 validity levels)"
          properties:
            spend_authority: { type: object, properties: { level: { const: proven }, actions: { type: integer } } }
            source_of_funds: { type: string, enum: [proven, not_disclosed] }
            outputs: { type: object, properties: { level: { const: opened }, count: { type: integer } } }
            confirmations: { type: [integer, "null"] }
            finality: { type: string, enum: [final, provisional, unknown] }
            challenge: { type: string, enum: [matched, present, none] }
            sender_address: { type: string, enum: [proven, not_disclosed] }
    BatchVerification:
      type: object
      properties:
        object: { const: batch_verification }
        valid: { const: true }
        id: { type: string }
        total_zatoshis: { type: integer }
        totals_by_recipient: { type: object, additionalProperties: { type: integer } }
        receipts: { type: array, items: { $ref: "#/components/schemas/Verification" } }
    ProofRequest:
      type: object
      properties:
        id: { type: string, examples: [preq_3f9a…] }
        object: { const: proof_request }
        livemode: { type: boolean }
        status: { type: string, enum: [open, fulfilled, held_for_review, rejected, expired] }
        source_of_funds: { type: [object, "null"], description: "The policy as created (owner view); the public view shows only required and screened_addresses" }
        provenance:
          type: [object, "null"]
          description: "Deposit desks: the screening result for the accepted receipt"
          properties:
            status: { enum: [cleared, review] }
            reasons: { type: array, items: { type: string } }
            funding: { type: array, items: { type: object, properties: { address: { type: string }, zatoshis: { type: integer }, source_txid: { type: string } } } }
            checked_at: { type: integer }
            review: { type: object, properties: { decision: { enum: [accept, reject] }, note: { type: string }, at: { type: integer } } }
        challenge: { type: string }
        payee: { type: string }
        amount_zatoshis: { type: [integer, "null"] }
        label: { type: [string, "null"] }
        metadata: { type: object }
        created: { type: integer }
        expires_at: { type: [integer, "null"] }
        kind: { type: string, enum: [single, partial, campaign] }
        received_zatoshis: { type: integer }
        remaining_zatoshis: { type: [integer, "null"] }
        payments: { type: array, items: { type: object, properties: { txid: { type: string }, zatoshis: { type: integer }, mined_height: { type: integer }, memo: { type: [string, "null"] } } } }
        client_secret: { type: string, description: "Lets a payer open the hosted page and submit a receipt without your API key" }
        hosted_url: { type: string, description: "Hosted pay page: /pay/{id}?secret=…" }
        public_url: { type: [string, "null"], description: "Campaigns only: public tally page /c/{id}" }
        payment_uri: { type: string, description: "ZIP-321 URI: zcash:<payee>?amount=…&memo=… (asks for the remaining amount)" }
        receipt_link: { type: string, description: "Verifier page link; pass it to `zceipt create --request`" }
        cli_command: { type: string }
        fulfillment:
          type: [object, "null"]
          properties:
            fulfilled_at: { type: integer }
            txid: { type: string }
            paid_zatoshis: { type: integer }
            verification: { $ref: "#/components/schemas/Verification" }
    WebhookEndpoint:
      type: object
      properties:
        id: { type: string }
        object: { const: webhook_endpoint }
        url: { type: string }
        events: { type: array, items: { type: string } }
        secret: { type: string, description: "whsec_…, only on creation" }
    Event:
      type: object
      properties:
        id: { type: string }
        object: { const: event }
        type: { type: string }
        created: { type: integer }
        livemode: { type: boolean }
        data: { type: object, properties: { object: { type: object } } }
