{
  "openapi": "3.1.0",
  "info": {
    "title": "Abundera QR 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 QR Check product capabilities document",
        "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",
        "parameters": [
          {
            "in": "header",
            "name": "If-None-Match",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Weak ETag from a prior response. A match returns 304."
          }
        ],
        "responses": {
          "200": {
            "description": "Capabilities document",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "schema_version",
                    "product",
                    "api",
                    "auth",
                    "rate_limits"
                  ],
                  "properties": {
                    "$schema": {
                      "type": "string",
                      "format": "uri"
                    },
                    "schema_version": {
                      "type": "string",
                      "example": "1.0"
                    },
                    "product": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "check"
                        },
                        "name": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": ["ga", "beta", "alpha", "deprecated"]
                        },
                        "dashboard_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "docs_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "support_url": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    },
                    "api": {
                      "type": "object",
                      "properties": {
                        "base_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "openapi_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "auth_methods": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "auth": {
                      "type": "object",
                      "properties": {
                        "api_key_prefix": {
                          "type": "string",
                          "example": "abnd_check_"
                        },
                        "scopes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": ["id", "label", "description"],
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              },
                              "description": {
                                "type": "string"
                              },
                              "default": {
                                "type": "boolean"
                              }
                            }
                          }
                        }
                      }
                    },
                    "rate_limits": {
                      "type": "object",
                      "properties": {
                        "unit": {
                          "type": "string",
                          "example": "requests"
                        },
                        "tiers": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": ["id", "window", "limit"],
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "window": {
                                "type": "string",
                                "example": "1m"
                              },
                              "limit": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified (ETag matched)"
          }
        }
      }
    },
    "/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.\n",
        "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"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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).\n",
        "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"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verdict",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "payload_type",
                    "threat_class",
                    "findings",
                    "schema_version"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": 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",
                      "const": 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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "enum": ["invalid_json", "validation"]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "const": "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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "enum": ["config_error", "internal_error"]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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\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",
        "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",
                      "const": 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",
                      "const": 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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "const": "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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "const": "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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "const": "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.\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",
        "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",
                      "const": "check",
                      "description": "Product identifier for this surface."
                    },
                    "tiers": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": ["personal", "family", "starter", "pro"]
                      },
                      "description": "Founding member tiers available for this product."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "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.\n",
        "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\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",
        "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",
                      "const": true
                    },
                    "state": {
                      "type": "string",
                      "const": "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",
                      "const": 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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "const": "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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "const": "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",
                      "const": 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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "const": "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.\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",
        "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",
                      "const": 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",
                      "const": "pending",
                      "description": "Subscription state; always \"pending\" until confirmed."
                    },
                    "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",
                      "const": 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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "const": "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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "const": "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",
                      "const": false
                    },
                    "error": {
                      "type": "string",
                      "const": "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`.\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",
        "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",
                      "const": 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",
                      "const": 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",
                      "const": 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",
                      "const": 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",
                      "const": 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.\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",
        "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\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",
        "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).\n"
                        },
                        {
                          "type": "null",
                          "description": "Null when the cf-ipcountry header is absent (local dev or missing Cloudflare edge context)."
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {}
  },
  "tags": [
    {
      "name": ".well-known",
      "description": "RFC 8615 well-known URIs. Public discovery endpoints aggregated by the\nabundera.ai hub for federated UIs (api-keys, webhooks, notifications).\n"
    },
    {
      "name": "coverage"
    },
    {
      "name": "feedback"
    },
    {
      "name": "founding-members"
    },
    {
      "name": "mutation-alerts"
    },
    {
      "name": "payments"
    },
    {
      "name": "scan"
    },
    {
      "name": "whereami"
    }
  ]
}
