{
  "openapi": "3.0.3",
  "info": {
    "title": "TurnCheckHK API",
    "version": "1.0.0",
    "description": "Integrate TurnCheckHK plagiarism &amp; AI-writing detection into your own product. Every request must carry your API key.\n\n### Authentication\nSend your key either as a Bearer token or in the `X-API-Key` header:\n\n```\nAuthorization: Bearer tck_live_xxx\n```\n```\nX-API-Key: tck_live_xxx\n```\n\nClick **Authorize** above, paste your key, then use **Try it out** on any endpoint to send a real request.\n\n### Live vs Sandbox\n- `tck_live_…` keys run **live** checks and deduct API credits (1 credit per check).\n- `tck_test_…` keys run in **sandbox**: deterministic mock results, **no credits charged**, no external calls. Great for wiring up your integration.\n\nIn sandbox you can steer the mock result with the `sandbox_scenario` field (see **POST /checks**).\n\n### Credits\nLive checks cost **1 API credit** each. API credits are a separate balance from site credits and **do not expire**. Top up from your developer dashboard.",
    "contact": { "name": "TurnCheckHK Support", "url": "https://turncheckhk.me" }
  },
  "x-service-info": {
    "name": "TurnCheckHK",
    "description": "Plagiarism (similarity) and AI-writing detection with Turnitin-based PDF reports.",
    "categories": ["document-verification", "plagiarism-detection", "ai-content-detection"]
  },
  "servers": [
    { "url": "/api/v1", "description": "This site" }
  ],
  "tags": [
    { "name": "General", "description": "Connectivity &amp; account." },
    { "name": "Checks", "description": "Submit documents and retrieve results &amp; reports." },
    { "name": "Logs", "description": "Recent API activity for your key." },
    { "name": "Guest store", "description": "Anonymous one-time check links — no API key, paid per order via Stripe Checkout (the auth.md anonymous flow). These paths live under /api, not /api/v1." }
  ],
  "security": [
    { "bearerAuth": [] }
  ],
  "paths": {
    "/guest-store/register": {
      "servers": [ { "url": "/api", "description": "Guest store (outside /api/v1)" } ],
      "post": {
        "tags": ["Guest store"],
        "summary": "Buy a one-time guest check link (anonymous, Stripe Checkout)",
        "description": "Creates a guest-store order plus a **Stripe Checkout session** — the anonymous registration flow documented in [auth.md](https://turncheckhk.me/auth.md). No authentication. Pay via the returned `url`, then claim the minted link with **GET /guest-store/claim/{order_token}**. `/api/guest-store/checkout` is the same endpoint under its original name. Per-use price and quantity bounds come from **GET /guest-store/config**. Rate limit: 5 attempts/min per IP.",
        "operationId": "guestStoreRegister",
        "security": [],
        "x-payment-info": {
          "intent": "session",
          "method": "stripe",
          "amount": 2,
          "currency": "USD",
          "description": "One-time plagiarism/AI check link, priced per use (quantity 1–20 per order). Amount reflects the current per-use price; read GET /api/guest-store/config for the live value."
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["quantity"],
                "properties": {
                  "quantity": { "type": "integer", "minimum": 1, "maximum": 20, "example": 1, "description": "Checks purchased; the minted link allows this many uses." },
                  "language": { "type": "string", "enum": ["en", "zh"], "example": "en" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order created; complete payment at `url`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": { "type": "string", "description": "Stripe Checkout URL — hand this to the human payer." },
                    "order_token": { "type": "string", "description": "Claim token for this order. Returned exactly once; persist it." },
                    "order_number": { "type": "string", "description": "Human-readable reference for receipt lookup." }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid quantity (outside the configured bounds)." },
          "429": { "description": "Rate limited (5 checkout attempts/min per IP)." },
          "503": { "description": "Store closed or card payment not configured." }
        }
      }
    },
    "/guest-store/config": {
      "servers": [ { "url": "/api", "description": "Guest store (outside /api/v1)" } ],
      "get": {
        "tags": ["Guest store"],
        "summary": "Guest store status &amp; live pricing",
        "description": "Whether the store is open, the current per-use price and currency, and quantity bounds. No authentication, free.",
        "operationId": "guestStoreConfig",
        "security": [],
        "responses": {
          "200": {
            "description": "Store configuration.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": { "type": "boolean" },
                    "unit_price_hkd": { "type": "number", "description": "Per-use price in `currency` (field name is historical)." },
                    "currency": { "type": "string", "example": "USD" },
                    "min_qty": { "type": "integer" },
                    "max_qty": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/guest-store/claim/{order_token}": {
      "servers": [ { "url": "/api", "description": "Guest store (outside /api/v1)" } ],
      "get": {
        "tags": ["Guest store"],
        "summary": "Claim the credential after payment",
        "description": "Poll after payment; when `order.status` is `paid`, `order.guest_url` is the one-time check link. `/api/guest-store/orders/{order_token}` is the same endpoint under its original name. No authentication, free.",
        "operationId": "guestStoreClaim",
        "security": [],
        "parameters": [
          { "name": "order_token", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Order state; `guest_url` is null until paid.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "order": {
                      "type": "object",
                      "properties": {
                        "status": { "type": "string", "enum": ["awaiting_payment", "paid", "failed"] },
                        "quantity": { "type": "integer" },
                        "guest_token": { "type": "string", "nullable": true },
                        "guest_url": { "type": "string", "nullable": true, "description": "The credential: https://turncheckhk.me/g/{token}" }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": { "description": "Unknown order token." }
        }
      }
    },
    "/ping": {
      "get": {
        "tags": ["General"],
        "summary": "Ping — verify connectivity &amp; key mode",
        "description": "Cheapest way to confirm your key works and whether it is a **live** or **sandbox** key. No credits charged.",
        "operationId": "ping",
        "responses": {
          "200": {
            "description": "Key is valid.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "example": true },
                    "mode": { "type": "string", "enum": ["live", "sandbox"], "example": "sandbox" },
                    "sandbox": { "type": "boolean", "example": true },
                    "request_id": { "type": "string", "example": "req_9f2c…" },
                    "timestamp": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/account": {
      "get": {
        "tags": ["General"],
        "summary": "Retrieve account &amp; credit balance",
        "description": "Returns your API credit balance and details about the key making the request. In sandbox, `credits` is `null` (no charge).",
        "operationId": "getAccount",
        "security": [ { "bearerAuth": ["account:read"] } ],
        "responses": {
          "200": {
            "description": "Account details.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Account" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/checks": {
      "post": {
        "tags": ["Checks"],
        "summary": "Submit a document for checking",
        "description": "Create a new check from **either** an uploaded file **or** inline text.\n\n- Pick `multipart/form-data` to upload a file (`.pdf`, `.doc`, `.docx`, `.txt`, `.rtf`, `.pptx`; max 50&nbsp;MB).\n- Pick `application/json` to send raw `text`.\n\nThe check is created asynchronously — poll **GET /checks/{id}** (or use a webhook) until `status` is `completed` or `failed`.\n\n**Sandbox tip:** with a `tck_test_` key, set `sandbox_scenario` to force a result: `completed`, `completed_high`, `completed_low`, `ai_unavailable`, `processing`, or `failed`.",
        "operationId": "createCheck",
        "security": [ { "bearerAuth": ["checks:write"] } ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": { "type": "string", "format": "binary", "description": "Document to check (.pdf, .doc, .docx, .txt, .rtf, .pptx; ≤ 50MB)." },
                  "title": { "type": "string", "description": "Optional label shown in your dashboard.", "maxLength": 200 },
                  "exclude_bibliography": { "type": "boolean", "default": true, "description": "Exclude the bibliography from similarity matching." },
                  "exclude_quoted_text": { "type": "boolean", "default": false, "description": "Exclude quoted text from similarity matching." },
                  "sandbox_scenario": { "type": "string", "enum": ["completed", "completed_high", "completed_low", "ai_unavailable", "processing", "failed"], "description": "Sandbox only — force a mock outcome." }
                },
                "required": ["file"]
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": { "type": "string", "description": "Raw text to check (used when no file is uploaded).", "example": "The quick brown fox jumps over the lazy dog." },
                  "filename": { "type": "string", "description": "Optional filename for inline text (defaults to document.txt).", "example": "essay.txt" },
                  "title": { "type": "string", "maxLength": 200 },
                  "exclude_bibliography": { "type": "boolean", "default": true },
                  "exclude_quoted_text": { "type": "boolean", "default": false },
                  "sandbox_scenario": { "type": "string", "enum": ["completed", "completed_high", "completed_low", "ai_unavailable", "processing", "failed"], "description": "Sandbox only — force a mock outcome.", "example": "completed_high" }
                },
                "required": ["text"]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Check created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "check": { "$ref": "#/components/schemas/Check" } }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": {
            "description": "Insufficient API credits (live mode).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "insufficient_credits", "message": "Not enough API credits." } } } }
          },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "413": {
            "description": "File exceeds the 50MB limit.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "file_too_large", "message": "File exceeds the 50MB limit." } } } }
          }
        }
      },
      "get": {
        "tags": ["Checks"],
        "summary": "List your checks",
        "description": "Returns checks created via the API, newest first.",
        "operationId": "listChecks",
        "security": [ { "bearerAuth": ["checks:read"] } ],
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 30, "minimum": 1, "maximum": 200 }, "description": "Max results (≤ 200)." },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0, "minimum": 0 }, "description": "Number to skip (pagination)." }
        ],
        "responses": {
          "200": {
            "description": "A page of checks.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": { "type": "string", "example": "list" },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Check" } },
                    "total": { "type": "integer", "example": 42 },
                    "limit": { "type": "integer", "example": 30 },
                    "offset": { "type": "integer", "example": 0 }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/checks/{id}": {
      "get": {
        "tags": ["Checks"],
        "summary": "Retrieve a check",
        "description": "Fetch a single check by id. Poll this until `status` is `completed` or `failed`.",
        "operationId": "getCheck",
        "security": [ { "bearerAuth": ["checks:read"] } ],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The check id returned by POST /checks." }
        ],
        "responses": {
          "200": {
            "description": "The check.",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "check": { "$ref": "#/components/schemas/Check" } } } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/checks/{id}/report/{type}": {
      "get": {
        "tags": ["Checks"],
        "summary": "Download a PDF report",
        "description": "Download a report PDF for a **completed** check. `type` is one of `ai`, `plagiarism`, or `highlights`.\n\n**Sandbox:** with a `tck_test_` key this returns a clearly-labelled **sample PDF** (mock data) so you can test the download flow end to end — try `ai` and `plagiarism` on a completed sandbox check.",
        "operationId": "downloadReport",
        "security": [ { "bearerAuth": ["checks:read"] } ],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "type", "in": "path", "required": true, "schema": { "type": "string", "enum": ["ai", "plagiarism", "highlights"] } }
        ],
        "responses": {
          "200": {
            "description": "The report PDF.",
            "content": { "application/pdf": { "schema": { "type": "string", "format": "binary" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "Check is not completed / report not ready yet.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "not_ready", "message": "Check is not completed yet." } } } }
          }
        }
      }
    },
    "/logs": {
      "get": {
        "tags": ["Logs"],
        "summary": "Recent API access logs",
        "description": "The most recent API requests made with your keys — handy for debugging integrations.",
        "operationId": "listLogs",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "minimum": 1, "maximum": 200 } }
        ],
        "responses": {
          "200": {
            "description": "Recent log entries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": { "type": "string", "example": "list" },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/LogEntry" } },
                    "limit": { "type": "integer", "example": 50 }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Paste your API key (e.g. `tck_live_…` or `tck_test_…`). Swagger sends it as `Authorization: Bearer <key>`.\n\nYou only need this **one** key. If you'd rather send it in your own code as the `X-API-Key` header instead of `Authorization`, that also works — the server accepts either."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "authentication_error", "message": "Missing API key." } } } }
      },
      "Forbidden": {
        "description": "The key lacks the required scope, or API access has been revoked.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "insufficient_scope", "message": "This key is missing the required scope." } } } }
      },
      "BadRequest": {
        "description": "Invalid request.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "invalid_request", "message": "Provide either a \"file\" upload or a \"text\" field." } } } }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "not_found", "message": "Check not found." } } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "type": { "type": "string", "description": "Machine-readable error type.", "example": "invalid_request" },
              "message": { "type": "string", "description": "Human-readable message.", "example": "Provide either a file upload or a text field." }
            }
          }
        }
      },
      "Account": {
        "type": "object",
        "properties": {
          "object": { "type": "string", "example": "account" },
          "user_id": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "credits": { "type": "integer", "nullable": true, "description": "API credit balance (null in sandbox).", "example": 250 },
          "api_credits": { "type": "integer", "nullable": true, "example": 250 },
          "mode": { "type": "string", "enum": ["live", "sandbox"] },
          "sandbox": { "type": "boolean" },
          "api_key": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "name": { "type": "string" },
              "prefix": { "type": "string", "example": "tck_live_a1b2" },
              "scopes": { "type": "array", "items": { "type": "string" }, "example": ["checks:read", "checks:write", "account:read"] }
            }
          }
        }
      },
      "Check": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "chk_a1b2c3" },
          "object": { "type": "string", "example": "check" },
          "mode": { "type": "string", "enum": ["live", "sandbox"] },
          "sandbox": { "type": "boolean" },
          "status": { "type": "string", "enum": ["pending", "processing", "completed", "failed"], "example": "completed" },
          "title": { "type": "string", "nullable": true },
          "file_name": { "type": "string", "nullable": true, "example": "essay.pdf" },
          "file_size": { "type": "integer", "nullable": true, "example": 18234 },
          "similarity_score": { "type": "number", "nullable": true, "description": "Plagiarism/similarity percentage (0–100).", "example": 12 },
          "ai_score": { "type": "number", "nullable": true, "description": "AI-writing percentage (0–100).", "example": 4 },
          "ai_report_unavailable": { "type": "boolean" },
          "ai_report_status": { "type": "string", "nullable": true },
          "word_count": { "type": "integer", "nullable": true, "example": 842 },
          "error_message": { "type": "string", "nullable": true },
          "created_at": { "type": "string", "format": "date-time", "nullable": true },
          "completed_at": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "LogEntry": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "request_id": { "type": "string" },
          "mode": { "type": "string", "enum": ["live", "sandbox"] },
          "method": { "type": "string", "example": "POST" },
          "path": { "type": "string", "example": "/api/v1/checks" },
          "status_code": { "type": "integer", "example": 201 },
          "response_time_ms": { "type": "integer", "example": 87 },
          "key_prefix": { "type": "string", "example": "tck_live_a1b2" },
          "error": { "type": "string", "nullable": true },
          "created_at": { "type": "string", "format": "date-time" }
        }
      }
    }
  }
}
