openapi: 3.0.3
info:
  title: Abundera Check API
  version: 1.0.0
  description: QR / barcode safety classification service.
  contact:
    name: Abundera
    email: support@abundera.ai
servers:
- url: https://check.qr.abundera.ai
  description: Production
paths:
  /.well-known/abundera-capabilities.json:
    get:
      tags:
      - .well-known
      summary: Fetch Abundera Check product capabilities document
      description: 'JSON document describing the check.qr.abundera.ai API surface, OAuth-style

        scopes, and rate-limit tiers. Follows RFC 8615 (well-known URI) and is

        aggregated by abundera.ai''s federated /account/api-keys UI.


        Consumers must ignore unknown fields (RFC 8259 §4). Minor schema_version

        bumps are backward-compatible.


        Public; no authentication required. CORS-open so cross-origin aggregators

        and integrators can fetch directly.


        Caching: `Cache-Control: public, max-age=60, stale-while-revalidate=600`.

        A weak ETag (first 32 hex of body SHA-256) supports `If-None-Match` 304s.

        '
      responses:
        '200':
          description: Capabilities document
          content:
            application/json:
              schema:
                type: object
                properties:
                  $schema:
                    type: string
                  schema_version:
                    type: string
                  product:
                    type: object
                  api:
                    type: object
                  auth:
                    type: object
                  rate_limits:
                    type: object
        '304':
          description: Not modified (ETag matched)
        '404':
          description: Error
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    const: false
                    description: False on error.
                  error:
                    type: string
                    description: Machine-readable error code.
                  retry_after:
                    type: integer
                    description: Seconds to wait before retry. Present on 429.
  /.well-known/gpc.json:
    get:
      tags:
      - .well-known
      summary: known/gpc.json
      description: 'Global Privacy Control signal acknowledgement. Declares that this

        site honors the GPC header (Sec-GPC: 1) as a valid opt-out of sale

        and sharing of personal information under CCPA/CPRA. Format per

        https://privacycg.github.io/gpc-spec/.'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                properties:
                  ok:
                    type: boolean
                    description: True when the request succeeded.
        '429':
          description: Rate limited
        '404':
          description: Error
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    const: false
                    description: False on error.
                  error:
                    type: string
                    description: Machine-readable error code.
                  retry_after:
                    type: integer
                    description: Seconds to wait before retry. Present on 429.
  /.well-known/llms.txt:
    get:
      tags:
      - .well-known
      summary: known/llms.txt
      description: 'Permanent alias to /llms.txt. Some LLM crawlers probe the well-known

        location before the root; this 301 keeps a single canonical file.

        See ~/projects/siteops/docs/LLMS-TXT-STANDARD.md.'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                properties:
                  ok:
                    type: boolean
                    description: True when the request succeeded.
        '429':
          description: Rate limited
        '404':
          description: Error
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    const: false
                    description: False on error.
                  error:
                    type: string
                    description: Machine-readable error code.
                  retry_after:
                    type: integer
                    description: Seconds to wait before retry. Present on 429.
  /.well-known/qr-safety-policy.json:
    get:
      tags:
      - .well-known
      summary: policy.json
      description: 'Family-canonical declaration of how this Abundera Check instance

        classifies QR / barcode payload safety. Lets external integrators

        (browsers, MDMs, mail filters, sister-product safety pipelines)

        consume verdicts knowing exactly what the verdict schema means.


        Pair with /.well-known/qr-redirect-policy.json on qr.abundera.ai ,

        one declares "how I mint redirects", the other declares "how I

        classify them."


        Schema: ~/projects/siteops/docs/WELL-KNOWN-CATALOG.md'
      responses:
        '200':
          description: OK
        '204':
          description: No content
        '304':
          description: Not modified
        '429':
          description: Rate limited
        '404':
          description: Error
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    const: false
                    description: False on error.
                  error:
                    type: string
                    description: Machine-readable error code.
                  retry_after:
                    type: integer
                    description: Seconds to wait before retry. Present on 429.
  /api/billing/session:
    get:
      tags:
      - billing
      summary: Sanitized read of a Stripe Checkout Session for the post-checkout
      description: "Sanitized read of a Stripe Checkout Session for the post-checkout\nfounding-member success modal. Mirrors\
        \ pro.qr + sign — same response\nshape, same 1-hour replay window, same operator-IP test-mode routing\nvia resolveStripeContext.\n\
        \nPublic endpoint: no end-user auth, because the session_id IS the\nauth (Stripe-issued random token, single-use,\
        \ expires).\n\nHardening:\n  - Refuses session_ids that don't start with \"cs_\".\n  - Refuses sessions where payment_status\
        \ !== \"paid\" (no leaking\n    names / prices for abandoned checkouts).\n  - Refuses sessions older than 1 hour (replay\
        \ window).\n  - 404 on fetch failure so the client modal's graceful-fallback\n    path stays tight.\n\nResponse: {\
        \ status, cohort, plan_label, price_label, customer_name }\n  - price_label uses session.amount_total (post-coupon),\
        \ so the\n    modal shows what the buyer actually paid — matching the\n    pricing-page rate and the Stripe Checkout\
        \ receipt."
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                properties:
                  ok:
                    type: boolean
                    description: True when the request succeeded.
        '429':
          description: Rate limited
        '404':
          description: Error
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    const: false
                    description: False on error.
                  error:
                    type: string
                    description: Machine-readable error code.
                  retry_after:
                    type: integer
                    description: Seconds to wait before retry. Present on 429.
  /api/coverage:
    get:
      tags:
      - coverage
      summary: Public QR Type Catalog manifest
      description: 'Returns the live QR Type Catalog grouped by category, with status counts and credential-risk highlights.
        Powers the public /coverage/ page and is stable as a JSON API for third-party docs. Single source of truth is `~/projects/abundera-shared/data/qr-type-catalog.json`.
        Cached at the edge for 1 hour.

        '
      responses:
        '200':
          description: Coverage manifest
          content:
            application/json:
              schema:
                type: object
                required:
                - catalog_version
                - totals
                - categories
                - types
                properties:
                  catalog_version:
                    type: string
                    description: ISO date (YYYY-MM-DD)
                  totals:
                    type: object
                    properties:
                      total:
                        type: integer
                      shipped:
                        type: integer
                      partial:
                        type: integer
                      backlog:
                        type: integer
                      credential_risk:
                        type: integer
                  categories:
                    type: object
                    additionalProperties:
                      type: integer
                  national_rollouts:
                    type: object
                    additionalProperties:
                      type: integer
                  types:
                    type: array
                    items:
                      type: object
                      required:
                      - id
                      - category
                      - human_name
                      - status
                      properties:
                        id:
                          type: string
                        category:
                          type: string
                        subcategory:
                          type: string
                        human_name:
                          type: string
                        summary:
                          type: string
                        status:
                          type: string
                          enum:
                          - shipped
                          - partial
                          - backlog
                        credential_risk:
                          type: boolean
                        scanner_supported:
                          type: boolean
                        generator_supported:
                          type: boolean
                        patents:
                          type: array
                          items:
                            type: string
        '404':
          description: Error
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    const: false
                    description: False on error.
                  error:
                    type: string
                    description: Machine-readable error code.
                  retry_after:
                    type: integer
                    description: Seconds to wait before retry. Present on 429.
  /api/scan:
    post:
      tags:
      - scan
      summary: Classify a decoded QR / barcode payload
      description: 'Production embodiment of patents QR-17 (multi-hop redirect-chain mutability) and QR-18 (multi-modal payload
        threat classification). Public endpoint, no auth. Anonymous tier rate-limited to 10/min burst AND 3/24h per IP (both
        gates; daily is strictest). No Turnstile. Raw payload is never persisted; verdict cache is keyed by SHA-256(payload_type
        || discriminator || server_salt).

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - payload
              additionalProperties: false
              properties:
                payload:
                  type: string
                  maxLength: 4096
                  description: Decoded payload text (URL, vCard, WiFi config, etc.)
                client_decoded_type:
                  type: string
                  description: Optional client-side hint about payload kind.
                decoder_format:
                  type: string
                  description: Symbology the browser-side decoder reported.
                  enum:
                  - qr
                  - aztec
                  - code_128
                  - code_39
                  - code_93
                  - codabar
                  - data_matrix
                  - ean_13
                  - ean_8
                  - itf
                  - pdf417
                  - upc_a
                  - upc_e
                decoder_source:
                  type: string
                  enum:
                  - camera
                  - upload
                  - paste
                  - type
                  - deep_link
                  - vision_model
                embedded_logo_hint:
                  type: object
                  description: Optional. QR-22-CIP browser-side classifier output for the QR's embedded brand logo. Cross-checked
                    server-side against brand-list.json canonical_host vs. the resolved URL host. Ignored unless EMBEDDED_LOGO_DETECTION_ENABLED
                    is on.
                  properties:
                    brand_label:
                      type: string
                      maxLength: 64
                    confidence:
                      type: number
                      minimum: 0
                      maximum: 1
                    model_version:
                      type: string
                      maxLength: 64
      responses:
        '200':
          description: Verdict
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - payload_type
                - threat_class
                - findings
                - schema_version
                properties:
                  ok:
                    type: boolean
                    enum:
                    - true
                  payload_type:
                    type: string
                  subtype:
                    type: string
                  threat_class:
                    type: string
                    enum:
                    - safe
                    - caution
                    - danger
                    - unknown
                  mutability:
                    type: string
                    enum:
                    - immutable
                    - dynamic
                    - redirector
                    - unknown
                  attribution:
                    type: object
                  chain:
                    type: array
                    items:
                      type: object
                  findings:
                    type: array
                    items:
                      type: object
                  sub_payloads:
                    type: array
                    items:
                      type: object
                  disclosure:
                    type: string
                  latency_ms:
                    type: integer
                  payload_preview:
                    type: string
                  schema_version:
                    type: integer
                    enum:
                    - 1
                  decoder_format:
                    type: string
                  decoder_source:
                    type: string
                  visitor:
                    type: object
                  learn_more_url:
                    type: string
                    format: uri
                  info_snippet:
                    type: string
        '400':
          description: Invalid JSON or schema validation failure
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - invalid_json
                    - validation
        '429':
          description: Rate limited
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - rate_limited
                  retry_after:
                    type: integer
                    description: Seconds until the limit resets.
        '500':
          description: Server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - config_error
                    - internal_error
  /api/v1/auth/whoami:
    get:
      tags:
      - auth
      summary: RBAC introspection for the current credential
      description: 'Side-effect-free. Returns `{ authenticated, product, user_id, email,

        name, auth_source, role, tier, scopes, key, rate_limit }` for the

        presented JWT/API key, or `{ authenticated:false, reason }` (200)

        when absent/invalid. Never cached.

        '
      security:
      - bearerAuth: []
      responses:
        '200':
          description: Introspection result
          content:
            application/json:
              schema:
                type: object
                properties:
                  authenticated:
                    type: boolean
                  product:
                    type: string
                  user_id:
                    type: string
                    nullable: true
                  email:
                    type: string
                    nullable: true
                  auth_source:
                    type: string
                    nullable: true
                    enum:
                    - api_key
                    - jwt
                    - session
                  role:
                    type: string
                    nullable: true
                  tier:
                    type: string
                    nullable: true
                  scopes:
                    type: array
                    items:
                      type: string
                  key:
                    type: object
                    nullable: true
                  rate_limit:
                    type: object
                    nullable: true
        '404':
          description: Error
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    const: false
                    description: False on error.
                  error:
                    type: string
                    description: Machine-readable error code.
                  retry_after:
                    type: integer
                    description: Seconds to wait before retry. Present on 429.
  /api/v1/feedback:
    post:
      tags:
      - feedback
      summary: Submit feedback on a scan verdict
      description: 'Collects user feedback on a verdict produced by the scan API. No authentication

        is required. The scanned payload is never stored, only a SHA-256 hash is kept,

        matching the privacy posture of the verdict cache (QR-17/18 §6.4.x).


        Each report is stored in the RATE_LIMIT_KV namespace under a key of the form

        `feedback:<ts>:<hash-prefix>` with a 90-day TTL. No D1 dependency.


        Rate-limited to 10 requests per hour per IP address. Operator IPs (as configured

        in the environment) bypass the rate limit. If the KV binding is unavailable the

        endpoint returns 503. Rate-limit failures are non-fatal and fail open so legitimate

        feedback is not blocked.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - payload_hash
              - verdict_class
              - reason_class
              properties:
                payload_hash:
                  type: string
                  minLength: 64
                  maxLength: 64
                  pattern: ^[0-9a-fA-F]{64}$
                  description: SHA-256 hex digest of the originally scanned payload.
                verdict_class:
                  type: string
                  maxLength: 32
                  description: The verdict class the scan API returned (e.g. safe, caution, danger).
                reason_class:
                  type: string
                  maxLength: 24
                  enum:
                  - false_positive
                  - false_negative
                  - wrong_type
                  - stale_data
                  - missing_feature
                  - other
                  description: Reason category explaining why the verdict appears incorrect.
                comment:
                  type: string
                  maxLength: 600
                  description: Optional free-text elaboration from the user.
                email:
                  type: string
                  maxLength: 200
                  format: email
                  description: Optional contact email for follow-up.
      responses:
        '200':
          description: Feedback accepted and stored
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - report_id
                properties:
                  ok:
                    type: boolean
                    enum:
                    - true
                  report_id:
                    type: string
                    description: Unique identifier for this report, e.g. fb_<timestamp>_<hash-prefix>.
        '400':
          description: Request body is malformed or fails validation
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - invalid_json
                    - validation
                    - invalid_payload_hash
                  errors:
                    type: array
                    description: Present when error is "validation"; lists field-level failures.
                    items:
                      type: string
        '429':
          description: Per-IP rate limit exceeded (10 per hour)
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until the rate limit window resets.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - rate_limited
                  retry_after:
                    type: integer
                    description: Seconds until the rate limit window resets.
        '500':
          description: KV write failed
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - storage_error
                  detail:
                    type: string
                    description: Error message from the KV exception.
        '503':
          description: KV binding not configured
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - not_configured
  /api/v1/founding-members:
    get:
      tags:
      - founding-members
      summary: Check founding member offer availability for Check QR
      description: 'Returns whether the Check QR founding member offer is still open, how many days remain until the deadline,
        and which tiers are available.


        The client uses this response to render a live countdown and disable purchase buttons after the deadline passes. The
        deadline for this product is 2026-09-01T00:00:00Z.


        No authentication required. Public endpoint. Responses are cached for one hour (Cache-Control: public, max-age=3600).
        No side effects.

        '
      responses:
        '200':
          description: Offer status returned
          content:
            application/json:
              schema:
                type: object
                required:
                - available
                - deadline
                - days_remaining
                - product
                - tiers
                properties:
                  available:
                    type: boolean
                    description: True if the current time is before the deadline.
                  deadline:
                    type: string
                    format: date-time
                    description: ISO 8601 timestamp of when the founding member offer closes.
                  days_remaining:
                    type: integer
                    minimum: 0
                    description: Whole days left until the deadline, floored at 0 after it passes.
                  product:
                    type: string
                    description: Product identifier for this surface.
                    enum:
                    - check
                  tiers:
                    type: array
                    items:
                      type: string
                      enum:
                      - personal
                      - family
                      - starter
                      - pro
                    description: Founding member tiers available for this product.
        '404':
          description: Error
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    const: false
                    description: False on error.
                  error:
                    type: string
                    description: Machine-readable error code.
                  retry_after:
                    type: integer
                    description: Seconds to wait before retry. Present on 429.
    options:
      tags:
      - founding-members
      summary: CORS preflight for founding member endpoint
      description: 'Handles CORS preflight requests for GET /api/v1/founding-members. Returns the allowed methods and headers.
        No authentication required.

        '
      responses:
        '204':
          description: Preflight accepted
  /api/v1/mutation-alerts/confirm:
    post:
      tags:
      - mutation-alerts
      summary: Confirm a mutation-alert subscription via double-opt-in
      description: 'Completes the double-opt-in flow for a mutation-alert subscription. The caller

        supplies the subscription_id and the confirm_token that was delivered out-of-band

        (e.g. via email link). The token is compared in constant time against the pending

        KV record to prevent timing attacks.


        On first confirmation the endpoint flips the subscription state from pending to

        active, mints a 48-hex-char unsubscribe token, removes the confirm token from

        storage, and extends the KV TTL to 365 days so the record survives until the

        digest cron job picks it up.


        If the subscription is already active the call is idempotent: the existing

        unsubscribe token is returned along with `already: true`.


        No authentication is required. The confirm_token (minimum 48 chars) serves as

        the bearer credential. No PII is included in any response body.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - subscription_id
              - confirm_token
              properties:
                subscription_id:
                  type: string
                  maxLength: 80
                  description: Opaque identifier for the pending subscription record.
                confirm_token:
                  type: string
                  minLength: 48
                  maxLength: 80
                  description: Secret token delivered to the subscriber out-of-band; used to prove ownership.
      responses:
        '200':
          description: Subscription confirmed (or already active)
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - state
                - unsubscribe_token
                - subscription_id
                properties:
                  ok:
                    type: boolean
                    enum:
                    - true
                  state:
                    type: string
                    enum:
                    - active
                  already:
                    type: boolean
                    description: Present and true only when the subscription was already active before this call.
                  unsubscribe_token:
                    type: string
                    description: Token the subscriber can use to cancel the subscription.
                  subscription_id:
                    type: string
                    description: Echo of the subscription_id from the stored record.
        '400':
          description: Invalid JSON body or schema validation failure
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - invalid_json
                    - validation
                  errors:
                    type: array
                    description: Present when error is "validation"; lists field-level messages.
                    items:
                      type: string
        '403':
          description: confirm_token does not match the stored record
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - bad_token
        '404':
          description: No pending subscription found for the given subscription_id, or it has expired
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - not_found_or_expired
        '500':
          description: Internal error, corrupt KV record or storage write failure
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - corrupt_record
                    - storage_error
                  detail:
                    type: string
                    description: Present when error is "storage_error"; stringified exception message.
        '503':
          description: KV namespace not configured in this environment
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - not_configured
  /api/v1/mutation-alerts/subscribe:
    post:
      tags:
      - mutation-alerts
      summary: Subscribe to QR payload mutation alerts
      description: 'Captures intent to receive mutation watchlist alerts for a given QR payload hash (QR-17). Stores a pending
        subscription record in KV keyed by a hash of the payload hash and email address. The full scanned payload is never
        stored, only its SHA-256 hex digest, matching the verdict cache privacy posture.


        Double-opt-in flow: the response includes an opaque `confirm_token` that the caller is expected to email to the user.
        The subscription remains in `state: "pending"` until confirmed via `POST /api/v1/mutation-alerts/confirm`. Unconfirmed
        rows expire after 7 days. Confirmed subscriptions persist for 1 year.


        No authentication is required. Rate-limited to 10 subscription attempts per IP per hour. Operator IPs (configured
        via environment) bypass the rate limit. Requires `RATE_LIMIT_KV` binding to be configured; returns 503 if absent.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - payload_hash
              - email
              properties:
                payload_hash:
                  type: string
                  minLength: 64
                  maxLength: 64
                  pattern: ^[0-9a-fA-F]{64}$
                  description: SHA-256 hex digest of the scanned QR payload.
                email:
                  type: string
                  maxLength: 200
                  format: email
                  description: Subscriber email address. Stored lowercased and trimmed.
                frequency:
                  type: string
                  enum:
                  - daily
                  - weekly
                  default: daily
                  description: Desired alert frequency. Defaults to daily if omitted.
                label:
                  type: string
                  maxLength: 80
                  description: Optional free-text note the user associates with this subscription.
      responses:
        '200':
          description: Subscription created in pending state
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - subscription_id
                - confirm_token
                - state
                - next_step
                properties:
                  ok:
                    type: boolean
                    enum:
                    - true
                  subscription_id:
                    type: string
                    description: Stable identifier for this subscription, derived from payload hash prefix and email hash.
                  confirm_token:
                    type: string
                    description: Opaque token to be sent to the subscriber for double-opt-in confirmation.
                  state:
                    type: string
                    description: Subscription state; always "pending" until confirmed.
                    enum:
                    - pending
                  next_step:
                    type: string
                    description: Human-readable instruction directing caller to confirm via the confirm endpoint.
        '400':
          description: Validation or input error
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - invalid_json
                    - validation
                    - invalid_payload_hash
                  errors:
                    type: array
                    description: Field-level validation errors, present when error is "validation".
                    items:
                      type: string
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                - retry_after
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - rate_limited
                  retry_after:
                    type: integer
                    description: Seconds until the rate limit window resets.
        '500':
          description: KV storage write failed
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - storage_error
                  detail:
                    type: string
                    description: Error message from the underlying exception.
        '503':
          description: KV binding not configured
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - not_configured
  /api/v1/mutation-alerts/unsubscribe:
    post:
      tags:
      - mutation-alerts
      summary: Unsubscribe from mutation alerts
      description: 'Removes an active mutation-alert subscription identified by `subscription_id`.

        The caller must supply the `unsubscribe_token` that was minted at confirmation

        time; the token is compared in constant time to prevent timing attacks.


        No authentication header is required, the unsubscribe token itself is the

        credential. Tokens rotate with each email digest delivery so a leaked token

        from an old digest cannot be used against a still-active subscriber.


        The operation is idempotent: if the subscription has already been deleted,

        the endpoint returns HTTP 200 with `already: true` rather than an error,

        so a double-click or bookmark refresh on the unsubscribe page does not

        surface an error to the user.


        No rate limiting is applied at the handler level. The endpoint is

        unauthenticated and open-CORS.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - subscription_id
              - unsubscribe_token
              properties:
                subscription_id:
                  type: string
                  maxLength: 80
                  description: Opaque identifier for the subscription record.
                unsubscribe_token:
                  type: string
                  minLength: 48
                  maxLength: 80
                  description: Single-use token issued at confirmation time, rotated per digest delivery.
      responses:
        '200':
          description: Subscription deleted (or was already absent)
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - state
                properties:
                  ok:
                    type: boolean
                    enum:
                    - true
                  state:
                    type: string
                    enum:
                    - deleted
                  already:
                    type: boolean
                    description: Present and true when the subscription was already gone before this call.
        '400':
          description: Invalid JSON body or schema validation failure
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - invalid_json
                    - validation
                  errors:
                    type: array
                    items:
                      type: string
                    description: Field-level validation messages; present only when error is "validation".
        '403':
          description: Unsubscribe token does not match the stored record
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - bad_token
        '500':
          description: Corrupt KV record or storage deletion failure
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - corrupt_record
                    - storage_error
                  detail:
                    type: string
                    description: Error message string; present only for storage_error.
        '503':
          description: KV binding not configured
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                    - false
                  error:
                    type: string
                    enum:
                    - not_configured
  /api/v1/payments/founding-checkout:
    post:
      tags:
      - payments
      summary: Create a Stripe Checkout Session for a Founding Member subscription
      description: 'Creates a Stripe Checkout Session for a Check Founding Member subscription at one of four annual pricing
        tiers (personal, family, starter, pro). No authentication is required, this is a public endpoint.


        The founding offer has a hard deadline of 2026-09-01T00:00:00Z. Requests after that date receive 410 Gone.


        Tier-to-price mapping is resolved server-side via environment variables (`STRIPE_FOUNDING_PRICE_*`). The caller supplies
        only the tier name and an optional pre-fill email address. On success the response contains a Stripe-hosted Checkout
        URL the client should redirect to.


        3DS/SCA is forced for all card payments (`request_three_d_secure: any`) to shift fraud-dispute liability. Stripe Radar
        handles further risk scoring.


        Rate limit: 5 founding-checkout attempts per IP per hour via `RATE_LIMIT_KV`. Operator IPs are exempt. Rate-limit
        failures are fail-open (a KV error does not block the request).


        Session metadata records `type=founding_member`, `plan=<tier>`, and `product=qrcheck` at both the Session and Subscription
        levels. Promotion codes are disabled; billing address collection is automatic.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - tier
              properties:
                tier:
                  type: string
                  enum:
                  - personal
                  - family
                  - starter
                  - pro
                  description: Founding member pricing tier. Determines which Stripe price ID is used.
                email:
                  type: string
                  format: email
                  description: Optional customer email to pre-fill in Stripe Checkout.
      responses:
        '200':
          description: Stripe Checkout Session created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
                    description: Stripe-hosted Checkout URL. Redirect the user here to complete payment.
                  id:
                    type: string
                    description: Stripe Checkout Session ID.
        '400':
          description: Invalid request body or unsupported tier
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                    - invalid_json
                    - invalid_tier
                    - stripe_error
                  message:
                    type: string
                  detail:
                    type: string
                    description: Stripe error message, present only when error is stripe_error.
                  stripe_status:
                    type: integer
                    description: HTTP status returned by Stripe, present only when error is stripe_error.
        '410':
          description: Founding Member offer has closed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                    - founding_offer_closed
                  message:
                    type: string
        '429':
          description: Per-IP rate limit exceeded
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until the rate limit window resets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                    - rate_limited
                  retry_after:
                    type: integer
                    description: Seconds until the next attempt is allowed.
                  message:
                    type: string
        '500':
          description: Network error reaching Stripe or unparseable Stripe response
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                    - network_error
                    - stripe_error
                  detail:
                    type: string
                    description: Error detail string, present for network_error.
        '502':
          description: Stripe returned a 5xx error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                    - stripe_error
                  detail:
                    type: string
                  stripe_status:
                    type: integer
        '503':
          description: Stripe not configured or tier price ID missing
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                    - not_configured
                    - tier_not_configured
                  message:
                    type: string
  /api/whereami:
    get:
      tags:
      - whereami
      summary: Return visitor's country code from Cloudflare geo header
      description: 'Returns the visitor''s ISO 3166-1 alpha-2 country code as detected by Cloudflare''s

        edge network via the `cf-ipcountry` request header.


        Intended for picking a demo redirect destination guaranteed to be in a different

        country than the visitor. No authentication required.


        No PII is captured or logged. The only Cloudflare geo field read is the two-letter

        country code. IP address, ASN, city, latitude, longitude, and all other enrichment

        fields are explicitly ignored.


        Cloudflare may set the header to `"XX"` for unknown origins or `"T1"` for Tor exit

        nodes; both values are passed through unchanged. When running outside a Cloudflare

        context (local dev, missing CF edge), the header is absent and `country` is `null`.


        No rate limits apply. No side effects. Responses are not cached (`cache-control: no-store`).

        '
      responses:
        '200':
          description: Country code resolved (or null if CF context unavailable)
          content:
            application/json:
              schema:
                type: object
                required:
                - country
                properties:
                  country:
                    oneOf:
                    - type: string
                      minLength: 2
                      maxLength: 2
                      description: 'ISO 3166-1 alpha-2 country code, or a Cloudflare sentinel value ("XX" for unknown, "T1"
                        for Tor).

                        '
                    - type: 'null'
                      description: Null when the cf-ipcountry header is absent (local dev or missing Cloudflare edge context).
        '404':
          description: Error
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - error
                properties:
                  ok:
                    type: boolean
                    const: false
                    description: False on error.
                  error:
                    type: string
                    description: Machine-readable error code.
                  retry_after:
                    type: integer
                    description: Seconds to wait before retry. Present on 429.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ApiKey
      description: Long-lived API key.
tags:
- name: .well-known
- name: auth
- name: billing
- name: coverage
- name: feedback
- name: founding-members
- name: mutation-alerts
- name: payments
- name: scan
- name: whereami
