openapi: 3.1.0
info:
  title: CashLink Proof Camera Public API
  version: 2.0.0
  description: |
    Permanent private file proofs, resumable uploads, owner-only delivery,
    CashLink publication, paid IPFS publication, and Bitcoin Cash anchoring.
    Uses the shared CashLink Media API climg_ account and paid capacity.
    Terms version for payment creation: 2026-08-15.
servers:
  - url: /api/v2/proof-camera
security:
  - AccountToken: []
tags:
  - name: Proofs
  - name: Upload sessions
  - name: Publication
  - name: Payments
paths:
  /proofs:
    get:
      tags: [Proofs]
      operationId: listProofs
      summary: List the latest 100 proofs owned by the account
      responses:
        '200':
          description: Proof collection envelope
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [proofs]
                        properties:
                          proofs:
                            type: array
                            maxItems: 100
                            items: {$ref: '#/components/schemas/Proof'}
        default: {$ref: '#/components/responses/Problem'}
    post:
      tags: [Proofs]
      operationId: createProof
      summary: Create a permanent private proof from a multipart file
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: {type: string, format: binary}
                expected_sha256:
                  type: [string, 'null']
                  pattern: '^[A-Fa-f0-9]{64}$'
      responses:
        '202':
          description: Proof accepted for asynchronous security processing
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProofEnvelope'}
        '402': {$ref: '#/components/responses/Problem'}
        default: {$ref: '#/components/responses/Problem'}
  /proofs/{proof}:
    parameters:
      - $ref: '#/components/parameters/ProofId'
    get:
      tags: [Proofs]
      operationId: getProof
      responses:
        '200':
          description: Complete machine-readable proof state
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProofEnvelope'}
        default: {$ref: '#/components/responses/Problem'}
  /proofs/{proof}/preview:
    parameters:
      - $ref: '#/components/parameters/ProofId'
    get:
      tags: [Proofs]
      operationId: getProofPreview
      summary: Deliver owner-only preview bytes when preview.content_ready is true
      description: Native and safely transcoded images may be available before antivirus completes. PDF, video, and spreadsheet content previews are rendered only after ClamAV explicitly reports clean; all other file types use the public placeholder_url instead.
      responses:
        '200':
          description: Private preview bytes
          headers:
            Cache-Control: {$ref: '#/components/headers/PrivateNoStore'}
            X-Content-Type-Options: {$ref: '#/components/headers/NoSniff'}
            Content-Security-Policy: {$ref: '#/components/headers/SandboxCsp'}
          content:
            image/jpeg: {schema: {type: string, format: binary}}
            image/png: {schema: {type: string, format: binary}}
            image/gif: {schema: {type: string, format: binary}}
            image/webp: {schema: {type: string, format: binary}}
        default: {$ref: '#/components/responses/Problem'}
  /proofs/{proof}/original:
    parameters:
      - $ref: '#/components/parameters/ProofId'
    get:
      tags: [Proofs]
      operationId: downloadProofOriginal
      summary: Download after antivirus is clean or unscannable
      responses:
        '200':
          description: Private original bytes
          headers:
            Cache-Control: {$ref: '#/components/headers/PrivateNoStore'}
            X-Content-Type-Options: {$ref: '#/components/headers/NoSniff'}
            Content-Security-Policy: {$ref: '#/components/headers/SandboxCsp'}
          content:
            application/octet-stream:
              schema: {type: string, format: binary}
        '409':
          description: Antivirus processing has not reached an eligible state
          content:
            application/problem+json:
              schema: {$ref: '#/components/schemas/Problem'}
        default: {$ref: '#/components/responses/Problem'}
  /proofs/{proof}/shares:
    parameters:
      - $ref: '#/components/parameters/ProofId'
    post:
      tags: [Publication]
      operationId: createProofCashLinkShare
      summary: Idempotently create the public CashLink URL
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [publication_acknowledged]
              properties:
                publication_acknowledged: {type: boolean, const: true}
      responses:
        '201':
          description: Share created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Envelope'}
        '200':
          description: Existing share reused
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /proofs/{proof}/blockchain:
    parameters:
      - $ref: '#/components/parameters/ProofId'
    post:
      tags: [Publication]
      operationId: createProofBlockchainIntent
      summary: Redeem an existing blockchain credit for the proof
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/BlockchainIntentInput'}
      responses:
        '202':
          description: Intent created or reused
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /proofs/{proof}/ipfs-payment-quote:
    parameters:
      - $ref: '#/components/parameters/ProofId'
    get:
      tags: [Payments]
      operationId: quoteProofIpfsPublication
      deprecated: true
      description: Retired per-file product. Returns 410; buy storage through Image Space API instead.
      responses:
        '410': {$ref: '#/components/responses/Problem'}
        default: {$ref: '#/components/responses/Problem'}
  /proofs/{proof}/ipfs-payment-orders:
    parameters:
      - $ref: '#/components/parameters/ProofId'
    post:
      tags: [Payments]
      operationId: createProofIpfsPaymentOrder
      deprecated: true
      description: Retired per-file product. Returns 410; existing orders remain available through order routes.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/IpfsPaymentOrderInput'}
      responses:
        '410': {$ref: '#/components/responses/Problem'}
        default: {$ref: '#/components/responses/Problem'}
  /proofs/{proof}/blockchain-payment-quote:
    parameters:
      - $ref: '#/components/parameters/ProofId'
    get:
      tags: [Payments]
      operationId: quoteProofBlockchainCredits
      parameters:
        - name: units
          in: query
          required: true
          schema: {type: integer, minimum: 1, maximum: 100}
        - name: blockchain_acknowledged
          in: query
          required: true
          schema: {type: boolean, const: true}
        - name: blockchain_format
          in: query
          required: true
          schema: {$ref: '#/components/schemas/BlockchainFormat'}
        - name: include_ipfs_cid
          in: query
          schema: {type: boolean, default: false}
        - name: ipfs_cid
          in: query
          schema: {type: string, maxLength: 255}
        - name: include_cashlink_url
          in: query
          schema: {type: boolean, default: false}
        - name: cashlink_url
          in: query
          schema: {type: string, format: uri, maxLength: 2048}
      responses:
        '200':
          description: Signed BCH-credit payment quote
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /proofs/{proof}/blockchain-payment-orders:
    parameters:
      - $ref: '#/components/parameters/ProofId'
    post:
      tags: [Payments]
      operationId: createProofBlockchainPaymentOrder
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/BlockchainPaymentOrderInput'}
      responses:
        '201':
          description: Order created; the scoped order token is returned exactly once
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PaymentOrderCreationEnvelope'}
        '200':
          description: Idempotent replay; the order token is not returned again
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PaymentOrderCreationEnvelope'}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders:
    get:
      tags: [Payments]
      operationId: listProofPaymentOrders
      summary: List recent IPFS and BCH orders for the account
      responses:
        '200':
          description: Payment order collection
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders/{order}:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    get:
      tags: [Payments]
      operationId: getProofPaymentOrder
      security:
        - OrderToken: []
      responses:
        '200':
          description: Scoped order state
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PaymentOrderEnvelope'}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders/{order}/activate:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    post:
      tags: [Payments]
      operationId: activateProofPaymentOrder
      security:
        - OrderToken: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [saved_order_token]
              properties:
                saved_order_token: {type: boolean, const: true}
      responses:
        '200':
          description: Activated order
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PaymentOrderEnvelope'}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders/{order}/events:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    get:
      tags: [Payments]
      operationId: streamProofPaymentOrderEvents
      security:
        - OrderToken: []
      parameters:
        - $ref: '#/components/parameters/LastEventId'
      responses:
        '200':
          description: Resumable Server-Sent Events
          headers:
            Cache-Control: {$ref: '#/components/headers/PrivateNoStore'}
          content:
            text/event-stream:
              schema:
                type: string
                description: SSE frames with proof_camera.payment.updated events
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders/{order}/qr:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    get:
      tags: [Payments]
      operationId: getProofPaymentOrderQr
      security:
        - OrderToken: []
      responses:
        '200':
          description: Private BCH payment QR code
          headers:
            Cache-Control: {$ref: '#/components/headers/PrivateNoStore'}
            X-Content-Type-Options: {$ref: '#/components/headers/NoSniff'}
          content:
            image/svg+xml:
              schema: {type: string}
        default: {$ref: '#/components/responses/Problem'}
  /payment-orders/{order}/refunds:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    get:
      tags: [Payments]
      operationId: listProofPaymentOrderRefunds
      security:
        - OrderToken: []
      responses:
        '200':
          description: Refund liabilities and transactions
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
  /upload-sessions:
    post:
      tags: [Upload sessions]
      operationId: createProofUploadSession
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/UploadSessionInput'}
      responses:
        '201':
          description: Session created; upload_token is returned once
          content:
            application/json:
              schema: {$ref: '#/components/schemas/UploadSessionCreationEnvelope'}
        '402': {$ref: '#/components/responses/Problem'}
        default: {$ref: '#/components/responses/Problem'}
  /upload-sessions/{session}:
    parameters:
      - $ref: '#/components/parameters/SessionId'
      - $ref: '#/components/parameters/UploadToken'
    get:
      tags: [Upload sessions]
      operationId: getProofUploadSession
      responses:
        '200':
          description: Current accepted offset and session state
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
    patch:
      tags: [Upload sessions]
      operationId: appendProofUploadChunk
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/UploadOffset'
        - $ref: '#/components/parameters/UploadChunkSha256'
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: {type: string, format: binary}
      responses:
        '200':
          description: Chunk accepted and offset advanced
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Envelope'}
        default: {$ref: '#/components/responses/Problem'}
    delete:
      tags: [Upload sessions]
      operationId: cancelProofUploadSession
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204': {description: Session cancelled}
        default: {$ref: '#/components/responses/Problem'}
  /upload-sessions/{session}/complete:
    parameters:
      - $ref: '#/components/parameters/SessionId'
      - $ref: '#/components/parameters/UploadToken'
    post:
      tags: [Upload sessions]
      operationId: completeProofUploadSession
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Proof created and accepted for security processing
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProofEnvelope'}
        default: {$ref: '#/components/responses/Problem'}
