openapi: 3.0.3
info:
  title: 0byte Registry API
  version: 1.4.0
  description: |
    The 0byte origin protocol for AI content.

    **Stamp** signs a spec-compliant C2PA manifest into your file (when the
    deployment has a signing certificate configured) and anchors a perceptual
    fingerprint in a public, append-only transparency log — so the proof
    survives screenshots, re-encoding, and stripped metadata.

    **Verify** is evidence-first: it checks every readable origin signal
    (embedded C2PA manifest, registry fingerprint match) and returns one
    verdict with cited evidence. Never a probability; a failed signal is
    disclosed, never silently folded into the result.

    **Stability policy:** within `/v1`, changes are additive only — existing
    fields never change meaning or disappear. New fields may appear at any
    time; clients must ignore fields they do not recognize.

    **Request ids:** every response carries `X-Request-Id` (yours, echoed,
    when you send one — letters, digits, `.`, `_`, `-`, ≤128 chars — else
    generated). Error bodies repeat it as `request_id`; quote it when you
    write in.

    **Rate limits:** responses to API-key requests on a rate-limited tier
    (free, dev, pro) carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and
    `X-RateLimit-Reset` (epoch seconds when one unit of capacity returns,
    on the response `Date` clock; the window slides over the last minute).
    Operator keys on the `unlimited` tier carry none — there is nothing to
    report. A 429 — per-key `RATE_LIMITED`, or per-IP `IP_RATE_LIMITED` —
    always carries `Retry-After` in seconds. The per-IP limiter has two
    lanes: unauthenticated requests get a 60-request burst refilling at
    1/s; requests carrying an API key get a 1,000-request burst refilling at
    ~17/s, so the tier limit — not the IP limit — is what binds a key.

    **Scope:** this document is the complete PUBLIC developer contract. The
    `/v1/auth/*` and `/v1/dashboard/*` routes are intentionally excluded —
    they are session endpoints internal to the 0byte web dashboard, carry no
    stability guarantee for external callers, and may change without notice.
  contact:
    email: hello@0byte.tech
servers:
  - url: https://api.0byte.tech/v1
tags:
  - name: stamp
    description: Create proofs of origin (API key required)
  - name: verify
    description: Free, public verification — no account needed
  - name: proofs
    description: Public proof records
  - name: transparency
    description: The append-only transparency log — verify the math yourself
  - name: cli
    description: >-
      Device-code pairing for `zerobyte login`. A terminal starts a pairing, the
      user approves it in a browser while signed in, and the terminal collects an
      API key exactly once. Approval is a POST carrying the session token, never
      a link, so arriving at a URL can never authorize anything.
  - name: keys
    description: >-
      API key management: list and revoke, with an account-bound key carrying
      the `keys` scope. Keys are MINTED only by the `zerobyte login` device
      flow (see the cli tag) — there is no creation endpoint, so a raw key
      never transits anything but the owner's CLI.

