openapi: "3.1.0"
info:
  title: "FrameAI REST API"
  version: "1.1.0"
  description: |
    Full developer reference for the FrameAI structural engineering API.

    **Two API surfaces are available:**

    1. **Job Pipeline API** (Studio only) — upload structural PDFs, poll job status,
       retrieve Eurocode check outputs, download DXF/IFC/PDF/BOM exports.
    2. **Eurocode Check API** (Free/Pro/Studio) — stateless per-endpoint checks for
       EN 1993-1-8, EN 1993-1-9, EN 1993-1-1, EN 1992-1-1, EN 1995-1-1, EN 1998-1.

    **Authentication:** all Studio endpoints require `Authorization: Bearer fai_…`.
    Free-tier check endpoints run under IP-keyed quota (10 calls/month).

    **Rate limits:** 60 requests/minute per token (sliding window). Hit the 429?
    back off and retry with `Retry-After` seconds.

    **Base URL:** `https://frameai-structural.polsia.app/api/v1`
  contact:
    name: "FrameAI Support"
    url: "https://frameai-structural.polsia.app/docs/api"
    email: "frameai@polsia.app"
  license:
    name: "Proprietary"

servers:
  - url: "https://frameai-structural.polsia.app/api/v1"
    description: "Production"

security:
  - bearerAuth: []