components:
  securitySchemes:
    AccountToken:
      type: http
      scheme: bearer
      bearerFormat: climg_
      description: Shared CashLink Media API account token.
    OrderToken:
      type: http
      scheme: bearer
      bearerFormat: clapi_
      description: One-time-returned token scoped to one Proof payment order.
  parameters:
    ProofId:
      name: proof
      in: path
      required: true
      schema: {type: string, format: uuid}
    SessionId:
      name: session
      in: path
      required: true
      schema: {type: string, format: uuid}
    OrderId:
      name: order
      in: path
      required: true
      schema: {type: string, format: uuid}
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: {type: string, minLength: 16, maxLength: 200}
    UploadToken:
      name: Upload-Token
      in: header
      required: true
      schema: {type: string, minLength: 32}
    UploadOffset:
      name: Upload-Offset
      in: header
      required: true
      schema: {type: integer, minimum: 0}
    UploadChunkSha256:
      name: Upload-Chunk-SHA256
      in: header
      required: true
      schema: {type: string, pattern: '^[a-f0-9]{64}$'}
    LastEventId:
      name: Last-Event-ID
      in: header
      required: false
      schema: {type: integer, minimum: 0}
  headers:
    PrivateNoStore:
      schema: {type: string, const: 'private, no-store'}
    NoSniff:
      schema: {type: string, const: nosniff}
    SandboxCsp:
      schema: {type: string}
      description: Restrictive sandbox CSP.
  responses:
    Problem:
      description: RFC 9457 Problem Details error
      content:
        application/problem+json:
          schema: {$ref: '#/components/schemas/Problem'}
  schemas:
    Envelope:
      type: object
      required: [data, links, meta]
      properties:
        data: {type: object, additionalProperties: true}
        links:
          type: object
          required: [self]
          properties:
            self: {type: string, format: uri}
        meta:
          type: object
          required: [contract_version]
          properties:
            contract_version: {type: integer, const: 2}
    Problem:
      type: object
      required: [type, title, status]
      properties:
        type: {type: string, format: uri-reference}
        title: {type: string}
        status: {type: integer, minimum: 400, maximum: 599}
        detail: {type: string}
        instance: {type: string, format: uri-reference}
        errors:
          type: object
          additionalProperties:
            type: array
            items: {type: string}
    ProofEnvelope:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          properties:
            data:
              type: object
              required: [proof]
              properties:
                proof: {$ref: '#/components/schemas/Proof'}
    Proof:
      type: object
      required: [id, hash, file, retention, security, preview, analysis, cashlink, ipfs, blockchain, created_at, links]
      properties:
        id: {type: string, format: uuid}
        hash: {$ref: '#/components/schemas/ProofHash'}
        file: {$ref: '#/components/schemas/ProofFile'}
        retention:
          type: object
          required: [permanent, saved_at, expires_at]
          properties:
            permanent: {type: boolean}
            saved_at: {type: [string, 'null'], format: date-time}
            expires_at: {type: [string, 'null'], format: date-time}
        security:
          type: object
          required: [lifecycle_status, scan_status, nsfw_status, nsfw_probability, malware_deleted]
          properties:
            lifecycle_status: {type: [string, 'null']}
            scan_status: {type: [string, 'null']}
            nsfw_status: {type: [string, 'null']}
            nsfw_probability: {type: [number, 'null'], minimum: 0, maximum: 1}
            malware_deleted: {type: boolean}
        preview:
          type: object
          required: [kind, status, content_ready, ready, url, placeholder_url]
          properties:
            kind:
              type: string
              enum: [native_image, transcoded_image, video_frame, pdf_first_page, spreadsheet_excerpt, type_illustration]
            status: {type: [string, 'null']}
            content_ready:
              type: boolean
              description: True only when the private preview endpoint currently has actual user-content bytes.
            ready: {type: boolean}
            url:
              type: [string, 'null']
              format: uri
              description: Owner-only actual content preview. Null while scanning/rendering, after failure, or for illustration-only types.
            placeholder_url:
              type: string
              format: uri
              description: Public static type illustration that never contains uploaded content.
        analysis:
          type: object
          required: [status, exif_status, c2pa_status, error_code]
          properties:
            status: {type: [string, 'null']}
            exif_status: {type: [string, 'null']}
            c2pa_status: {type: [string, 'null']}
            error_code: {type: [string, 'null']}
        cashlink:
          type: object
          required: [url]
          properties:
            url: {type: [string, 'null'], format: uri}
        ipfs:
          type: object
          required: [status, cid]
          properties:
            status: {type: [string, 'null']}
            cid: {type: [string, 'null']}
        blockchain:
          type: object
          required: [status, format, txid, op_return, fee_satoshis, explorer_links]
          properties:
            status: {type: [string, 'null']}
            format:
              oneOf:
                - $ref: '#/components/schemas/BlockchainFormat'
                - type: 'null'
            txid: {type: [string, 'null'], pattern: '^[a-fA-F0-9]{64}$'}
            op_return: {type: [string, 'null']}
            fee_satoshis: {type: [integer, 'null'], minimum: 0}
            explorer_links:
              type: object
              additionalProperties: {type: string, format: uri}
        created_at: {type: [string, 'null'], format: date-time}
        links:
          type: object
          required: [self, preview, original]
          properties:
            self: {type: string, format: uri}
            preview: {type: string, format: uri}
            original: {type: string, format: uri}
    ProofHash:
      type: object
      required: [algorithm, actual, expected, matches_expected]
      properties:
        algorithm: {type: string, const: sha256}
        actual: {type: string, pattern: '^[a-f0-9]{64}$'}
        expected: {type: [string, 'null'], pattern: '^[a-f0-9]{64}$'}
        matches_expected: {type: [boolean, 'null']}
    ProofFile:
      type: object
      required: [name, size_bytes, mime_type, is_image]
      properties:
        name: {type: string}
        size_bytes: {type: integer, minimum: 1}
        mime_type: {type: string}
        is_image: {type: boolean}
    BlockchainFormat:
      type: string
      enum: [json, memo_cash]
    BlockchainIntentInput:
      type: object
      required: [blockchain_acknowledged, blockchain_format]
      properties:
        blockchain_acknowledged: {type: boolean, const: true}
        blockchain_format: {$ref: '#/components/schemas/BlockchainFormat'}
        include_ipfs_cid: {type: boolean, default: false}
        ipfs_cid: {type: [string, 'null'], maxLength: 255}
        include_cashlink_url: {type: boolean, default: false}
        cashlink_url: {type: [string, 'null'], format: uri, maxLength: 2048}
    UploadSessionInput:
      type: object
      required: [name, size]
      properties:
        name: {type: string, minLength: 1, maxLength: 255}
        mime: {type: [string, 'null'], maxLength: 150}
        size: {type: integer, minimum: 1}
        expected_sha256: {type: [string, 'null'], pattern: '^[A-Fa-f0-9]{64}$'}
    UploadSessionCreationEnvelope:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          properties:
            data:
              type: object
              required: [upload_session, upload_token]
              properties:
                upload_session: {$ref: '#/components/schemas/UploadSession'}
                upload_token: {type: string}
    UploadSession:
      type: object
      required: [id]
      properties:
        id: {type: string, format: uuid}
        status: {type: string}
        size: {type: integer, minimum: 1}
        offset: {type: integer, minimum: 0}
        expires_at: {type: [string, 'null'], format: date-time}
    IpfsPaymentOrderInput:
      type: object
      required:
        - quote_token
        - refund_address
        - refund_address_confirmation
        - terms_version
        - terms_accepted
        - ipfs_public_acknowledgement
      properties:
        quote_token: {type: string, maxLength: 4096}
        refund_address: {type: string, maxLength: 120}
        refund_address_confirmation: {type: string, maxLength: 120}
        terms_version: {type: string, const: '2026-08-15'}
        terms_accepted: {type: boolean, const: true}
        ipfs_public_acknowledgement: {type: boolean, const: true}
        ipfs_unscannable_acknowledgement: {type: boolean}
    BlockchainPaymentOrderInput:
      type: object
      required:
        - quote_token
        - refund_address
        - refund_address_confirmation
        - terms_version
        - terms_accepted
        - blockchain_permanent_acknowledgement
      properties:
        quote_token: {type: string, maxLength: 8192}
        refund_address: {type: string, maxLength: 120}
        refund_address_confirmation: {type: string, maxLength: 120}
        terms_version: {type: string, const: '2026-08-15'}
        terms_accepted: {type: boolean, const: true}
        blockchain_permanent_acknowledgement: {type: boolean, const: true}
    PaymentOrderCreationEnvelope:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          properties:
            data:
              type: object
              required: [created, order_token, token_returned, order]
              properties:
                created: {type: boolean}
                order_token:
                  type: [string, 'null']
                  pattern: '^clapi_[A-Za-z0-9_-]{43}$'
                token_returned: {type: boolean}
                order: {$ref: '#/components/schemas/PaymentOrder'}
    PaymentOrderEnvelope:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          properties:
            data:
              type: object
              required: [order]
              properties:
                order: {$ref: '#/components/schemas/PaymentOrder'}
    PaymentOrder:
      type: object
      required: [id, proof_id, status]
      additionalProperties: true
      properties:
        id: {type: string, format: uuid}
        proof_id: {type: string, format: uuid}
        product_type: {type: string}
        status: {type: string}
        amount_satoshis: {type: integer, minimum: 0}
        address: {type: [string, 'null']}
        refund_address: {type: [string, 'null']}
        terms_version: {type: string, const: '2026-08-15'}
        proof_url: {type: string, format: uri}
        activation_url: {type: string, format: uri}
        events_url: {type: string, format: uri}
        qr_url: {type: [string, 'null'], format: uri}
        refunds_url: {type: string, format: uri}
