{
  "openapi": "3.0.3",
  "info": {
    "title": "FeneCAM Agent API",
    "version": "1.0.0",
    "description": "Authenticated 1-CSP / 2-CSP cutting optimization and saw-plan export for aluminium and uPVC window shops. This is an access channel for agents — FeneCAM remains a browser cutting optimizer / CAM, not an “AI Agent” product. Exports: PDF, XLS, CSV, DXF only. Named CNC dialects (PTX, Biesse, Elumatec, etc.) are undocumented and are not part of this API. Credits: 1 credit = $0.01. A new optimize_1csp or optimize_2csp solve uses 1 credit; an unchanged plan returns the saved result for 0 credits. Plan credits: Free 1,000 once per account (no monthly refill); Standard 1,000, Professional 2,000 and Enterprise API ($99/month) 12,000 per UTC month. Credit packs (1,000 for $10, 2,000 for $20, 10,000 for $100) add credits that do not expire; plan credits are spent first. All other calls use no credits. REST and MCP share one balance per account. POST /import/parse (MCP parse_clipboard) reads a table pasted from Excel or Google Sheets into a plan draft with Kimi; it uses no credits and is rate limited.",
    "contact": {
      "email": "support@fenecam.com"
    }
  },
  "servers": [
    {
      "url": "https://www.fenecam.com/api/v1",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Plans",
      "description": "Import, persist, optimize, read, export"
    },
    {
      "name": "Meta",
      "description": "OpenAPI, MCP, credits and quotas"
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "fnc_live",
        "description": "API key from the FeneCAM dashboard. Also accepted as X-API-Key."
      },
      "webSession": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Logged-in fenecam.com session (Supabase access token). Accepted only by POST /import/parse, which the website calls."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "UNAUTHORIZED",
              "FORBIDDEN_SCOPE",
              "VALIDATION_ERROR",
              "PLAN_NOT_FOUND",
              "PLAN_LIMIT_EXCEEDED",
              "QUOTA_EXCEEDED",
              "CREDITS_EXCEEDED",
              "RATE_LIMITED",
              "OPTIMIZE_BUSY",
              "OPTIMIZE_COOLDOWN",
              "NOT_OPTIMIZED",
              "EXPORT_FORMAT_UNSUPPORTED",
              "SOLVER_FAILED",
              "STOCK_SHORTAGE",
              "UNPLACED_PARTS",
              "WRONG_KIND",
              "FEATURE_DISABLED",
              "EMPTY_CLIPBOARD",
              "NOT_A_TABLE",
              "CLIPBOARD_TOO_LARGE",
              "PARSE_PROVIDER_FAILED",
              "EMAIL_NOT_VERIFIED"
            ]
          },
          "details": {}
        }
      },
      "CreditsExceededError": {
        "type": "object",
        "description": "HTTP 402. Plan credits and pack credits together cannot cover this optimize. Nothing ran and nothing was charged. Free does not refill: upgrade or buy a credit pack.",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "string",
            "example": "API credits exhausted"
          },
          "code": {
            "type": "string",
            "enum": [
              "CREDITS_EXCEEDED"
            ]
          },
          "details": {
            "type": "object",
            "properties": {
              "credits_remaining": {
                "type": "integer",
                "example": 0
              },
              "credits_allotment": {
                "type": "integer",
                "example": 1000
              },
              "credits_used_this_period": {
                "type": "integer",
                "example": 1000
              },
              "period_key": {
                "type": "string",
                "example": "free"
              },
              "credits_per_optimize": {
                "type": "integer",
                "example": 1
              },
              "credit_usd": {
                "type": "number",
                "example": 0.01
              },
              "upgrade_url": {
                "type": "string",
                "example": "https://www.fenecam.com/#pricing"
              },
              "hint": {
                "type": "string"
              },
              "credits_refill": {
                "type": "string",
                "enum": [
                  "none",
                  "monthly"
                ],
                "example": "none"
              },
              "pack_credits_remaining": {
                "type": "integer",
                "example": 0
              },
              "credit_packs_url": {
                "type": "string",
                "example": "https://www.fenecam.com/dashboard/account.html#billing"
              },
              "credit_packs": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "credits": {
                      "type": "integer"
                    },
                    "usd": {
                      "type": "number"
                    }
                  }
                },
                "example": [
                  {
                    "credits": 1000,
                    "usd": 10
                  },
                  {
                    "credits": 2000,
                    "usd": 20
                  },
                  {
                    "credits": 10000,
                    "usd": 100
                  }
                ]
              }
            }
          }
        }
      },
      "OptimizeRequest": {
        "type": "object",
        "properties": {
          "idempotency_key": {
            "type": "string",
            "maxLength": 255,
            "description": "Same as the Idempotency-Key header. The header wins when both are sent."
          }
        }
      },
      "OptimizeResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "plan_id": {
            "type": "string",
            "format": "uuid"
          },
          "summary": {
            "$ref": "#/components/schemas/ResultSummary"
          },
          "result": {
            "type": "object",
            "description": "Full solver result: patterns, plan, verification"
          },
          "credits_used": {
            "type": "integer",
            "description": "Credits this call used: 1 for a new solve, 0 for a cached unchanged plan or an Idempotency-Key replay",
            "example": 1
          },
          "credits_remaining": {
            "type": "integer",
            "example": 1999,
            "description": "Credits the account can still spend: plan credits left plus pack credits"
          },
          "credits_allotment": {
            "type": "integer",
            "example": 2000,
            "description": "Plan credits. Free: 1,000 once per account, no refill. Standard 1,000, Professional 2,000, Enterprise API 12,000 per UTC month."
          },
          "period_key": {
            "type": "string",
            "description": "free for the one-time Free grant; on paid plans the UTC month (YYYY-MM) the plan credits belong to",
            "example": "2026-10"
          },
          "idempotent_replay": {
            "type": "boolean",
            "description": "Present and true when this is the saved outcome of an earlier call with the same Idempotency-Key"
          },
          "credits_refill": {
            "type": "string",
            "enum": [
              "none",
              "monthly"
            ],
            "description": "none on Free (one-time grant), monthly on paid plans"
          },
          "pack_credits_remaining": {
            "type": "integer",
            "description": "Credits from credit packs. They do not expire and are spent after plan credits. Included in credits_remaining.",
            "example": 0
          }
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "user_id": {
            "type": "string"
          },
          "key_prefix": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "standard",
              "professional",
              "enterprise"
            ]
          },
          "period": {
            "type": "string",
            "example": "2026-10"
          },
          "limits": {
            "type": "object",
            "properties": {
              "requestsPerMinute": {
                "type": "integer"
              },
              "optimizePerMinute": {
                "type": "integer"
              },
              "parsePerMinute": {
                "type": "integer",
                "description": "Clipboard parses per minute (POST /import/parse, parse_clipboard), web and API together"
              },
              "exportPerMonth": {
                "type": "integer"
              },
              "maxKeys": {
                "type": "integer"
              }
            }
          },
          "used": {
            "type": "object",
            "properties": {
              "request_count": {
                "type": "integer"
              },
              "optimize_count": {
                "type": "integer"
              },
              "export_count": {
                "type": "integer"
              }
            }
          },
          "credits_used": {
            "type": "integer",
            "description": "Plan credits used so far in period_key (the Free grant or this UTC month). Pack credits are not counted here.",
            "example": 1
          },
          "credits_remaining": {
            "type": "integer",
            "example": 1999,
            "description": "Credits the account can still spend: plan credits left plus pack credits"
          },
          "credits_allotment": {
            "type": "integer",
            "example": 2000,
            "description": "Plan credits. Free: 1,000 once per account, no refill. Standard 1,000, Professional 2,000, Enterprise API 12,000 per UTC month."
          },
          "period_key": {
            "type": "string",
            "example": "2026-10",
            "description": "free for the one-time Free grant; on paid plans the UTC month (YYYY-MM) the plan credits belong to"
          },
          "credits_per_optimize": {
            "type": "integer",
            "example": 1
          },
          "credit_usd": {
            "type": "number",
            "example": 0.01
          },
          "credits_refill": {
            "type": "string",
            "enum": [
              "none",
              "monthly"
            ],
            "description": "none on Free (one-time grant), monthly on paid plans"
          },
          "pack_credits_remaining": {
            "type": "integer",
            "description": "Credits from credit packs. They do not expire and are spent after plan credits. Included in credits_remaining.",
            "example": 0
          }
        }
      },
      "ImportPartsRequest": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "1csp",
              "2csp"
            ]
          },
          "file_format": {
            "type": "string",
            "enum": [
              "json",
              "csv",
              "sob"
            ],
            "description": "Data file format. Further formats can be added here. format is accepted as an alias."
          },
          "format": {
            "type": "string",
            "enum": [
              "json",
              "csv",
              "sob"
            ],
            "description": "Alias of file_format."
          },
          "csv": {
            "type": "string",
            "description": "Import-manual key-column CSV, or SOB text when file_format is sob."
          },
          "sob": {
            "type": "string",
            "description": "SOB file text when file_format is sob. KTN part numbers are kept as each part id."
          },
          "filename": {
            "type": "string"
          },
          "persist": {
            "type": "boolean",
            "description": "If true, also create a plan"
          },
          "name": {
            "type": "string"
          },
          "sizes": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "parts": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "stocks": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "kerf": {
            "type": "number"
          },
          "leftTrim": {
            "type": "number"
          },
          "rightTrim": {
            "type": "number"
          }
        }
      },
      "ClipboardParseRequest": {
        "type": "object",
        "required": [
          "kind",
          "clipboard_text"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "1csp",
              "2csp",
              "1d",
              "2d"
            ],
            "description": "1csp = bars cut to length, 2csp = panels on sheets. 1d/2d are aliases; the response always says 1csp or 2csp."
          },
          "clipboard_text": {
            "type": "string",
            "maxLength": 65536,
            "description": "Cells copied from Excel or Google Sheets: rows separated by newlines, cells by tabs (CSV text with , or ; also works). At most 64 KB and 1,000 rows."
          },
          "locale_hint": {
            "type": "string",
            "example": "zh",
            "description": "Language of the headers. Optional."
          },
          "unit_hint": {
            "type": "string",
            "enum": [
              "mm",
              "cm",
              "m",
              "in",
              "ft"
            ],
            "description": "Unit to assume when the paste states none. Default mm. A unit the paste states (e.g. a \"Length (in)\" header) wins; lengths are always returned in mm."
          },
          "name": {
            "type": "string",
            "description": "Plan name. Defaults to a name in the paste, else \"Clipboard import\"."
          },
          "persist": {
            "type": "boolean",
            "default": false,
            "description": "If true, also create the plan (plan size limits apply) and return plan_id."
          },
          "projectId": {
            "type": "string",
            "description": "Project for the plan when persist is true."
          }
        },
        "example": {
          "kind": "1csp",
          "clipboard_text": "Length\tQty\tLabel\n1200\t7\tPart A\n1500\t4\tPart B\nStock\t6000\t10",
          "locale_hint": "en",
          "unit_hint": "mm"
        }
      },
      "ClipboardParseResponse": {
        "type": "object",
        "description": "A plan draft with the fields POST /plans takes (send it as is, or its plan field), plus how the paste was read. Show columns_mapped, assumptions and warnings to the user before saving. Lengths in mm.",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "kind": {
            "type": "string",
            "enum": [
              "1csp",
              "2csp"
            ]
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "Material / notes (import-manual M)"
          },
          "unit": {
            "type": "string",
            "enum": [
              "mm"
            ]
          },
          "kerf": {
            "type": "number",
            "example": 3
          },
          "leftTrim": {
            "type": "number",
            "description": "1csp only"
          },
          "rightTrim": {
            "type": "number",
            "description": "1csp only"
          },
          "trim": {
            "type": "number",
            "description": "1csp: same as leftTrim. 2csp: sheet edge trim."
          },
          "stockLength": {
            "type": "number",
            "description": "1csp only: first stock length"
          },
          "stocks": {
            "type": "array",
            "description": "1csp: { length, quantity } with quantity null = unlimited. 2csp: { width, height, quantity, grain? } with quantity -1 = unlimited.",
            "items": {
              "type": "object"
            }
          },
          "sizes": {
            "type": "array",
            "description": "1csp only: { size, quantity, label?, group?, leftAngle, rightAngle }. Angles default to 90.",
            "items": {
              "type": "object"
            }
          },
          "parts": {
            "type": "array",
            "description": "2csp only: { width, height, quantity, label?, group?, grain? (H or V), can_rotate (0 or 1) }.",
            "items": {
              "type": "object"
            }
          },
          "settings": {
            "type": "object"
          },
          "columns_mapped": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "nullable": true
            },
            "description": "Plan field → the header (or column / stock row) it was read from; null when not in the paste."
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "defaults_applied": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "kerf",
                "stocks",
                "trim"
              ]
            },
            "description": "Values not in the paste that were filled with defaults: kerf 3 mm; 1csp one 6000 mm unlimited bar and no trim; 2csp one 2440 × 1220 mm unlimited sheet and 10 mm trim."
          },
          "table": {
            "type": "object",
            "properties": {
              "rows": {
                "type": "integer"
              },
              "columns": {
                "type": "integer"
              },
              "delimiter": {
                "type": "string",
                "enum": [
                  "tab",
                  "semicolon",
                  "comma",
                  "none"
                ]
              },
              "part_rows": {
                "type": "integer"
              },
              "stock_rows": {
                "type": "integer"
              },
              "skipped_rows": {
                "type": "integer"
              }
            }
          },
          "plan": {
            "type": "object",
            "description": "The same plan draft as one object, ready for POST /plans"
          },
          "plan_id": {
            "type": "string",
            "format": "uuid",
            "description": "Present when persist was true"
          },
          "persisted": {
            "type": "boolean"
          },
          "credits_used": {
            "type": "integer",
            "enum": [
              0
            ],
            "description": "Parsing never uses credits"
          },
          "provider": {
            "type": "string",
            "example": "kimi"
          },
          "model": {
            "type": "string",
            "example": "kimi-k3"
          }
        }
      },
      "ResultSummary": {
        "type": "object",
        "properties": {
          "plan_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "1csp",
              "2csp"
            ]
          },
          "status": {
            "type": "string"
          },
          "algo": {
            "type": "string"
          },
          "bars_used": {
            "type": "integer"
          },
          "sheets_used": {
            "type": "integer"
          },
          "yield_percent": {
            "type": "number"
          },
          "waste_percent": {
            "type": "number"
          },
          "waste_mm": {
            "type": "number"
          },
          "remnants": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "patterns": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "issue_codes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "unplaced_items": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "valid_result": {
            "type": "boolean"
          }
        }
      }
    }
  },
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/import/parse": {
      "post": {
        "tags": [
          "Plans"
        ],
        "operationId": "parse_clipboard",
        "summary": "Read a table pasted from Excel or Google Sheets into a plan draft. Uses no credits.",
        "description": "Kimi (Moonshot AI) recognizes the columns and rows; FeneCAM reads the numbers from the paste and returns a plan draft that passes the same validation as POST /plans. Nothing is saved (unless persist) and nothing is optimized. Uses 0 credits. Rate limit per account: Free 10, paid plans 30 parses per minute (429 RATE_LIMITED with Retry-After). Max 64 KB and 1,000 rows. Auth: API key with plans:write, or the logged-in website session. No anonymous calls.",
        "security": [
          {
            "apiKey": []
          },
          {
            "webSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClipboardParseRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Plan draft plus columns_mapped, assumptions and warnings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClipboardParseResponse"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR (bad kind or unit_hint, or the recognized table is not a valid plan: details.issues uses the import_parts codes such as NO_PARTS, PART_LONGER_THAN_STOCK, INVALID_MITER, and details.result holds the best-effort draft), EMPTY_CLIPBOARD, or NOT_A_TABLE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No API key or web session (UNAUTHORIZED)"
          },
          "403": {
            "description": "FORBIDDEN_SCOPE (key lacks plans:write) or EMAIL_NOT_VERIFIED"
          },
          "413": {
            "description": "CLIPBOARD_TOO_LARGE: over 64 KB or 1,000 rows",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED. Wait for Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "PARSE_PROVIDER_FAILED: Kimi timed out, failed or returned unusable output. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "FEATURE_DISABLED: clipboard parsing is not configured on this server"
          }
        }
      }
    },
    "/import_parts": {
      "post": {
        "tags": [
          "Plans"
        ],
        "operationId": "import_parts",
        "summary": "Parse JSON or CSV BOM (import-manual semantics)",
        "security": [
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportPartsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Parsed plan draft"
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/plans": {
      "post": {
        "tags": [
          "Plans"
        ],
        "operationId": "create_or_update_plan",
        "summary": "Create a plan the optimizer can run",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportPartsRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "400": {
            "description": "Validation error"
          }
        }
      }
    },
    "/plans/{id}": {
      "put": {
        "tags": [
          "Plans"
        ],
        "operationId": "update_plan",
        "summary": "Update an existing plan",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportPartsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "get": {
        "tags": [
          "Plans"
        ],
        "operationId": "get_plan",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Plan metadata"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/plans/{id}/optimize_1csp": {
      "post": {
        "tags": [
          "Plans"
        ],
        "operationId": "optimize_1csp",
        "summary": "Run linear profile (1-CSP) optimization",
        "description": "Credits: a new solve uses 1 credit ($0.01), also when the solver then fails or rejects the plan (SOLVER_FAILED, STOCK_SHORTAGE, UNPLACED_PARTS). An unchanged plan returns the saved result for 0 credits. Calls refused before the solver starts (for example OPTIMIZE_BUSY, RATE_LIMITED, OPTIMIZE_COOLDOWN, WRONG_KIND, PLAN_LIMIT_EXCEEDED, CREDITS_EXCEEDED) use 0 credits. On an optimize error, details.credits_used is what the call used; if it is missing, the call used 0.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Retry the same optimize with the same key this UTC month and you get the first outcome back with credits_used 0. Without a key, a retried new solve uses another credit."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OptimizeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Yield, waste, remnants, patterns, and credits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OptimizeResponse"
                }
              }
            }
          },
          "400": {
            "description": "Solver rejected the plan, or a machine-aware solve left parts unplaced",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Credits exhausted (CREDITS_EXCEEDED): plan credits and pack credits together cannot cover this optimize",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditsExceededError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED, OPTIMIZE_BUSY or OPTIMIZE_COOLDOWN. Wait for Retry-After. No credit used.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Finite stock cannot cover the demand (STOCK_SHORTAGE or UNPLACED_PARTS)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Solver failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/plans/{id}/optimize_2csp": {
      "post": {
        "tags": [
          "Plans"
        ],
        "operationId": "optimize_2csp",
        "summary": "Run sheet/panel (2-CSP) nesting",
        "description": "Credits: a new solve uses 1 credit ($0.01), also when the solver then fails or rejects the plan (SOLVER_FAILED, STOCK_SHORTAGE, UNPLACED_PARTS). An unchanged plan returns the saved result for 0 credits. Calls refused before the solver starts (for example OPTIMIZE_BUSY, RATE_LIMITED, OPTIMIZE_COOLDOWN, WRONG_KIND, PLAN_LIMIT_EXCEEDED, CREDITS_EXCEEDED) use 0 credits. On an optimize error, details.credits_used is what the call used; if it is missing, the call used 0.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Optional. Retry the same optimize with the same key this UTC month and you get the first outcome back with credits_used 0. Without a key, a retried new solve uses another credit."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OptimizeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sheets used, yield, waste, remnants, patterns, and credits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OptimizeResponse"
                }
              }
            }
          },
          "400": {
            "description": "Sheet solver rejected the plan"
          },
          "402": {
            "description": "Credits exhausted (CREDITS_EXCEEDED): plan credits and pack credits together cannot cover this optimize",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditsExceededError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED, OPTIMIZE_BUSY or OPTIMIZE_COOLDOWN. Wait for Retry-After. No credit used."
          },
          "422": {
            "description": "Finite sheet stock cannot cover the demand"
          },
          "502": {
            "description": "Solver failed"
          }
        }
      }
    },
    "/plans/{id}/result": {
      "get": {
        "tags": [
          "Plans"
        ],
        "operationId": "get_result",
        "summary": "Idempotent read of yield / remnants / waste / patterns. Uses no credits.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Result summary"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/plans/{id}/export": {
      "post": {
        "tags": [
          "Plans"
        ],
        "operationId": "export_plan",
        "summary": "Export PDF, XLS, CSV or DXF. Uses no credits; counts toward the monthly export cap (QUOTA_EXCEEDED when reached).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "raw",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "If true, return file bytes instead of JSON+base64"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "format"
                ],
                "properties": {
                  "format": {
                    "type": "string",
                    "enum": [
                      "pdf",
                      "xls",
                      "csv",
                      "dxf"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "File (raw) or JSON with base64"
          },
          "400": {
            "description": "Unsupported format"
          },
          "409": {
            "description": "Not optimized yet"
          },
          "429": {
            "description": "Monthly export cap reached (QUOTA_EXCEEDED) or rate limited"
          }
        }
      }
    },
    "/me": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "get_quota",
        "summary": "Current plan, scopes, credit balance, and monthly usage. Uses no credits.",
        "responses": {
          "200": {
            "description": "Credit balance and quota snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "get_openapi",
        "security": [],
        "summary": "OpenAPI 3 document",
        "responses": {
          "200": {
            "description": "OpenAPI JSON"
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": [
          "Meta"
        ],
        "operationId": "mcp_jsonrpc",
        "summary": "MCP JSON-RPC (initialize, tools/list, tools/call). optimize_1csp / optimize_2csp use the same credits as REST from the same balance; errors carry the code in error.data.code.",
        "responses": {
          "200": {
            "description": "JSON-RPC response"
          }
        }
      }
    },
    "/mcp/schema": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "mcp_schema",
        "security": [],
        "summary": "MCP tool names, descriptions, JSON Schema args, error codes",
        "responses": {
          "200": {
            "description": "Tool schema"
          }
        }
      }
    }
  }
}