tags:
  - name: "Projects"
    description: "Project workspace management (Studio)"
  - name: "Jobs"
    description: "Job pipeline: upload → poll → result (Studio)"
  - name: "Exports"
    description: "Fabrication exports: BOM, IFC, DXF, shop drawings (Studio)"
  - name: "Eurocode Checks"
    description: "Stateless Eurocode check endpoints (Free/Pro/Studio)"
  - name: "Webhooks"
    description: "Outbound webhook events delivered to your endpoint"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Studio API key from **Settings → API Keys**. Format: `fai_…`.
        Free-tier Eurocode endpoints accept unauthenticated requests up to 10/month (IP-keyed).

  schemas:

    # ─── Domain objects ──────────────────────────────────────────────────────────

    Project:
      type: object
      required: [id, name, created_at]
      properties:
        id:
          type: integer
          example: 42
        name:
          type: string
          example: "Warehouse Amersfoort 2026"
        description:
          type: ["string", "null"]
          example: "Main hall + annexe"
        national_annex:
          type: ["string", "null"]
          enum: [NL, DE, FR, IT, BE, "recommended", null]
          description: "Country-specific partial factors. Null → recommended Eurocode defaults."
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        jobs:
          type: array
          items:
            $ref: "#/components/schemas/JobSummary"
          description: "Populated only on GET /projects/:id."

    JobSummary:
      type: object
      required: [id, status, created_at]
      properties:
        id:
          type: integer
          example: 1337
        status:
          type: string
          enum: [pending, processing, done, failed]
        project_id:
          type: ["integer", "null"]
        priority:
          type: integer
          description: "10 = Studio priority, 0 = standard"
        result_url:
          type: ["string", "null"]
          format: uri
          description: "Pre-signed R2 URL to the output PDF (available when status=done)"
        error:
          type: ["string", "null"]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Job:
      allOf:
        - $ref: "#/components/schemas/JobSummary"
      properties:
        parent_job_id:
          type: ["integer", "null"]
        revision_number:
          type: ["integer", "null"]
        extracted_data:
          type: ["object", "null"]
          description: "Raw Eurocode extraction result. Large — prefer /jobs/:id/run endpoint for structured summaries."
        connections_data:
          type: ["array", "null"]
        welds_data:
          type: ["array", "null"]
        export_urls:
          type: ["object", "null"]
          description: "Populated when status=done."
          properties:
            shop_pdf:
              type: string
            bom_xlsx:
              type: string
            bom_csv:
              type: string
            model_ifc:
              type: string
            package_zip:
              type: string

    Run:
      type: object
      required: [id, status]
      properties:
        id:
          type: integer
        run_id:
          type: integer
        status:
          type: string
          enum: [pending, processing, done, failed]
        project_id:
          type: ["integer", "null"]
        result_url:
          type: ["string", "null"]
          format: uri
        error:
          type: ["string", "null"]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        utilisation_summary:
          type: ["object", "null"]
          properties:
            member_count:
              type: integer
            passed_count:
              type: integer
            failed_count:
              type: integer
            max_utilisation:
              type: number
              description: "Highest η across all members"
            total_mass_kg:
              type: ["number", "null"]
        export_urls:
          type: ["object", "null"]
          description: "Populated when status=done."
          properties:
            shop_pdf:
              type: string
              example: "/api/v1/projects/42/export/shop.pdf?run_id=1337"
            bom_xlsx:
              type: string
            bom_csv:
              type: string
            model_ifc:
              type: string
            package:
              type: string

    Revision:
      type: object
      properties:
        id:
          type: integer
        parent_job_id:
          type: integer
        child_job_id:
          type: integer
        revision_index:
          type: integer
        source:
          type: string
          enum: [manual, s3, http, email]
        diff_json:
          type: ["object", "null"]
        status:
          type: string
          enum: [pending, processing, done, failed]
        created_at:
          type: string
          format: date-time

    CheckResponse:
      type: object
      required: [passed, utilisation, summary]
      properties:
        check_id:
          type: ["string", "null"]
        endpoint:
          type: string
          example: "/check/en-1993-1-8"
        passed:
          type: boolean
        utilisation:
          type: ["number", "null"]
          description: "Utilisation ratio as percentage (0–100). 100 = exactly at limit."
          example: 68.4
        summary:
          type: string
          example: "F_v,Rd = 94.2 kN > V_Ed = 50.0 kN — PASS (η = 0.53)"
        result:
          type: object
          description: "Full numerical result with intermediate values."
        eurocode_clauses_cited:
          type: array
          items:
            type: string
          example: ["EN 1993-1-8:2005 §3.6.1", "EN 1993-1-8:2005 §3.6.7"]
        api_version:
          type: string
          example: "1.0.0"

    WebhookEvent:
      type: object
      description: "Payload delivered via HTTP POST to your registered webhook URL."
      required: [event, data, timestamp]
      properties:
        event:
          type: string
          enum:
            - project.created
            - job.started
            - job.completed
            - job.failed
            - revision.detected
            - export.ready
          example: "job.completed"
        data:
          type: object
          description: "Event-specific payload. See Webhook Events section."
        timestamp:
          type: string
          format: date-time
        api_version:
          type: string
          example: "1.1.0"

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: "Project not found"

    UpgradeRequired:
      type: object
      required: [error, upgrade_url]
      properties:
        error:
          type: string
          example: "Studio plan required for API access"
        upgrade_url:
          type: string
          example: "https://frameai-structural.polsia.app/pricing"

  responses:
    Unauthorized:
      description: "Missing or invalid bearer token"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: "Unauthorized — provide Bearer token"

    Forbidden:
      description: "Token exists but resource belongs to another account"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    NotFound:
      description: "Resource not found"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    UpgradeRequired:
      description: "Studio subscription required"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/UpgradeRequired"

    RateLimited:
      description: "Rate limit exceeded — back off and retry"
      headers:
        Retry-After:
          schema:
            type: integer
          description: "Seconds to wait before retrying"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: "Rate limit exceeded"

# ─── Paths ───────────────────────────────────────────────────────────────────

