{"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_refill":{"type":"string","enum":["none","monthly"],"example":"none"},"credits_used_this_period":{"type":"integer","example":1000},"pack_credits_remaining":{"type":"integer","example":0},"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"},"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}]},"hint":{"type":"string"}}}}},"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","description":"Credits the account can still spend: plan credits left plus pack credits","example":1999},"credits_allotment":{"type":"integer","description":"Plan credits. Free: 1,000 once per account, no refill. Standard 1,000, Professional 2,000, Enterprise API 12,000 per UTC month.","example":2000},"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},"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"}}},"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","description":"Credits the account can still spend: plan credits left plus pack credits","example":1999},"credits_allotment":{"type":"integer","description":"Plan credits. Free: 1,000 once per account, no refill. Standard 1,000, Professional 2,000, Enterprise API 12,000 per UTC month.","example":2000},"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},"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"},"credits_per_optimize":{"type":"integer","example":1},"credit_usd":{"type":"number","example":0.01}}},"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"}}}},"422":{"description":"Finite stock cannot cover the demand (STOCK_SHORTAGE or UNPLACED_PARTS)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED, OPTIMIZE_BUSY or OPTIMIZE_COOLDOWN. Wait for Retry-After. No credit used.","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"}}}},"422":{"description":"Finite sheet stock cannot cover the demand"},"429":{"description":"RATE_LIMITED, OPTIMIZE_BUSY or OPTIMIZE_COOLDOWN. Wait for Retry-After. No credit used."},"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"}}}}}}