{
  "info": {
    "name": "check.qr.abundera.ai API",
    "description": "QR / barcode safety classification service.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_postman_id": "abundera-generated",
    "version": "1.0.0"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{token}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://check.qr.abundera.ai",
      "type": "string"
    },
    {
      "key": "token",
      "value": "",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": ".well-known",
      "item": [
        {
          "name": "Fetch Abundera QR Check product capabilities document",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "If-None-Match",
                "value": "string",
                "description": "Weak ETag from a prior response. A match returns 304.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{base_url}}/.well-known/abundera-capabilities.json",
              "host": ["{{base_url}}"],
              "path": [".well-known", "abundera-capabilities.json"],
              "query": []
            },
            "description": "JSON document describing the check.qr.abundera.ai API surface, OAuth-style\nscopes, and rate-limit tiers. Follows RFC 8615 (well-known URI) and is\naggregated by abundera.ai's federated /account/api-keys UI.\n\nConsumers must ignore unknown fields (RFC 8259 \u00a74). Minor `schema_version`\nbumps are backward-compatible.\n\nPublic; no authentication required. CORS-open so cross-origin aggregators\nand integrators can fetch directly.\n\nCaching: `Cache-Control: public, max-age=60, stale-while-revalidate=600`.\nA weak ETag (first 32 hex chars of body SHA-256) supports `If-None-Match`\n304 responses.\n"
          },
          "response": []
        }
      ]
    },
    {
      "name": "coverage",
      "item": [
        {
          "name": "Public QR Type Catalog manifest",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/coverage",
              "host": ["{{base_url}}"],
              "path": ["api", "coverage"],
              "query": []
            },
            "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.\n"
          },
          "response": []
        }
      ]
    },
    {
      "name": "feedback",
      "item": [
        {
          "name": "Submit feedback on a scan verdict",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/feedback",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "feedback"],
              "query": []
            },
            "description": "Collects user feedback on a verdict produced by the scan API. No authentication\nis required. The scanned payload is never stored, only a SHA-256 hash is kept,\nmatching the privacy posture of the verdict cache (QR-17/18 \u00a76.4.x).\n\nEach report is stored in the RATE_LIMIT_KV namespace under a key of the form\n`feedback:<ts>:<hash-prefix>` with a 90-day TTL. No D1 dependency.\n\nRate-limited to 10 requests per hour per IP address. Operator IPs (as configured\nin the environment) bypass the rate limit. If the KV binding is unavailable the\nendpoint returns 503. Rate-limit failures are non-fatal and fail open so legitimate\nfeedback is not blocked.\n",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"payload_hash\": \"string\",\n  \"verdict_class\": \"string\",\n  \"reason_class\": \"false_positive\",\n  \"comment\": \"string\",\n  \"email\": \"user@example.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "founding-members",
      "item": [
        {
          "name": "Check founding member offer availability for Check QR",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/founding-members",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "founding-members"],
              "query": []
            },
            "description": "Returns whether the Check QR founding member offer is still open, how many days remain until the deadline, and which tiers are available.\n\nThe 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.\n\nNo authentication required. Public endpoint. Responses are cached for one hour (Cache-Control: public, max-age=3600). No side effects.\n"
          },
          "response": []
        },
        {
          "name": "CORS preflight for founding member endpoint",
          "request": {
            "method": "OPTIONS",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/founding-members",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "founding-members"],
              "query": []
            },
            "description": "Handles CORS preflight requests for GET /api/v1/founding-members. Returns the allowed methods and headers. No authentication required.\n"
          },
          "response": []
        }
      ]
    },
    {
      "name": "mutation-alerts",
      "item": [
        {
          "name": "Confirm a mutation-alert subscription via double-opt-in",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/mutation-alerts/confirm",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "mutation-alerts", "confirm"],
              "query": []
            },
            "description": "Completes the double-opt-in flow for a mutation-alert subscription. The caller\nsupplies the subscription_id and the confirm_token that was delivered out-of-band\n(e.g. via email link). The token is compared in constant time against the pending\nKV record to prevent timing attacks.\n\nOn first confirmation the endpoint flips the subscription state from pending to\nactive, mints a 48-hex-char unsubscribe token, removes the confirm token from\nstorage, and extends the KV TTL to 365 days so the record survives until the\ndigest cron job picks it up.\n\nIf the subscription is already active the call is idempotent: the existing\nunsubscribe token is returned along with `already: true`.\n\nNo authentication is required. The confirm_token (minimum 48 chars) serves as\nthe bearer credential. No PII is included in any response body.\n",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"subscription_id\": \"string\",\n  \"confirm_token\": \"string\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Subscribe to QR payload mutation alerts",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/mutation-alerts/subscribe",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "mutation-alerts", "subscribe"],
              "query": []
            },
            "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.\n\nDouble-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.\n\nNo 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.\n",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"payload_hash\": \"string\",\n  \"email\": \"user@example.com\",\n  \"frequency\": \"daily\",\n  \"label\": \"string\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Unsubscribe from mutation alerts",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/mutation-alerts/unsubscribe",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "mutation-alerts", "unsubscribe"],
              "query": []
            },
            "description": "Removes an active mutation-alert subscription identified by `subscription_id`.\nThe caller must supply the `unsubscribe_token` that was minted at confirmation\ntime; the token is compared in constant time to prevent timing attacks.\n\nNo authentication header is required, the unsubscribe token itself is the\ncredential. Tokens rotate with each email digest delivery so a leaked token\nfrom an old digest cannot be used against a still-active subscriber.\n\nThe operation is idempotent: if the subscription has already been deleted,\nthe endpoint returns HTTP 200 with `already: true` rather than an error,\nso a double-click or bookmark refresh on the unsubscribe page does not\nsurface an error to the user.\n\nNo rate limiting is applied at the handler level. The endpoint is\nunauthenticated and open-CORS.\n",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"subscription_id\": \"string\",\n  \"unsubscribe_token\": \"string\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "payments",
      "item": [
        {
          "name": "Create a Stripe Checkout Session for a Founding Member subscription",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/payments/founding-checkout",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "payments", "founding-checkout"],
              "query": []
            },
            "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.\n\nThe founding offer has a hard deadline of 2026-09-01T00:00:00Z. Requests after that date receive 410 Gone.\n\nTier-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.\n\n3DS/SCA is forced for all card payments (`request_three_d_secure: any`) to shift fraud-dispute liability. Stripe Radar handles further risk scoring.\n\nRate 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).\n\nSession 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.\n",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"tier\": \"personal\",\n  \"email\": \"user@example.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "scan",
      "item": [
        {
          "name": "Classify a decoded QR / barcode payload",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/scan",
              "host": ["{{base_url}}"],
              "path": ["api", "scan"],
              "query": []
            },
            "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).\n",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"payload\": \"string\",\n  \"client_decoded_type\": \"string\",\n  \"decoder_format\": \"qr\",\n  \"decoder_source\": \"camera\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "whereami",
      "item": [
        {
          "name": "Return visitor's country code from Cloudflare geo header",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/whereami",
              "host": ["{{base_url}}"],
              "path": ["api", "whereami"],
              "query": []
            },
            "description": "Returns the visitor's ISO 3166-1 alpha-2 country code as detected by Cloudflare's\nedge network via the `cf-ipcountry` request header.\n\nIntended for picking a demo redirect destination guaranteed to be in a different\ncountry than the visitor. No authentication required.\n\nNo PII is captured or logged. The only Cloudflare geo field read is the two-letter\ncountry code. IP address, ASN, city, latitude, longitude, and all other enrichment\nfields are explicitly ignored.\n\nCloudflare may set the header to `\"XX\"` for unknown origins or `\"T1\"` for Tor exit\nnodes; both values are passed through unchanged. When running outside a Cloudflare\ncontext (local dev, missing CF edge), the header is absent and `country` is `null`.\n\nNo rate limits apply. No side effects. Responses are not cached (`cache-control: no-store`).\n"
          },
          "response": []
        }
      ]
    }
  ]
}