paths:

  # ── Projects ──────────────────────────────────────────────────────────────

  /projects:
    get:
      tags: [Projects]
      summary: "List projects"
      description: "Returns all project workspaces for the authenticated Studio account, newest first."
      operationId: listProjects
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
          description: "Max results to return (max 200)"
        - name: created_after
          in: query
          schema:
            type: string
            format: date-time
          description: "ISO-8601 timestamp — only return projects created after this time"
      responses:
        "200":
          description: "Project list"
          content:
            application/json:
              schema:
                type: object
                properties:
                  projects:
                    type: array
                    items:
                      $ref: "#/components/schemas/Project"
                  count:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/UpgradeRequired"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      tags: [Projects]
      summary: "Create project"
      description: |
        Create a new project workspace. Fires the `project.created` webhook event.
      operationId: createProject
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  example: "Warehouse Amersfoort 2026"
                description:
                  type: string
                  example: "Main hall + annexe"
                national_annex:
                  type: string
                  enum: [NL, DE, FR, IT, BE, recommended]
                  description: "Country-specific partial factors. Default: recommended."
            example:
              name: "Warehouse Amersfoort 2026"
              national_annex: "NL"
      responses:
        "201":
          description: "Project created"
          content:
            application/json:
              schema:
                type: object
                properties:
                  project:
                    $ref: "#/components/schemas/Project"
        "400":
          description: "Validation error — name required"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/UpgradeRequired"

  /projects/{projectId}:
    get:
      tags: [Projects]
      summary: "Get project"
      description: "Full project object including its most recent 50 jobs."
      operationId: getProject
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: integer
          example: 42
      responses:
        "200":
          description: "Project with jobs"
          content:
            application/json:
              schema:
                type: object
                properties:
                  project:
                    $ref: "#/components/schemas/Project"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /projects/{projectId}/revisions:
    get:
      tags: [Projects]
      summary: "List project revisions"
      description: "Revision history across all jobs in this project, newest first (max 100)."
      operationId: listProjectRevisions
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: "Revision list"
          content:
            application/json:
              schema:
                type: object
                properties:
                  revisions:
                    type: array
                    items:
                      $ref: "#/components/schemas/Revision"
                  count:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /projects/{projectId}/runs:
    post:
      tags: [Projects]
      summary: "Start a run"
      description: |
        Trigger a new structural analysis run on a project.

        Accepts either:
        - `multipart/form-data` with a `pdf` file field
        - `application/json` body with `{ "pdf_url": "https://…" }`

        Returns **202 Accepted** immediately. Poll `GET /runs/{runId}` until
        `status` is `done` or `failed`.

        Fires `job.started` webhook on pickup and `job.completed` / `job.failed` on finish.
      operationId: createRun
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [pdf]
              properties:
                pdf:
                  type: string
                  format: binary
                  description: "Structural steel PDF drawing (max 20 MB)"
          application/json:
            schema:
              type: object
              required: [pdf_url]
              properties:
                pdf_url:
                  type: string
                  format: uri
                  example: "https://storage.example.com/drawings/warehouse.pdf"
      responses:
        "202":
          description: "Run accepted — poll poll_url until complete"
          content:
            application/json:
              schema:
                type: object
                properties:
                  run_id:
                    type: integer
                    example: 1337
                  job_id:
                    type: integer
                  status:
                    type: string
                    enum: [pending]
                  priority:
                    type: integer
                  project_id:
                    type: integer
                  poll_url:
                    type: string
                    example: "/api/v1/runs/1337"
        "400":
          description: "No PDF provided or invalid project ID"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Runs ──────────────────────────────────────────────────────────────────

  /runs/{runId}:
    get:
      tags: [Jobs]
      summary: "Poll run status"
      description: |
        Status and result summary for a specific run.

        **Poll every 5–10 s** until `status` is `done` or `failed`.
        Completed runs include `export_urls` and `utilisation_summary`.
      operationId: getRun
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: integer
          example: 1337
      responses:
        "200":
          description: "Run status"
          content:
            application/json:
              schema:
                type: object
                properties:
                  run:
                    $ref: "#/components/schemas/Run"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Jobs (legacy v1) ──────────────────────────────────────────────────────

  /jobs:
    get:
      tags: [Jobs]
      summary: "List jobs"
      description: "All jobs for the account, newest first."
      operationId: listJobs
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, processing, done, failed]
        - name: project_id
          in: query
          schema:
            type: integer
      responses:
        "200":
          description: "Job list"
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobs:
                    type: array
                    items:
                      $ref: "#/components/schemas/JobSummary"
                  count:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      tags: [Jobs]
      summary: "Upload PDF & start job"
      description: |
        Upload a structural steel PDF and start a full Eurocode analysis.
        Same as `POST /projects/{id}/runs` but without a project grouping.
      operationId: createJob
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [pdf]
              properties:
                pdf:
                  type: string
                  format: binary
                project_id:
                  type: integer
          application/json:
            schema:
              type: object
              required: [pdf_url]
              properties:
                pdf_url:
                  type: string
                  format: uri
                project_id:
                  type: integer
      responses:
        "202":
          description: "Job accepted"
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id:
                    type: integer
                  status:
                    type: string
                    enum: [pending]
                  priority:
                    type: integer
                  project_id:
                    type: ["integer", "null"]
                  poll_url:
                    type: string
        "401":
          $ref: "#/components/responses/Unauthorized"

  /jobs/{jobId}:
    get:
      tags: [Jobs]
      summary: "Get job"
      operationId: getJob
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: "Job"
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    $ref: "#/components/schemas/Job"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /jobs/{jobId}/dxf:
    get:
      tags: [Exports]
      summary: "Download DXF"
      description: "Fabrication DXF for the first member of a completed job."
      operationId: getJobDxf
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: "DXF file"
          content:
            application/dxf:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: "Job not in `done` status"

  /jobs/{jobId}/ifc:
    get:
      tags: [Exports]
      summary: "Download IFC"
      description: "IFC 4.0 BIM model for a completed job."
      operationId: getJobIfc
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: "IFC file"
          content:
            application/x-step:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: "Job not in `done` status"

  /jobs/{jobId}/diff/{previousJobId}:
    get:
      tags: [Jobs]
      summary: "Compare revisions"
      description: "Structured diff between two jobs in the same revision chain."
      operationId: diffJobs
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: integer
        - name: previousJobId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: "Revision diff"
          content:
            application/json:
              schema:
                type: object
                properties:
                  diff:
                    type: object
                    properties:
                      members:
                        type: object
                        properties:
                          added:
                            type: array
                          removed:
                            type: array
                          changed:
                            type: array
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: "One or both jobs not in `done` status"

  # ── Project exports ───────────────────────────────────────────────────────

  /projects/{projectId}/export/{format}:
    get:
      tags: [Exports]
      summary: "Download project export"
      description: |
        Stream a fabrication export for a project's most recent completed run,
        or a specific run via `?run_id=N`.

        | Format | Content-Type | Description |
        |--------|-------------|-------------|
        | `shop.pdf` | `application/pdf` | Shop drawing PDF |
        | `bom.xlsx` | Excel | Bill of materials with EN 10365 weights |
        | `bom.csv` | CSV | BOM as plain CSV |
        | `model.ifc` | STEP | IFC 4.0 CoordinationView 2.0 |
        | `package.zip` | ZIP | DSTV NC1 + IFC + BOM bundled |
      operationId: getProjectExport
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: integer
        - name: format
          in: path
          required: true
          schema:
            type: string
            enum: [shop.pdf, bom.xlsx, bom.csv, model.ifc, package.zip]
        - name: run_id
          in: query
          schema:
            type: integer
          description: "Specific run ID. Defaults to latest completed run."
      responses:
        "200":
          description: "File download"
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
            text/csv:
              schema:
                type: string
            application/x-step:
              schema:
                type: string
                format: binary
            application/zip:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/UpgradeRequired"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: "Run not in `done` status"

  # ── Eurocode Check API ─────────────────────────────────────────────────────

  /check/en-1993-1-8:
    post:
      tags: [Eurocode Checks]
      summary: "EN 1993-1-8 Bolted connection"
      description: |
        Compute F_v,Rd (shear), F_b,Rd (bearing), F_t,Rd (tension) and utilisation
        ratio per EN 1993-1-8:2005 §3.5/§3.6.
      operationId: checkEn1993_1_8
      security:
        - {}
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bolt_grade, bolt_size, V_Ed]
              properties:
                bolt_grade:
                  type: string
                  enum: ["4.6","5.6","6.8","8.8","10.9"]
                  example: "8.8"
                bolt_size:
                  type: string
                  example: "M20"
                shear_planes:
                  type: integer
                  enum: [1, 2]
                  default: 1
                plate_fu:
                  type: number
                  description: "Ultimate tensile strength of connected plate (MPa)"
                  example: 360
                plate_t:
                  type: number
                  description: "Plate thickness (mm)"
                  example: 10
                e1:
                  type: number
                  description: "End distance in load direction (mm)"
                  example: 60
                e2:
                  type: number
                  description: "Edge distance perpendicular to load (mm)"
                  example: 40
                p1:
                  type: number
                  description: "Pitch in load direction (mm)"
                  example: 80
                is_end_bolt:
                  type: boolean
                  default: true
                V_Ed:
                  type: number
                  description: "Design shear force per bolt (N)"
                  example: 50000
                F_t_Ed:
                  type: number
                  description: "Design tension force per bolt (N)"
                  default: 0
            example:
              bolt_grade: "8.8"
              bolt_size: "M20"
              shear_planes: 1
              plate_fu: 360
              plate_t: 10
              e1: 60
              e2: 40
              p1: 80
              is_end_bolt: true
              V_Ed: 50000
              F_t_Ed: 0
      responses:
        "200":
          description: "Check result"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckResponse"
        "429":
          $ref: "#/components/responses/RateLimited"

  /check/en-1993-1-9:
    post:
      tags: [Eurocode Checks]
      summary: "EN 1993-1-9 Fatigue"
      description: "Safe-life or damage-tolerant fatigue assessment per EN 1993-1-9:2005."
      operationId: checkEn1993_1_9
      security:
        - {}
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [details]
              properties:
                details:
                  type: array
                  items:
                    type: object
                    properties:
                      detail_id:
                        type: string
                        example: "T8.6-71"
                      stress_history:
                        type: array
                        items:
                          type: object
                          properties:
                            delta_sigma:
                              type: number
                            n_cycles:
                              type: integer
                      method:
                        type: string
                        enum: [safe_life, damage_tolerant]
                        default: safe_life
                      consequence:
                        type: string
                        enum: [high, low]
                        default: high
      responses:
        "200":
          description: "Fatigue check result"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckResponse"
        "429":
          $ref: "#/components/responses/RateLimited"

  /check/en-1993-1-1:
    post:
      tags: [Eurocode Checks]
      summary: "EN 1993-1-1 Steel member"
      description: |
        Cross-section + member buckling check per EN 1993-1-1:2005.
        Covers section classes 1–4, bending, shear, combined N+M, LTB, flexural/torsional buckling.
      operationId: checkEn1993_1_1
      security:
        - {}
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [members]
              properties:
                members:
                  type: array
                  items:
                    type: object
                    required: [mark, profile, length_mm]
                    properties:
                      mark:
                        type: string
                        example: "B1"
                      profile:
                        type: string
                        example: "IPE 300"
                      length_mm:
                        type: number
                        example: 5000
                      N_Ed:
                        type: number
                        description: "Axial force (kN), positive = tension"
                        default: 0
                      M_y_Ed:
                        type: number
                        description: "Major-axis bending moment (kNm)"
                        default: 0
                      V_z_Ed:
                        type: number
                        description: "Shear force (kN)"
                        default: 0
                      section_class:
                        type: integer
                        enum: [1, 2, 3, 4]
                        default: 1
                national_annex:
                  type: string
                  enum: [NL, DE, FR, IT, BE, recommended]
                  default: recommended
      responses:
        "200":
          description: "Member check results"
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: "#/components/schemas/CheckResponse"
        "429":
          $ref: "#/components/responses/RateLimited"

  /check/en-1992-1-1:
    post:
      tags: [Eurocode Checks]
      summary: "EN 1992-1-1 Concrete column"
      description: "Column bending + shear + minimum reinforcement per EN 1992-1-1:2004."
      operationId: checkEn1992_1_1
      security:
        - {}
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [columns]
              properties:
                columns:
                  type: array
                  items:
                    type: object
                    required: [mark, b, h, fck, fyk, N_Ed, M_Ed, A_s, l0]
                    properties:
                      mark:
                        type: string
                        example: "C1"
                      b:
                        type: number
                        description: "Width (mm)"
                      h:
                        type: number
                        description: "Depth (mm)"
                      fck:
                        type: number
                        description: "Characteristic cylinder strength (MPa)"
                      fyk:
                        type: number
                        description: "Reinforcement yield strength (MPa)"
                      N_Ed:
                        type: number
                        description: "Axial design force (kN)"
                      M_Ed:
                        type: number
                        description: "Design moment (kNm)"
                      A_s:
                        type: number
                        description: "Total reinforcement area (mm²)"
                      l0:
                        type: number
                        description: "Effective length (mm)"
      responses:
        "200":
          description: "Concrete column check"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckResponse"
        "429":
          $ref: "#/components/responses/RateLimited"

  /check/en-1995-1-1:
    post:
      tags: [Eurocode Checks]
      summary: "EN 1995-1-1 Timber member"
      description: "Timber bending + shear + combined per EN 1995-1-1:2004."
      operationId: checkEn1995_1_1
      security:
        - {}
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [members]
              properties:
                members:
                  type: array
                  items:
                    type: object
                    required: [mark, species, b, h, length_mm]
                    properties:
                      mark:
                        type: string
                        example: "T1"
                      species:
                        type: string
                        enum: [C14, C16, C18, C20, C22, C24, C27, C30, C35, C40, GL24h, GL28h, GL32h]
                      b:
                        type: number
                        description: "Width (mm)"
                      h:
                        type: number
                        description: "Depth (mm)"
                      length_mm:
                        type: number
                      M_y_Ed:
                        type: number
                      V_z_Ed:
                        type: number
                      N_Ed:
                        type: number
      responses:
        "200":
          description: "Timber check"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckResponse"
        "429":
          $ref: "#/components/responses/RateLimited"

  /check/en-1998-1:
    post:
      tags: [Eurocode Checks]
      summary: "EN 1998-1 Seismic"
      description: "Base shear, spectral acceleration, and q-factor per EN 1998-1:2004."
      operationId: checkEn1998_1
      security:
        - {}
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agR, ground_type, q, T1, W]
              properties:
                agR:
                  type: number
                  description: "Reference peak ground acceleration (m/s²)"
                  example: 0.15
                ground_type:
                  type: string
                  enum: [A, B, C, D, E]
                spectrum_type:
                  type: integer
                  enum: [1, 2]
                  default: 1
                q:
                  type: number
                  description: "Behaviour factor"
                  example: 2.0
                importance_class:
                  type: integer
                  enum: [1, 2, 3, 4]
                  default: 2
                T1:
                  type: number
                  description: "Fundamental period (s)"
                  example: 0.6
                W:
                  type: number
                  description: "Seismic weight (kN)"
                  example: 5000
      responses:
        "200":
          description: "Seismic check"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckResponse"
        "429":
          $ref: "#/components/responses/RateLimited"

  /usage:
    get:
      tags: [Eurocode Checks]
      summary: "API quota usage"
      description: "Current month's call count and remaining quota for this token."
      operationId: getUsage
      responses:
        "200":
          description: "Usage summary"
          content:
            application/json:
              schema:
                type: object
                properties:
                  calls_this_month:
                    type: integer
                  quota:
                    type: ["integer", "null"]
                    description: "Null means unlimited (Studio)"
                  remaining:
                    type: ["integer", "null"]
                  plan:
                    type: string
                    enum: [free, pro, studio]