paths:
  /stamp:
    post:
      tags: [stamp]
      operationId: stamp
      summary: Stamp content — C2PA manifest + registry proof in one call
      description: |
        Computes the content's perceptual fingerprint, creates a signed proof
        in the registry (anchored into the transparency log within ~60s), and
        — when this deployment has a C2PA signing certificate configured and
        the format supports embedding — signs and embeds a C2PA manifest
        declaring AI origin (IPTC digitalSourceType `trainedAlgorithmicMedia`).

        The signature is countersigned by an RFC 3161 timestamp authority when
        one is configured (`C2PA_TSA_URL`), fixing the moment of signing
        independently of the signer. That is what keeps a signature checkable
        after the certificate that produced it expires or is revoked: the
        timestamp establishes that the signature predates that event, so files
        already published stay verifiable.

        `creator` is asserted twice in the manifest — as the C2PA
        `claim_generator`, which validators show as the producing tool, and as
        a `stds.schema-org.CreativeWork` assertion whose `author` is an
        `Organization` carrying the name and, when supplied, the `url`.

        The `binding` field always discloses what you got:
        `manifest+registry` (publish the returned `stamped_content`) or
        `registry_only` (with the reason). A signing failure can never lose
        the stamp — the registry proof exists either way.

        **Retries are safe with an `Idempotency-Key`.** The log is
        append-only, so a duplicated retry would be a permanent duplicate
        proof. Send a unique key per asset (a UUID is fine): a retry with the
        same key and the same request returns the original proof with
        `replayed: true` and an `Idempotency-Replayed: true` header; the same
        key with a different request is refused with 409. Keys are scoped to
        your API key and expire after 24 hours. A retried timeout is exactly
        the case this exists for — the proof may already have been committed
        before the response was lost.

        **Formats are checked before anything is decoded.** The declared
        `content_type` must be in the supported list and must match the
        bytes; anything else is a 415 carrying the list — never a bare 400.
      security:
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/RequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StampRequest'
      responses:
        '200':
          description: Proof created (or replayed — see the header)
          headers:
            Idempotency-Replayed:
              description: |
                Present and `true` when this response replayed an earlier
                stamp under the same Idempotency-Key (the body also carries
                `replayed: true`).
              schema: { type: string, enum: ['true'] }
            X-Request-Id: { $ref: '#/components/headers/XRequestId' }
            X-RateLimit-Limit: { $ref: '#/components/headers/XRateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/XRateLimitReset' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StampResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/ContentTooUniform'
        '429':
          $ref: '#/components/responses/RateLimited'

  /verify:
    post:
      tags: [verify]
      operationId: verify
      security: []   # deliberately public — free verification is the product
      summary: Verify content — evidence-first, free, no account
      description: |
        Checks every readable origin signal and fuses them into one verdict
        with cited evidence:

        - `verified_origin` — a registry fingerprint match, or a valid
          manifest from a **trust-listed** signer
        - `provenance_untrusted` — a cryptographically valid manifest from an
          unverified signer (a self-claim; see `evidence.manifest.signer`)
        - `provenance_invalid` — a manifest is present but fails validation
          (a tampering signal in itself)
        - `no_provenance_found` — an honest null. 0byte does not guess.

        The legacy `matched`/`confidence`/`proof` fields describe the
        registry signal only and are kept for compatibility.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyRequest'
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Verdict with cited evidence
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '503':
          description: |
            A verification signal failed and the remaining evidence is
            inconclusive — retry shortly. Never disguised as
            `no_provenance_found`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /analyze:
    post:
      tags: [verify]
      operationId: analyze
      deprecated: true
      security: []   # deliberately public
      summary: 'Legacy: registry lookup + AI-likelihood estimate'
      description: |
        **Deprecated — prefer `/verify`.** This legacy endpoint fuses the
        registry signal with a statistical AI-likelihood estimate into a
        probability, which contradicts 0byte's evidence-first verdicts
        (a verdict is never a probability). Kept for existing SDK v1
        integrations; scheduled for rework as an explicit opt-in estimate.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyRequest'
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Legacy analysis result (schema not stability-guaranteed)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '400':
          $ref: '#/components/responses/BadRequest'

  /openapi.yaml:
    get:
      tags: [transparency]
      operationId: getOpenApiSpec
      security: []   # deliberately public
      summary: This document
      description: |
        The spec is embedded in the service binary at compile time, so the
        served contract can never drift from the deployed code.
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: This OpenAPI document
          headers:
            X-Request-Id: { $ref: '#/components/headers/XRequestId' }
          content:
            application/yaml:
              schema:
                type: string

  /proofs/{proof_id}:
    get:
      tags: [proofs]
      operationId: getProof
      security: []   # deliberately public — free verification is the product
      summary: Fetch a proof record by id
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - $ref: '#/components/parameters/ProofId'
      responses:
        '200':
          description: The proof record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Proof'
        '404':
          $ref: '#/components/responses/NotFound'

  /transparency/head:
    get:
      tags: [transparency]
      operationId: getTreeHead
      security: []   # deliberately public — free verification is the product
      summary: The latest signed tree head (STH)
      description: |
        Verifiers must take `tree_size` and `root_hash` together from here —
        the Ed25519 signature over the pair (canonical form
        `sth:v1:{key_id}:{tree_size}:{root_hash}`) is what binds a proof to a
        specific log state.
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Latest signed head (fields are null while the log is empty)
          headers:
            X-Request-Id: { $ref: '#/components/headers/XRequestId' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TreeHead'

  /transparency/inclusion/{proof_id}:
    get:
      tags: [transparency]
      operationId: getInclusionProof
      security: []   # deliberately public — free verification is the product
      summary: Merkle inclusion proof for a stamped proof
      description: |
        RFC 6962/9162 inclusion path against the **current** signed head —
        because the log is append-only, a proof anchored at any point in the
        past always verifies against the latest head. The service self-verifies
        every path before serving it.
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - $ref: '#/components/parameters/ProofId'
      responses:
        '200':
          description: Inclusion proof
          headers:
            X-Request-Id: { $ref: '#/components/headers/XRequestId' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InclusionProof'
        '404':
          description: Proof not yet anchored (anchoring runs every ~60s)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /transparency/consistency/{from}/{to}:
    get:
      tags: [transparency]
      operationId: getConsistencyProof
      security: []   # deliberately public — free verification is the product
      summary: Consistency proof between two log sizes
      description: |
        Proves the log at size `from` is a strict prefix of the log at size
        `to` — history was appended, never rewritten (RFC 6962 §2.1.2).
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - name: from
          in: path
          required: true
          schema: { type: integer, format: int64, minimum: 1 }
        - name: to
          in: path
          required: true
          schema: { type: integer, format: int64, minimum: 1 }
      responses:
        '200':
          description: Consistency proof, with the signed heads at both sizes when published
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsistencyProof'
        '400':
          $ref: '#/components/responses/BadRequest'

  /transparency/keys:
    get:
      tags: [transparency]
      operationId: listSigningKeys
      security: []   # deliberately public — free verification is the product
      summary: Every signing key that ever signed proofs or tree heads
      description: |
        Historical proofs verify against the key that signed them, identified
        by the `signing_key_id` embedded in each proof. Verifiers should
        derive `key_id` (first 8 hex chars of SHA-256 over the public key
        bytes) rather than trusting the label.
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: The signing key registry
          headers:
            X-Request-Id: { $ref: '#/components/headers/XRequestId' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeys'

  /keys:
    get:
      tags: [keys]
      operationId: listKeys
      summary: List the caller's account's keys
      security:
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Active keys in the caller's account (raw keys never returned)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApiKeySummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /keys/{key_id}:
    delete:
      tags: [keys]
      operationId: revokeKey
      summary: Revoke a key in the caller's account
      description: |
        Soft revocation — takes effect immediately, works even for keys that
        have stamped, and preserves proof attribution. A foreign or
        nonexistent key id uniformly answers 404. Self-revocation is refused.
      security:
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
        - name: key_id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: { type: boolean }
                  id: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /cli/pair:
    post:
      tags: [cli]
      operationId: startCliPairing
      # Intentionally open: the caller has no credential yet.
      security: []
      summary: Start a device-code pairing
      description: >-
        Called by the CLI. Unauthenticated by necessity — the caller has no
        credential yet. Rate limited per source address, because the returned
        user code is short by design.
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        "201":
          description: Pairing started
          content:
            application/json:
              schema:
                type: object
                required: [device_code, user_code, verify_url, expires_in, interval]
                properties:
                  device_code:
                    type: string
                    description: Secret held by the CLI. Stored hashed server-side.
                  user_code:
                    type: string
                    example: DK5X-88T7
                    description: Shown to the human, to be compared against the browser.
                  verify_url:
                    type: string
                    example: https://0byte.tech/cli?code=DK5X-88T7
                  expires_in:
                    type: integer
                    example: 600
                  interval:
                    type: integer
                    example: 5
                    description: Seconds the client must wait between polls.
        "429":
          description: Too many pairings started from this address
  /cli/pair/approve:
    post:
      tags: [cli]
      operationId: approveCliPairing
      summary: Approve a pairing from the browser
      description: >-
        Requires a signed-in session. Deliberately a POST: a GET approve link
        would be a one-click phishing primitive. Every failure answers alike, so
        the short user code cannot be probed for existence.
      # The same Authorization: Bearer header, carrying a session token rather
      # than an API key — approving is an account action, not an API-key one.
      # Declared as its own scheme so generated clients send a session JWT here
      # instead of an 0b_key_ credential.
      security:
        - session: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_code]
              properties:
                user_code:
                  type: string
                  description: Case and the grouping dash are ignored.
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        "200":
          description: Approved
          content:
            application/json:
              schema:
                type: object
                properties:
                  approved:
                    type: boolean
        "400":
          description: The code is not eight characters
        "401":
          description: No valid session
        "409":
          description: Unusable — mistyped, expired, or already approved
  /cli/pair/token:
    post:
      tags: [cli]
      operationId: collectCliPairingToken
      # The device code is the credential here, carried in the body.
      security: []
      summary: Collect the API key
      description: >-
        Polled by the CLI. Returns 202 while approval is outstanding, and hands
        the key over exactly once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [device_code]
              properties:
                device_code:
                  type: string
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        "200":
          description: Approved; the key is returned once
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_key:
                    type: string
        "202":
          description: Still waiting for the browser approval
        "404":
          description: No such pairing
        "409":
          description: Already collected
        "410":
          description: Expired
        "429":
          description: Polling faster than the advertised interval
  /health:
    get:
      tags: [transparency]
      operationId: health
      security: []   # deliberately public — free verification is the product
      summary: Liveness check
      description: |
        Process liveness only — never touches the database, so a healthy
        response does NOT mean the instance can serve traffic. Point platform
        TRAFFIC health checks at /ready instead; keep restart-style liveness
        checks here so a database blip cannot cause a restart loop.
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Service is up
          headers:
            X-Request-Id: { $ref: '#/components/headers/XRequestId' }
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /ready:
    get:
      tags: [transparency]
      operationId: ready
      security: []
      summary: Readiness check
      description: |
        Whether this instance can actually serve traffic: probes the database
        under a 2s timeout. Returns 503 when the database is unreachable or
        slow, so the platform stops routing instead of serving errors.
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Ready to serve
          headers:
            X-Request-Id: { $ref: '#/components/headers/XRequestId' }
          content:
            application/json:
              schema:
                type: object
                required: [status, checks, index_size]
                properties:
                  status: { type: string, enum: [ready] }
                  checks:
                    type: object
                    required: [database]
                    properties:
                      database: { type: string, example: ok }
                  index_size: { type: integer }
        '503':
          description: Not ready — a dependency check failed
          content:
            application/json:
              schema:
                type: object
                required: [status, checks, index_size]
                properties:
                  status: { type: string, enum: [not_ready] }
                  checks:
                    type: object
                    required: [database]
                    properties:
                      database: { type: string, example: unreachable }
                  index_size: { type: integer }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: 'API key: `Authorization: Bearer 0b_key_…`'
    session:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'Dashboard session token: `Authorization: Bearer <jwt>`'

  parameters:
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      description: Your own correlation id. Echoed on the response and in error bodies when it matches the pattern; absent or unsafe values are replaced by a generated id.
      schema: { type: string, minLength: 1, maxLength: 128, pattern: '^[A-Za-z0-9._-]+$' }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Unique per asset (e.g. a UUID), 1-255 printable characters, scoped to
        your API key, valid for 24 hours. A retry with the same key and the
        same request returns the original proof (`replayed: true`); the same
        key with a different request is refused with 409.
      schema:
        type: string
        minLength: 1
        maxLength: 255
        example: 3f1e2c9a-5b7d-4e8f-9a0b-1c2d3e4f5a6b
    ProofId:
      name: proof_id
      in: path
      required: true
      schema: { type: string }
      example: 0b_1f6a2c...

  headers:
    XRequestId:
      description: The request id — echoed from the request when supplied, else generated. Error bodies repeat it as `request_id`.
      schema: { type: string, maxLength: 128, example: 3f1e2c9a5b7d4e8f9a0b1c2d3e4f5a6b }
    XRateLimitLimit:
      description: Requests per minute allowed for the key's tier.
      schema: { type: integer, example: 60 }
    XRateLimitRemaining:
      description: Requests left in the sliding one-minute window after this one.
      schema: { type: integer, example: 59 }
    XRateLimitReset:
      description: Epoch seconds at which one unit of capacity returns, on the same clock as the response's `Date` header (compute the wait as `X-RateLimit-Reset − Date`, or read `Retry-After` on a 429).
      schema: { type: integer, example: 1788109200 }
    RetryAfter:
      description: Seconds to wait before retrying (on 429).
      schema: { type: integer, example: 12 }

  responses:
    BadRequest:
      description: Invalid input
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing or invalid API key
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Key lacks the required scope or is not account-bound
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Not found (foreign resources answer identically)
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    PayloadTooLarge:
      description: Content exceeds the 10MB decoded limit
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: |
        `RATE_LIMITED` — the key's tier limit for the sliding minute; or
        `IP_RATE_LIMITED` — the per-address burst limit that fronts every
        route. Both carry `Retry-After`.
      headers:
        Retry-After: { $ref: '#/components/headers/RetryAfter' }
        X-RateLimit-Limit: { $ref: '#/components/headers/XRateLimitLimit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/XRateLimitReset' }
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    IdempotencyConflict:
      description: |
        `IDEMPOTENCY_KEY_REUSED` — the key was already used with a different
        request; or `IDEMPOTENCY_IN_PROGRESS` — the original request with
        this key is still being processed (retry in a few seconds).
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ContentTooUniform:
      description: |
        `CONTENT_TOO_UNIFORM` — the image has essentially no luminance
        variation (a blank or near-blank frame). Its fingerprint would be
        numerical noise shared by every such image, so no proof is created.
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    UnsupportedMediaType:
      description: |
        `UNSUPPORTED_MEDIA_TYPE` — the declared `content_type` is outside
        the supported list; or `CONTENT_TYPE_MISMATCH` — it is supported but
        the bytes are recognisably another format. Both carry `supported`.
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      description: |
        Structured error — `code` is machine-readable and stable, `docs_url`
        points at the section of the docs that explains it, and `supported`
        (415s only) lists the media types that would have been accepted.
      required: [code, message, docs_url, request_id]
      properties:
        code: { type: string, example: UNSUPPORTED_MEDIA_TYPE }
        message: { type: string }
        docs_url:
          type: string
          format: uri
          example: https://0byte.tech/docs#content-credentials
        request_id:
          type: string
          description: |
            The X-Request-Id of the request this error answers — on every
            error the registry produces, including malformed-JSON 400s,
            unknown-route 404s and per-IP 429s.
          example: 3f1e2c9a5b7d4e8f9a0b1c2d3e4f5a6b
        supported:
          type: array
          items: { type: string }
          example: [image/jpeg, image/png, image/webp, image/gif, image/tiff, image/bmp, image/x-icon]

    CreatorInfo:
      type: object
      description: |
        Brand attribution — named as the C2PA claim generator and schema.org
        author ("generated by {brand} using {model}"). An ASSERTED identity:
        a claim by the stamper, distinguished from the verified-identity tier.
      required: [name]
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        url: { type: string, maxLength: 300, example: https://acme.example }

    StampRequest:
      type: object
      required: [content, content_type, provider, model]
      properties:
        content:
          type: string
          format: byte
          description: Base64 of the content bytes (max 10MB decoded)
        content_type:
          type: string
          example: image/png
          description: |
            Full-stamp formats (manifest + registry): image/jpeg, image/png,
            image/webp, image/gif, image/tiff. Registry-only: image/bmp,
            image/x-icon. Anything else — and a supported type whose bytes
            are recognisably another format — is refused with 415 and the
            supported list. Aliases such as image/jpg are normalised; the
            canonical type is what the proof records.
        provider:
          type: string
          minLength: 1
          maxLength: 100
          example: acme-ai
        model:
          type: string
          minLength: 1
          maxLength: 100
          example: imagen-x
        metadata:
          type: object
          additionalProperties: true
        creator:
          $ref: '#/components/schemas/CreatorInfo'

    Proof:
      type: object
      description: A signed, log-anchored proof of origin.
      required:
        [id, fingerprint, provider, model, content_type, timestamp,
         signature, signing_key_id, canonical_version, fingerprint_version,
         verify_url]
      properties:
        id: { type: string, example: 0b_1f6a2c... }
        fingerprint:
          type: string
          description: |
            Perceptual fingerprint (hex). `fingerprint_version` names the
            algorithm: v2 is a 2-D pHash — luma → 32×32 → 2-D DCT-II →
            top-left 8×8 → each coefficient vs the median (DC excluded) →
            64 bits. v1 rows (an earlier hasher) stay on their proofs but
            never participate in matching.
        fingerprint_version:
          type: string
          enum: [v1, v2]
        provider: { type: string }
        model: { type: string }
        content_type: { type: string }
        timestamp: { type: string, format: date-time }
        signature:
          type: string
          description: Base64 Ed25519 signature over the canonical form
        signing_key_id:
          type: string
          description: Resolve via /transparency/keys
        canonical_version:
          type: string
          enum: [v1, v2]
          description: |
            Which canonical form the signature covers. v2 (current) is
            length-prefixed and injective; v1 is the legacy `:`-delimited
            form on historical proofs.
        verify_url: { type: string }
        metadata: { type: object, additionalProperties: true }

    StampResponse:
      allOf:
        - $ref: '#/components/schemas/Proof'
        - type: object
          required: [binding]
          properties:
            binding:
              type: string
              enum: ['manifest+registry', registry_only]
              description: Always disclosed — never a silent downgrade.
            binding_reason:
              type: string
              description: Present when binding is registry_only.
            stamped_content:
              type: string
              format: byte
              description: |
                Base64 of the manifest-embedded file. Present only for
                manifest+registry bindings — publish THIS file.
            replayed:
              type: boolean
              description: |
                Present and `true` when this response replayed an earlier
                stamp under the same Idempotency-Key. The proof is the
                original; the embedded manifest is re-signed over the resent
                content (0byte never stores content), so its signature bytes
                differ while its claims do not.

    VerifyRequest:
      type: object
      required: [content, content_type]
      properties:
        content: { type: string, format: byte }
        content_type: { type: string, example: image/png }

    ManifestEvidence:
      type: object
      description: The embedded C2PA manifest signal.
      required: [status]
      properties:
        status:
          type: string
          enum: [valid, invalid, absent, error]
        trusted:
          type: boolean
          description: |
            True only when the signer chains to the trust list. A valid
            manifest from an unknown signer is a self-claim — the verdict
            never promotes it to verified_origin.
        signer: { type: string }
        claim_generator: { type: string }
        digital_source_type:
          type: string
          description: trainedAlgorithmicMedia = the manifest declares AI origin.
        creator: { type: string, description: Asserted brand (schema.org author). }
        proof_ref:
          type: object
          additionalProperties: true
          description: The org.0byte.proof assertion, when 0byte-stamped.
        validation_codes:
          type: array
          items: { type: string }
        detail: { type: string }

    RegistryEvidence:
      type: object
      description: The registry fingerprint soft-binding signal.
      required: [status]
      properties:
        status:
          type: string
          enum: [match, no_match, skipped, error]
          description: |
            `skipped` — the content has essentially no luminance variation,
            so its fingerprint carries no identity and the registry was not
            consulted (`detail` says so). Never a match by accident.
        proof_id: { type: string }
        distance:
          type: integer
          description: Hamming distance of the fingerprint match (0 = exact).
        match_confidence:
          type: number
          description: Fingerprint match quality — NOT an AI-likelihood estimate.
        detail: { type: string }

    VerifyResponse:
      type: object
      required: [verdict, origin_sources, evidence, matched, confidence]
      properties:
        verdict:
          type: string
          enum:
            [verified_origin, provenance_untrusted, provenance_invalid,
             no_provenance_found]
        origin_sources:
          type: array
          items: { type: string, enum: [registry, manifest] }
          description: Signals supporting a verified_origin verdict.
        evidence:
          type: object
          required: [manifest, registry]
          properties:
            manifest: { $ref: '#/components/schemas/ManifestEvidence' }
            registry: { $ref: '#/components/schemas/RegistryEvidence' }
        matched:
          type: boolean
          description: 'Legacy: registry signal only.'
        confidence:
          type: number
          description: 'Legacy: registry match quality only.'
        proof:
          $ref: '#/components/schemas/Proof'

    TreeHead:
      type: object
      required: [tree_size, public_key]
      properties:
        tree_size: { type: integer, format: int64 }
        root_hash: { type: string, nullable: true }
        signature:
          type: string
          nullable: true
          description: Base64 Ed25519 over `sth:v1:{key_id}:{tree_size}:{root_hash}`
        key_id: { type: string, nullable: true }
        public_key: { type: string, description: Current verifying key (hex) }
        published_at: { type: string, nullable: true }

    InclusionProof:
      type: object
      required:
        [proof_id, leaf_hash, leaf_index, tree_size, root_hash, signature,
         key_id, inclusion_path]
      properties:
        proof_id: { type: string }
        leaf_hash: { type: string }
        leaf_index: { type: integer, format: int64 }
        tree_size: { type: integer, format: int64 }
        root_hash: { type: string }
        signature: { type: string }
        key_id: { type: string }
        published_at: { type: string }
        inclusion_path:
          type: array
          items: { type: string }
          description: |
            Sibling hashes (hex), leaf to root. Verify with the RFC 9162
            §2.1.3.2 algorithm using leaf_index and tree_size.

    ConsistencyProof:
      type: object
      required:
        [first_tree_size, second_tree_size, first_root_hash,
         second_root_hash, consistency_path]
      properties:
        first_tree_size: { type: integer, format: int64 }
        second_tree_size: { type: integer, format: int64 }
        first_root_hash: { type: string }
        second_root_hash: { type: string }
        first_head:
          type: object
          nullable: true
          additionalProperties: true
        second_head:
          type: object
          nullable: true
          additionalProperties: true
        consistency_path:
          type: array
          items: { type: string }

    SigningKeys:
      type: object
      required: [keys]
      properties:
        keys:
          type: array
          items:
            type: object
            required: [key_id, public_key, algorithm, created_at, current]
            properties:
              key_id: { type: string }
              public_key: { type: string }
              algorithm: { type: string, example: ed25519 }
              created_at: { type: string }
              retired_at: { type: string, nullable: true }
              current: { type: boolean }

    ApiKeySummary:
      type: object
      required: [id, name, created_at]
      properties:
        id: { type: string }
        name: { type: string }
        created_at: { type: string }
