REST API

For programs, scanners and machines

REST API

Every function of CutStock can be controlled via REST API – pieces by their code, materials by their short code.

Every function of the user interface is also available via the REST API under /api/ -- the interface itself uses nothing else. All endpoints accept and return JSON. CutStock serves an automatically generated, interactive description of all endpoints at /docs.

For programs, scanners and machines:

  • Materials can be addressed by ID or short code (AP40), pieces by ID or code (AP40-07, from the label), locations by ID or name.
  • Errors of the stock commands carry a fixed code: {"detail": {"code": "stueck_reserviert", "message": "...", "projekt": "..."}}.
  • Every stock booking is written to the stock journal, with source api or ui.

Authentication

When an API key is configured (Settings page), external requests must include it as a Bearer token:

curl -H "Authorization: Bearer YOUR_API_KEY" http://cutstock:8420/api/materials

Requests from the web UI (same-origin) and from localhost are always allowed without a key.

Endpoints

Method Path Description
Materials
GET /api/materials List all materials
POST /api/materials Create a material
PUT /api/materials/{id} Update a material
DELETE /api/materials/{id} Delete a material
Stock
GET /api/stock?material_id=N List stock (optional filter)
POST /api/stock Create stock entry
PUT /api/stock/{id} Update stock entry
DELETE /api/stock/{id} Delete stock entry
GET /api/stock/{id}/pieces List individual pieces (code, location, reserved for)
GET /api/pieces/{id or code} Look up a piece: material, size, location, reservation
PUT /api/pieces/{id or code} Change a piece's note
GET/POST/DELETE /api/materials/{id}/image Read, upload, remove material image
Stock locations (see below)
GET /api/locations Locations with number of materials and pieces
POST /api/locations Create a location {"name": "Rack 3"}
PUT /api/locations/{id or name} Rename a location
DELETE /api/locations/{id or name}?ziel=… Delete a location; if in use, ziel (target) is required
GET /api/locations/{id or name}/contents What is stored there: materials with pieces
PUT /api/materials/{id or short code}/location Assign a material to a location
POST /api/stock/receive Receive stock: add pieces, returns the new codes
POST /api/pieces/{id or code}/take Take a piece out (with reason)
GET /api/waste Contents of the offcut bin
POST /api/waste Put offcuts into the bin
POST /api/waste/take Take offcuts out of the bin
DELETE /api/waste?material=… Empty the bin (all or per material)
GET /api/journal?material=…&seit=… Stock journal, newest first
Defects
GET /api/stock/{id}/defects Defects on a stock entry
GET /api/pieces/{id}/defects Defects on a piece
POST /api/pieces/{id}/defects Add a defect
PUT /api/defects/{id} Update a defect
DELETE /api/defects/{id} Delete a defect
Saw Blades
GET /api/blades List saw blades
POST /api/blades Create a blade
PUT /api/blades/{id} Update a blade
DELETE /api/blades/{id} Delete a blade
Projects
GET /api/projects?art= List projects; art=sammel combined plans only, art=alle both (without: projects only)
GET /api/projects/{id} Get a project
POST /api/projects Create a project
PUT /api/projects/{id} Update a project (name, number of copies)
DELETE /api/projects/{id} Delete a project or combined plan (cut parts stay in the projects)
POST /api/projects/{id}/clone Duplicate a project
POST /api/projects/{id}/copy-parts Copy parts into a project
POST /api/projects/{id}/restart Restart project (progress 0, plans removed, stock unchanged)
Parts
GET /api/projects/{id}/parts List parts of a project
POST /api/projects/{id}/parts Add a part
PUT /api/parts/{id} Update a part (optional vorrat: set shelf stock)
DELETE /api/parts/{id} Delete a part
Optimization and Plans
POST /api/optimize Optimize and save as the project's plan for the material
POST /api/plans/{id}/fill-choice Choose the filling of a group of equal bar offcuts (wahl: null = automatic)
GET /api/projects/{id}/plans/{material_id} Get the saved plan (null if there is none)
POST /api/plan-pieces/{id} Mark a piece as cut / undo ({"gesaegt": true}), updates progress and stock
POST /api/plans/{id}/confirm-all Mark all open pieces of the plan as cut
DELETE /api/plans/{id} Discard plan (releases reserved pieces)
GET /api/projects/{id}/fill-candidates/{material_id} Possible filler parts from other projects
GET /api/sammel/{material_id} Combined plan status and eligible projects
POST /api/sammel Create a combined plan (material and projects, no cutting plan yet)
PUT /api/sammel/{id or code}/members Replace the members of a material's combined plan
POST /api/sammel/optimize Create/replace and compute the combined plan
GET /api/parts Parts of all projects ("Parts" tab)
POST /api/cut-lists Create a cut list from individual parts
POST /api/sammel/compare Compare separate vs. combined (not saved)
POST /api/projects/{id}/take-stock/{material_id} Take part stock ("shelf") over as cut
Export
POST /api/pdf Generate PDF cut plan
POST /api/labels Generate label PDF from ready-made texts
POST /api/labels/pieces Labels for stock pieces (select by code, location, material, filters)
System
GET /api/version App version and links
GET /api/license License status
GET /api/settings Read settings
PUT /api/settings Update settings
GET /api/backup Download database backup (ZIP)
POST /api/restore Restore from backup (ZIP)
GET /api/i18n/{lang} Translations for a language
POST /api/seed Create sample data (empty database only)
GET /api/is-desktop Is CutStock running as the desktop app?
POST /api/shutdown Quit the desktop app (desktop app only)

Optimization request

POST /api/optimize
{
  "project_id": 1,
  "material_id": 3,
  "blade_id": 1,
  "algorithm": "nested",
  "reste_zuerst": false,
  "fuellteile": [{"teil_id": 42, "modus": "auffuellen", "menge": 0}],
  "fuell_alle_reste": false
}

Algorithm choices: greedy, nested, ga. reste_zuerst (remnants first), fuellteile (filler parts) and fuell_alle_reste are optional (filler mode auffuellen = fill or fest = fixed quantity; without fuellteile the saved plan's selection is kept).

The response is the saved plan: plan_id, schnittplaene (per board e.g. stueck_code, lagerort, platzierungen with id and gesaegt), fehlende_teile (parts that didn't fit), gesamt_verschnitt_prozent (total waste %), materialbedarf (additional material needed -- empty when everything fits) and the hints veraltet (outdated) and lager_fehlt (stock missing).

Stock commands

Receive stock -- returns the codes for labelling:

POST /api/stock/receive
{"material": "AP40", "laenge": 6000, "anzahl": 4}

→ {"stock_id": 12, "codes": ["AP40-21", "AP40-22", "AP40-23", "AP40-24"], "lagerort": "Rack Alu"}

breite (width) only for panels, ist_rest: true for offcuts (stored at the offcut location).

Take a piece out:

POST /api/pieces/AP40-07/take
{"grund": "verbraucht", "notiz": "prototype"}

grund (reason): verbraucht (used), verkauft (sold), defekt (defective) or abfall (into the offcut bin, if switched on). If a saved plan reserves the piece, the answer is 409 with code: "stueck_reserviert" and the project; with "trotzdem": true it is taken out anyway. The plan then reports lager_fehlt and fehlende_stuecke.

Labels for stock pieces -- e.g. all pieces on a rack that have no label yet:

POST /api/labels/pieces
{"lagerort": "Wandregal Alu", "nur_ungedruckt": true, "startfeld": 5}

Select with codes (list of codes or IDs) or lagerort (location) and/or material; narrow down with nur_reste (offcuts only), nur_ganze (whole pieces only), nur_ungedruckt (not printed yet). startfeld skips fields already used on an A4 sheet. The answer is the PDF (header X-Label-Count = number of labels); the pieces then count as printed (etikett_gedruckt on the piece = Unix time, 0 = never), unless "markieren": false. No selection gives 400 auswahl_fehlt, nothing matching 404 keine_stuecke.

Assign a material to a location:

PUT /api/materials/AP40/location
{"lagerort": "Rack Alu", "lagerort_reste": "Offcut box Alu"}

lagerort_reste: "" = offcuts stay with the material. When creating/updating a material via /api/materials, an unknown location name is created automatically.

Delete a location that is still in use:

DELETE /api/locations/Rack%203              → 409 {"code": "lagerort_in_benutzung", "materialien": [...]}
DELETE /api/locations/Rack%203?ziel=Stock   → 204, the materials are now at "Stock"

Offcut bin (no codes, only material + size + quantity):

POST /api/waste        {"material": "AP40", "laenge": 180, "anzahl": 2}
POST /api/waste/take   {"material": "AP40", "laenge": 180}
DELETE /api/waste?material=AP40

Stock journal: GET /api/journal?material=AP40&seit=1767225600&limit=100 -- each entry has zeit (Unix time), art (zugang, korrektur_ab, entnahme, plan_verbrauch, plan_zurueck, rest_ein, rest_zurueck, abfall_ein, abfall_zurueck, abfall_ab, abfall_leer, material_lagerort, lagerort_neu, lagerort_umbenannt, lagerort_geloescht), code, sizes, anzahl, quelle (ui/api), notiz.

Combined plans

A combined plan is a project with art: "sammel", sammel_material_id and mitglieder (member project IDs); it has no parts of its own. GET /api/projects only lists it with ?art=sammel or ?art=alle; member projects name it under sammel ({material_id: name}).

POST /api/sammel
{"material": "AP40", "projekte": ["Workbench", "12"]}

→ 201 {"id": 31, "name": "Sammelplan Alu Profil 40x40", "art": "sammel", "sammel_material_id": 2, "mitglieder": [5, 12], ...}

Material by ID or code, projects by ID or name, name optional. If the material already has one, you get 409 sammel_vorhanden with sammel_id; unknown references give 404 material_unbekannt or projekt_unbekannt. The plan is calculated with POST /api/sammel/optimize.

Change members (a calculated plan is then veraltet, i.e. outdated):

PUT /api/sammel/AP40/members
{"projekte": ["Workbench"]}

Rename with PUT /api/projects/{id} (the number of copies stays 1), delete with DELETE /api/projects/{id}: what has been cut already counts in the member projects (gesaegt_anzahl), the rest becomes open again and reserved stock pieces are released. Adding, copying or duplicating parts is not possible for a combined plan (400 sammel_ohne_teile).

Responses (examples)

One example for every endpoint: what request and response look like – generated with the real CutStock and a sample database (sample data in German). Long lists are shortened ("… n weitere" = n more). Numbers are always millimetres, zeit is Unix time. Errors come as {"detail": …}. Click an example to expand it.

System

GET /api/version Version 200
Response
{
  "version": "2026.10.74",
  "repo": "orohde/CutStock",
  "github_url": "https://github.com/orohde/CutStock",
  "website_url": "https://cutstock.app/",
  "releases_api": "https://api.github.com/repos/orohde/CutStock/releases/latest",
  "docker": false
}
GET /api/is-desktop Running as desktop app? 200
Response
{
  "desktop": false
}
GET /api/license License 200
Response
{
  "valid": false,
  "type": "community",
  "holder": "",
  "issued": null,
  "expires": null,
  "error": "no_key"
}
GET /api/i18n/de Translations of a language (excerpt) 200
Response
{
  "app.title": "CutStock – Verschnittoptimierung",
  "app.quit": "Beenden",
  "app.quit_confirm": "CutStock wirklich beenden?",
  "app.quit_done": "CutStock wurde beendet.",
  "…": "491 weitere"
}

Settings

GET /api/settings Read settings 200
Response
{
  "language": "de",
  "unit": "mm",
  "theme": "system",
  "hotkeys": {},
  "label_format": "a4_3x8",
  "label_custom_w": 89,
  "label_custom_h": 36,
  "api_enabled": false,
  "api_port": 8421,
  "api_key": "",
  "license_key": "",
  "nr_lock_days": 1,
  "reservation": true,
  "waste_keep": false,
  "waste_min": 50
}
PUT /api/settings Change settings (only the given fields) 200
Request
{
  "waste_keep": true,
  "waste_min": 50
}
Response
{
  "language": "de",
  "unit": "mm",
  "theme": "system",
  "hotkeys": {},
  "label_format": "a4_3x8",
  "label_custom_w": 89,
  "label_custom_h": 36,
  "api_enabled": false,
  "api_port": 8421,
  "api_key": "",
  "license_key": "",
  "nr_lock_days": 1,
  "reservation": true,
  "waste_keep": true,
  "waste_min": 50.0
}

Materials

POST /api/materials Create material 201
Request
{
  "name": "Alu Profil 40x40 Nut 8",
  "typ": "Stange",
  "querschnitt_breite": 40,
  "querschnitt_tiefe": 40,
  "rest_min_laenge": 300,
  "kuerzel": "AP40",
  "lagerort": "Wandregal Alu",
  "lagerort_reste": "Restekiste Alu"
}
Response
{
  "id": 1,
  "name": "Alu Profil 40x40 Nut 8",
  "typ": "Stange",
  "dicke": 0.0,
  "querschnitt_breite": 40.0,
  "querschnitt_tiefe": 40.0,
  "maserung": "keine",
  "drehung_erlaubt": true,
  "besaeumung": 0.0,
  "rest_min_laenge": 300.0,
  "rest_min_breite": 0.0,
  "hat_bild": false,
  "kuerzel": "AP40",
  "lagerort": "Wandregal Alu",
  "lagerort_reste": "Restekiste Alu"
}
GET /api/materials All materials 200
Response
[
  {
    "id": 1,
    "name": "Alu Profil 40x40 Nut 8",
    "typ": "Stange",
    "dicke": 0.0,
    "querschnitt_breite": 40.0,
    "querschnitt_tiefe": 40.0,
    "maserung": "keine",
    "drehung_erlaubt": true,
    "besaeumung": 0.0,
    "rest_min_laenge": 300.0,
    "rest_min_breite": 0.0,
    "hat_bild": false,
    "kuerzel": "AP40",
    "lagerort": "Wandregal Alu",
    "lagerort_reste": "Restekiste Alu"
  },
  {
    "id": 2,
    "name": "MDF 19 mm",
    "typ": "Platte",
    "dicke": 19.0,
    "querschnitt_breite": 0.0,
    "querschnitt_tiefe": 0.0,
    "maserung": "keine",
    "drehung_erlaubt": true,
    "besaeumung": 0.0,
    "rest_min_laenge": 200.0,
    "rest_min_breite": 200.0,
    "hat_bild": false,
    "kuerzel": "MDF19",
    "lagerort": "Plattenwagen",
    "lagerort_reste": ""
  }
]
PUT /api/materials/1 Change material (omit location = unchanged) 200
Request
{
  "name": "Alu Profil 40x40 Nut 8",
  "typ": "Stange",
  "querschnitt_breite": 40,
  "querschnitt_tiefe": 40,
  "rest_min_laenge": 250,
  "kuerzel": "AP40"
}
Response
{
  "id": 1,
  "name": "Alu Profil 40x40 Nut 8",
  "typ": "Stange",
  "dicke": 0.0,
  "querschnitt_breite": 40.0,
  "querschnitt_tiefe": 40.0,
  "maserung": "keine",
  "drehung_erlaubt": true,
  "besaeumung": 0.0,
  "rest_min_laenge": 250.0,
  "rest_min_breite": 0.0,
  "hat_bild": false,
  "kuerzel": "AP40",
  "lagerort": "Wandregal Alu",
  "lagerort_reste": "Restekiste Alu"
}
POST /api/materials/1/image Upload image 204
Request
multipart/form-data, field "file" = profil.png (image/png)
Response
(no content, status 204)
GET /api/materials/1/image Get image 200
Response
File: image/png, 69 Bytes
DELETE /api/materials/1/image Delete image 204
Response
(no content, status 204)
DELETE /api/materials/3 Delete material 204
Response
(no content, status 204)

Stock

POST /api/stock/receive Receive: book pieces into stock 201
Request
{
  "material": "AP40",
  "laenge": 6000,
  "anzahl": 4
}
Response
{
  "stock_id": 1,
  "codes": [
    "AP40-01",
    "AP40-02",
    "… 2 weitere"
  ],
  "lagerort": "Wandregal Alu"
}
POST /api/stock/receive Receive an offcut 201
Request
{
  "material": "AP40",
  "laenge": 740,
  "anzahl": 1,
  "ist_rest": true
}
Response
{
  "stock_id": 2,
  "codes": [
    "AP40-05"
  ],
  "lagerort": "Restekiste Alu"
}
POST /api/stock Create stock row (as in the material dialog) 201
Request
{
  "material_id": 2,
  "laenge": 2800,
  "breite": 2070,
  "stueckzahl": 2
}
Response
{
  "id": 3,
  "material_id": 2,
  "laenge": 2800.0,
  "breite": 2070.0,
  "stueckzahl": 2,
  "ist_rest": false
}
GET /api/stock?material_id=1 Stock rows of a material 200
Response
[
  {
    "id": 1,
    "material_id": 1,
    "laenge": 6000.0,
    "breite": 0.0,
    "stueckzahl": 4,
    "ist_rest": false
  },
  {
    "id": 2,
    "material_id": 1,
    "laenge": 740.0,
    "breite": 0.0,
    "stueckzahl": 1,
    "ist_rest": true
  }
]
PUT /api/stock/3 Change stock row (quantity) 200
Request
{
  "material_id": 2,
  "laenge": 2800,
  "breite": 2070,
  "stueckzahl": 3
}
Response
{
  "id": 3,
  "material_id": 2,
  "laenge": 2800.0,
  "breite": 2070.0,
  "stueckzahl": 3,
  "ist_rest": false
}
GET /api/stock/1/pieces Pieces of a stock row 200
Response
[
  {
    "id": 1,
    "lagerstueck_id": 1,
    "nr": 1,
    "notiz": "",
    "defekt_count": 0,
    "code": "AP40-01",
    "lagerort": "Wandregal Alu",
    "lagerort_effektiv": "Wandregal Alu",
    "reserviert_fuer": "",
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "laenge": 6000.0,
    "breite": 0.0,
    "ist_rest": false,
    "etikett_gedruckt": 0.0
  },
  {
    "id": 2,
    "lagerstueck_id": 1,
    "nr": 2,
    "notiz": "",
    "defekt_count": 0,
    "code": "AP40-02",
    "lagerort": "Wandregal Alu",
    "lagerort_effektiv": "Wandregal Alu",
    "reserviert_fuer": "",
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "laenge": 6000.0,
    "breite": 0.0,
    "ist_rest": false,
    "etikett_gedruckt": 0.0
  },
  "… 2 weitere"
]
GET /api/pieces/AP40-02 Piece by its code (or ID) 200
Response
{
  "id": 2,
  "lagerstueck_id": 1,
  "nr": 2,
  "notiz": "",
  "defekt_count": 0,
  "code": "AP40-02",
  "lagerort": "Wandregal Alu",
  "lagerort_effektiv": "Wandregal Alu",
  "reserviert_fuer": "",
  "material_id": 1,
  "material": "Alu Profil 40x40 Nut 8",
  "laenge": 6000.0,
  "breite": 0.0,
  "ist_rest": false,
  "etikett_gedruckt": 0.0
}
PUT /api/pieces/AP40-02 Note on a piece 200
Request
{
  "notiz": "leicht verkratzt"
}
Response
{
  "id": 2,
  "lagerstueck_id": 1,
  "nr": 2,
  "notiz": "leicht verkratzt",
  "defekt_count": 0,
  "code": "AP40-02",
  "lagerort": "Wandregal Alu",
  "lagerort_effektiv": "Wandregal Alu",
  "reserviert_fuer": "",
  "material_id": 1,
  "material": "Alu Profil 40x40 Nut 8",
  "laenge": 6000.0,
  "breite": 0.0,
  "ist_rest": false,
  "etikett_gedruckt": 0.0
}
DELETE /api/stock/4 Delete stock row 204
Response
(no content, status 204)
POST /api/pieces/AP40-02/take Error: piece is reserved 409
Request
{
  "grund": "verbraucht"
}
Response
{
  "detail": {
    "code": "stueck_reserviert",
    "message": "AP40-02 is reserved by “Werkstattregal”",
    "projekt": "Werkstattregal"
  }
}
POST /api/pieces/AP40-03/take Take out a piece 200
Request
{
  "grund": "verkauft",
  "notiz": "an Nachbar"
}
Response
{
  "code": "AP40-03",
  "grund": "verkauft",
  "reserviert_fuer": "",
  "abfallkiste": false
}

Defects

POST /api/pieces/4/defects Add defect (position from the start) 201
Request
{
  "position": 1200,
  "laenge": 80
}
Response
{
  "id": 1,
  "einzelstueck_id": 4,
  "stueck_nr": 4,
  "position": 1200.0,
  "position_y": 0.0,
  "laenge": 80.0,
  "breite": 0.0
}
GET /api/pieces/4/defects Defects of a piece 200
Response
[
  {
    "id": 1,
    "einzelstueck_id": 4,
    "stueck_nr": 4,
    "position": 1200.0,
    "position_y": 0.0,
    "laenge": 80.0,
    "breite": 0.0
  }
]
GET /api/stock/1/defects Defects of a stock row 200
Response
[
  {
    "id": 1,
    "einzelstueck_id": 4,
    "stueck_nr": 4,
    "position": 1200.0,
    "position_y": 0.0,
    "laenge": 80.0,
    "breite": 0.0
  }
]
PUT /api/defects/1 Change defect 200
Request
{
  "position": 1200,
  "laenge": 120
}
Response
{
  "id": 1,
  "einzelstueck_id": 4,
  "stueck_nr": 4,
  "position": 1200.0,
  "position_y": 0.0,
  "laenge": 120.0,
  "breite": 0.0
}
DELETE /api/defects/1 Delete defect 204
Response
(no content, status 204)

Locations

GET /api/locations All locations 200
Response
[
  {
    "id": 4,
    "name": "Lager",
    "materialien": 0,
    "stuecke": 0
  },
  {
    "id": 3,
    "name": "Plattenwagen",
    "materialien": 1,
    "stuecke": 3
  },
  "… 2 weitere"
]
POST /api/locations Create location 201
Request
{
  "name": "Regal 3"
}
Response
{
  "id": 5,
  "name": "Regal 3",
  "materialien": 0,
  "stuecke": 0
}
PUT /api/locations/Regal 3 Rename location 200
Request
{
  "name": "Regal 3 oben"
}
Response
{
  "id": 5,
  "name": "Regal 3 oben",
  "materialien": 0,
  "stuecke": 0
}
GET /api/locations/Wandregal Alu/contents What is stored at a location 200
Response
[
  {
    "material_id": 1,
    "name": "Alu Profil 40x40 Nut 8",
    "kuerzel": "AP40",
    "typ": "Stange",
    "rolle": "bestand",
    "bestand": [
      {
        "stock_id": 1,
        "laenge": 6000.0,
        "breite": 0.0,
        "stueckzahl": 4,
        "ist_rest": false,
        "stuecke": [
          {
            "id": 1,
            "lagerstueck_id": 1,
            "nr": 1,
            "notiz": "",
            "defekt_count": 0,
            "code": "AP40-01",
            "lagerort": "Wandregal Alu",
            "lagerort_effektiv": "Wandregal Alu",
            "reserviert_fuer": "",
            "material_id": 1,
            "material": "Alu Profil 40x40 Nut 8",
            "laenge": 6000.0,
            "breite": 0.0,
            "ist_rest": false,
            "etikett_gedruckt": 0.0
          },
          "… 3 weitere"
        ]
      }
    ]
  }
]
PUT /api/materials/AP40/location Assign material 200
Request
{
  "lagerort": "Wandregal Alu",
  "lagerort_reste": "Restekiste Alu"
}
Response
{
  "id": 1,
  "name": "Alu Profil 40x40 Nut 8",
  "typ": "Stange",
  "dicke": 0.0,
  "querschnitt_breite": 40.0,
  "querschnitt_tiefe": 40.0,
  "maserung": "keine",
  "drehung_erlaubt": true,
  "besaeumung": 0.0,
  "rest_min_laenge": 250.0,
  "rest_min_breite": 0.0,
  "hat_bild": false,
  "kuerzel": "AP40",
  "lagerort": "Wandregal Alu",
  "lagerort_reste": "Restekiste Alu"
}
DELETE /api/locations/Wandregal Alu Error: location still in use 409
Response
{
  "detail": {
    "code": "lagerort_in_benutzung",
    "message": "Location is still used by materials",
    "materialien": [
      "Alu Profil 40x40 Nut 8"
    ]
  }
}
DELETE /api/locations/Regal 3 oben Delete empty location (with ?ziel=… also one in use) 204
Response
(no content, status 204)

Saw blades

POST /api/blades Create saw blade 201
Request
{
  "name": "Kappsäge 2,8 mm",
  "schnittbreite": 2.8
}
Response
{
  "id": 1,
  "name": "Kappsäge 2,8 mm",
  "schnittbreite": 2.8
}
GET /api/blades All saw blades 200
Response
[
  {
    "id": 1,
    "name": "Kappsäge 2,8 mm",
    "schnittbreite": 2.8
  }
]
PUT /api/blades/2 Change saw blade 200
Request
{
  "name": "Japansäge",
  "schnittbreite": 0.8
}
Response
{
  "id": 2,
  "name": "Japansäge",
  "schnittbreite": 0.8
}
DELETE /api/blades/2 Delete saw blade 204
Response
(no content, status 204)

Projects

POST /api/projects Create project 201
Request
{
  "name": "Werkstattregal",
  "anzahl": 1
}
Response
{
  "id": 1,
  "name": "Werkstattregal",
  "anzahl": 1,
  "teile": [],
  "plaene": [],
  "sammel": {},
  "art": "",
  "sammel_material_id": 0,
  "mitglieder": []
}
POST /api/projects/1/parts Add part 201
Request
{
  "label": "Pfosten",
  "typ": "Stange",
  "material_id": 1,
  "laenge": 1800,
  "stueckzahl": 4
}
Response
{
  "id": 1,
  "projekt_id": 1,
  "label": "Pfosten",
  "typ": "Stange",
  "material_id": 1,
  "laenge": 1800.0,
  "breite": 0.0,
  "stueckzahl": 4,
  "gesaegt_anzahl": 0,
  "maserung": "egal",
  "status": "offen",
  "offen_anzahl": 4,
  "gesamt_anzahl": 4,
  "vorrat": 0,
  "herkunft_teil_id": 0
}
PUT /api/parts/1 Change part 200
Request
{
  "label": "Pfosten",
  "typ": "Stange",
  "material_id": 1,
  "laenge": 1750,
  "stueckzahl": 4
}
Response
{
  "id": 1,
  "projekt_id": 1,
  "label": "Pfosten",
  "typ": "Stange",
  "material_id": 1,
  "laenge": 1750.0,
  "breite": 0.0,
  "stueckzahl": 4,
  "gesaegt_anzahl": 0,
  "maserung": "egal",
  "status": "offen",
  "offen_anzahl": 4,
  "gesamt_anzahl": 4,
  "vorrat": 0,
  "herkunft_teil_id": 0
}
GET /api/projects/1/parts Parts of a project 200
Response
[
  {
    "id": 1,
    "projekt_id": 1,
    "label": "Pfosten",
    "typ": "Stange",
    "material_id": 1,
    "laenge": 1750.0,
    "breite": 0.0,
    "stueckzahl": 4,
    "gesaegt_anzahl": 0,
    "maserung": "egal",
    "status": "offen",
    "offen_anzahl": 4,
    "gesamt_anzahl": 4,
    "vorrat": 0,
    "herkunft_teil_id": 0
  },
  {
    "id": 2,
    "projekt_id": 1,
    "label": "Riegel",
    "typ": "Stange",
    "material_id": 1,
    "laenge": 760.0,
    "breite": 0.0,
    "stueckzahl": 6,
    "gesaegt_anzahl": 0,
    "maserung": "egal",
    "status": "offen",
    "offen_anzahl": 6,
    "gesamt_anzahl": 6,
    "vorrat": 0,
    "herkunft_teil_id": 0
  }
]
PUT /api/projects/1 Change project (2 copies) 200
Request
{
  "name": "Werkstattregal",
  "anzahl": 2
}
Response
{
  "id": 1,
  "name": "Werkstattregal",
  "anzahl": 2,
  "teile": [
    {
      "id": 1,
      "projekt_id": 1,
      "label": "Pfosten",
      "typ": "Stange",
      "material_id": 1,
      "laenge": 1750.0,
      "breite": 0.0,
      "stueckzahl": 4,
      "gesaegt_anzahl": 0,
      "maserung": "egal",
      "status": "offen",
      "offen_anzahl": 8,
      "gesamt_anzahl": 8,
      "vorrat": 0,
      "herkunft_teil_id": 0
    },
    {
      "id": 2,
      "projekt_id": 1,
      "label": "Riegel",
      "typ": "Stange",
      "material_id": 1,
      "laenge": 760.0,
      "breite": 0.0,
      "stueckzahl": 6,
      "gesaegt_anzahl": 0,
      "maserung": "egal",
      "status": "offen",
      "offen_anzahl": 12,
      "gesamt_anzahl": 12,
      "vorrat": 0,
      "herkunft_teil_id": 0
    }
  ],
  "plaene": [],
  "sammel": {},
  "art": "",
  "sammel_material_id": 0,
  "mitglieder": []
}
GET /api/projects/1 A project with parts and plans 200
Response
{
  "id": 1,
  "name": "Werkstattregal",
  "anzahl": 1,
  "teile": [
    {
      "id": 1,
      "projekt_id": 1,
      "label": "Pfosten",
      "typ": "Stange",
      "material_id": 1,
      "laenge": 1750.0,
      "breite": 0.0,
      "stueckzahl": 4,
      "gesaegt_anzahl": 0,
      "maserung": "egal",
      "status": "offen",
      "offen_anzahl": 4,
      "gesamt_anzahl": 4,
      "vorrat": 0,
      "herkunft_teil_id": 0
    },
    {
      "id": 2,
      "projekt_id": 1,
      "label": "Riegel",
      "typ": "Stange",
      "material_id": 1,
      "laenge": 760.0,
      "breite": 0.0,
      "stueckzahl": 6,
      "gesaegt_anzahl": 0,
      "maserung": "egal",
      "status": "offen",
      "offen_anzahl": 6,
      "gesamt_anzahl": 6,
      "vorrat": 0,
      "herkunft_teil_id": 0
    }
  ],
  "plaene": [],
  "sammel": {},
  "art": "",
  "sammel_material_id": 0,
  "mitglieder": []
}
POST /api/projects/1/clone Copy project 201
Response
{
  "id": 2,
  "name": "Werkstattregal (Kopie)",
  "anzahl": 1,
  "teile": [
    {
      "id": 3,
      "projekt_id": 2,
      "label": "Pfosten",
      "typ": "Stange",
      "material_id": 1,
      "laenge": 1750.0,
      "breite": 0.0,
      "stueckzahl": 4,
      "gesaegt_anzahl": 0,
      "maserung": "egal",
      "status": "offen",
      "offen_anzahl": 4,
      "gesamt_anzahl": 4,
      "vorrat": 0,
      "herkunft_teil_id": 0
    },
    {
      "id": 4,
      "projekt_id": 2,
      "label": "Riegel",
      "typ": "Stange",
      "material_id": 1,
      "laenge": 760.0,
      "breite": 0.0,
      "stueckzahl": 6,
      "gesaegt_anzahl": 0,
      "maserung": "egal",
      "status": "offen",
      "offen_anzahl": 6,
      "gesamt_anzahl": 6,
      "vorrat": 0,
      "herkunft_teil_id": 0
    }
  ],
  "plaene": [],
  "sammel": {},
  "art": "",
  "sammel_material_id": 0,
  "mitglieder": []
}
POST /api/projects/3/copy-parts Copy parts into another project 201
Request
{
  "part_ids": [
    1
  ]
}
Response
[
  {
    "id": 5,
    "projekt_id": 3,
    "label": "Pfosten",
    "typ": "Stange",
    "material_id": 1,
    "laenge": 1750.0,
    "breite": 0.0,
    "stueckzahl": 4,
    "gesaegt_anzahl": 0,
    "maserung": "egal",
    "status": "offen",
    "offen_anzahl": 4,
    "gesamt_anzahl": 4,
    "vorrat": 0,
    "herkunft_teil_id": 0
  }
]
DELETE /api/parts/7 Delete part 204
Response
(no content, status 204)
DELETE /api/projects/2 Delete project 204
Response
(no content, status 204)
GET /api/projects All projects 200
Response
[
  {
    "id": 3,
    "name": "Schubladenschrank",
    "anzahl": 1,
    "teile": [
      {
        "id": 6,
        "projekt_id": 3,
        "label": "Leiste",
        "typ": "Stange",
        "material_id": 1,
        "laenge": 200.0,
        "breite": 0.0,
        "stueckzahl": 10,
        "gesaegt_anzahl": 0,
        "maserung": "egal",
        "status": "offen",
        "offen_anzahl": 10,
        "gesamt_anzahl": 10,
        "vorrat": 0,
        "herkunft_teil_id": 0
      },
      "… 1 weitere"
    ],
    "plaene": [],
    "sammel": {},
    "art": "",
    "sammel_material_id": 0,
    "mitglieder": []
  },
  "… 1 weitere"
]

Parts and cut lists

GET /api/parts Parts of all projects (Parts tab) 200
Response
[
  {
    "teil_id": 6,
    "label": "Leiste",
    "projekt_id": 3,
    "projekt": "Schubladenschrank",
    "material_id": 1,
    "laenge": 200.0,
    "breite": 0.0,
    "offen": 10,
    "gesamt": 10,
    "vorrat": 0
  },
  {
    "teil_id": 5,
    "label": "Pfosten",
    "projekt_id": 3,
    "projekt": "Schubladenschrank",
    "material_id": 1,
    "laenge": 1750.0,
    "breite": 0.0,
    "offen": 4,
    "gesamt": 4,
    "vorrat": 0
  },
  "… 2 weitere"
]
POST /api/cut-lists Create a cut list from parts 201
Request
{
  "name": "Nachschub Leisten",
  "eintraege": [
    {
      "teil_id": 6,
      "menge": 4
    }
  ]
}
Response
{
  "id": 4,
  "name": "Nachschub Leisten",
  "anzahl": 1,
  "teile": [
    {
      "id": 8,
      "projekt_id": 4,
      "label": "Leiste (Schubladenschrank)",
      "typ": "Stange",
      "material_id": 1,
      "laenge": 200.0,
      "breite": 0.0,
      "stueckzahl": 4,
      "gesaegt_anzahl": 0,
      "maserung": "egal",
      "status": "offen",
      "offen_anzahl": 4,
      "gesamt_anzahl": 4,
      "vorrat": 0,
      "herkunft_teil_id": 6
    }
  ],
  "plaene": [],
  "sammel": {},
  "art": "liste",
  "sammel_material_id": 0,
  "mitglieder": []
}

Optimization

GET /api/projects/1/fill-candidates/1 Possible filler parts from other projects 200
Response
[
  {
    "teil_id": 6,
    "label": "Leiste",
    "projekt": "Schubladenschrank",
    "projekt_id": 3,
    "laenge": 200.0,
    "breite": 0.0,
    "offen": 10,
    "vorrat": 0
  },
  {
    "teil_id": 5,
    "label": "Pfosten",
    "projekt": "Schubladenschrank",
    "projekt_id": 3,
    "laenge": 1750.0,
    "breite": 0.0,
    "offen": 4,
    "vorrat": 0
  }
]
POST /api/optimize Optimize (response = saved plan) 200
Request
{
  "project_id": 1,
  "material_id": 1,
  "blade_id": 1,
  "algorithm": "nested",
  "fuellteile": [
    {
      "teil_id": 6,
      "modus": "auffuellen",
      "menge": 0
    }
  ]
}
Response
{
  "schnittplaene": [
    {
      "lagerstueck_id": 1,
      "lager_laenge": 6000.0,
      "lager_breite": 0.0,
      "stueck_nr": 1,
      "platzierungen": [
        {
          "teil_label": "Pfosten",
          "laenge": 1750.0,
          "breite": 0.0,
          "x": 0.0,
          "y": 0.0,
          "gedreht": false,
          "id": 1,
          "gesaegt": false,
          "ist_fuellteil": false
        },
        "… 5 weitere"
      ],
      "reste": [
        [
          3.1999999999989086
        ]
      ],
      "verschnitt_mm": 6.0,
      "verschnitt_prozent": 0.1,
      "defekte": [],
      "id": 1,
      "verbraucht": false,
      "stueck_code": "AP40-01",
      "lagerort": "Wandregal Alu",
      "ist_rest": false,
      "reste_stuecke": [],
      "reste_ziel": [
        "weg"
      ]
    },
    "… 1 weitere"
  ],
  "fehlende_teile": [],
  "gesamt_verschnitt_prozent": 0.3,
  "materialbedarf": [],
  "plan_id": 1,
  "projekt_id": 1,
  "material_id": 1,
  "saegeblatt_id": 1,
  "algorithmus": "nested",
  "erstellt": 1793530040.890228,
  "version": 1,
  "veraltet": false,
  "lager_fehlt": false,
  "fehlende_stuecke": [],
  "fuellteile": [
    {
      "teil_id": 6,
      "modus": "auffuellen",
      "menge": 0
    }
  ],
  "fuell_alle_reste": false,
  "vorrat_verfuegbar": [],
  "max_pro_stange": 0,
  "fuell_vorschlaege": [
    {
      "gruppe": "206.0",
      "rest": 206.0,
      "stangen": 2,
      "vorschlaege": [
        {
          "teile": [
            {
              "teil_id": 6,
              "label": "Leiste → Schubladenschrank",
              "laenge": 200.0,
              "anzahl": 1
            }
          ],
          "rest": 3.2
        }
      ],
      "gewaehlt": 0,
      "manuell": false,
      "automatisch": 0
    }
  ]
}
POST /api/plans/1/fill-choice Choose the filling of an offcut group (null = automatic) 200
Request
{
  "gruppe": "206.0",
  "wahl": null
}
Response
{
  "schnittplaene": [
    {
      "lagerstueck_id": 1,
      "lager_laenge": 6000.0,
      "lager_breite": 0.0,
      "stueck_nr": 1,
      "platzierungen": [
        {
          "teil_label": "Pfosten",
          "laenge": 1750.0,
          "breite": 0.0,
          "x": 0.0,
          "y": 0.0,
          "gedreht": false,
          "id": 13,
          "gesaegt": false,
          "ist_fuellteil": false
        },
        "… 5 weitere"
      ],
      "reste": [
        [
          3.1999999999989086
        ]
      ],
      "verschnitt_mm": 6.0,
      "verschnitt_prozent": 0.1,
      "defekte": [],
      "id": 3,
      "verbraucht": false,
      "stueck_code": "AP40-01",
      "lagerort": "Wandregal Alu",
      "ist_rest": false,
      "reste_stuecke": [],
      "reste_ziel": [
        "weg"
      ]
    },
    "… 1 weitere"
  ],
  "fehlende_teile": [],
  "gesamt_verschnitt_prozent": 0.3,
  "materialbedarf": [],
  "plan_id": 2,
  "projekt_id": 1,
  "material_id": 1,
  "saegeblatt_id": 1,
  "algorithmus": "nested",
  "erstellt": 1793530040.890228,
  "version": 2,
  "veraltet": false,
  "lager_fehlt": false,
  "fehlende_stuecke": [],
  "fuellteile": [
    {
      "teil_id": 6,
      "modus": "auffuellen",
      "menge": 0
    }
  ],
  "fuell_alle_reste": false,
  "vorrat_verfuegbar": [],
  "max_pro_stange": 0,
  "fuell_vorschlaege": [
    {
      "gruppe": "206.0",
      "rest": 206.0,
      "stangen": 2,
      "vorschlaege": [
        {
          "teile": [
            {
              "teil_id": 6,
              "label": "Leiste → Schubladenschrank",
              "laenge": 200.0,
              "anzahl": 1
            }
          ],
          "rest": 3.2
        }
      ],
      "gewaehlt": 0,
      "manuell": false,
      "automatisch": 0
    }
  ]
}
GET /api/projects/1/plans/1 Get saved plan 200
Response
{
  "schnittplaene": [
    {
      "lagerstueck_id": 1,
      "lager_laenge": 6000.0,
      "lager_breite": 0.0,
      "stueck_nr": 1,
      "platzierungen": [
        {
          "teil_label": "Pfosten",
          "laenge": 1750.0,
          "breite": 0.0,
          "x": 0.0,
          "y": 0.0,
          "gedreht": false,
          "id": 13,
          "gesaegt": false,
          "ist_fuellteil": false
        },
        "… 5 weitere"
      ],
      "reste": [
        [
          3.1999999999989086
        ]
      ],
      "verschnitt_mm": 6.0,
      "verschnitt_prozent": 0.1,
      "defekte": [],
      "id": 3,
      "verbraucht": false,
      "stueck_code": "AP40-01",
      "lagerort": "Wandregal Alu",
      "ist_rest": false,
      "reste_stuecke": [],
      "reste_ziel": [
        "weg"
      ]
    },
    "… 1 weitere"
  ],
  "fehlende_teile": [],
  "gesamt_verschnitt_prozent": 0.3,
  "materialbedarf": [],
  "plan_id": 2,
  "projekt_id": 1,
  "material_id": 1,
  "saegeblatt_id": 1,
  "algorithmus": "nested",
  "erstellt": 1793530040.890228,
  "version": 2,
  "veraltet": false,
  "lager_fehlt": false,
  "fehlende_stuecke": [],
  "fuellteile": [
    {
      "teil_id": 6,
      "modus": "auffuellen",
      "menge": 0
    }
  ],
  "fuell_alle_reste": false,
  "vorrat_verfuegbar": [],
  "max_pro_stange": 0,
  "fuell_vorschlaege": [
    {
      "gruppe": "206.0",
      "rest": 206.0,
      "stangen": 2,
      "vorschlaege": [
        {
          "teile": [
            {
              "teil_id": 6,
              "label": "Leiste → Schubladenschrank",
              "laenge": 200.0,
              "anzahl": 1
            }
          ],
          "rest": 3.2
        }
      ],
      "gewaehlt": 0,
      "manuell": false,
      "automatisch": 0
    }
  ]
}
POST /api/plan-pieces/18 Tick off a piece as cut 200
Request
{
  "gesaegt": true
}
Response
{
  "plan": {
    "schnittplaene": [
      {
        "lagerstueck_id": 1,
        "lager_laenge": 6000.0,
        "lager_breite": 0.0,
        "stueck_nr": 1,
        "platzierungen": [
          {
            "teil_label": "Pfosten",
            "laenge": 1750.0,
            "breite": 0.0,
            "x": 0.0,
            "y": 0.0,
            "gedreht": false,
            "id": 13,
            "gesaegt": false,
            "ist_fuellteil": false
          },
          "… 5 weitere"
        ],
        "reste": [
          [
            3.1999999999989086
          ]
        ],
        "verschnitt_mm": 6.0,
        "verschnitt_prozent": 0.1,
        "defekte": [],
        "id": 3,
        "verbraucht": true,
        "stueck_code": "AP40-01",
        "lagerort": "Wandregal Alu",
        "ist_rest": false,
        "reste_stuecke": [
          {
            "code": "AP40-01",
            "laenge": 5797.2,
            "breite": 0.0,
            "lagerort": "Restekiste Alu"
          }
        ],
        "reste_ziel": [
          "weg"
        ]
      },
      "… 1 weitere"
    ],
    "fehlende_teile": [],
    "gesamt_verschnitt_prozent": 0.3,
    "materialbedarf": [],
    "plan_id": 2,
    "projekt_id": 1,
    "material_id": 1,
    "saegeblatt_id": 1,
    "algorithmus": "nested",
    "erstellt": 1793530040.890228,
    "version": 3,
    "veraltet": false,
    "lager_fehlt": false,
    "fehlende_stuecke": [],
    "fuellteile": [
      {
        "teil_id": 6,
        "modus": "auffuellen",
        "menge": 0
      }
    ],
    "fuell_alle_reste": false,
    "vorrat_verfuegbar": [],
    "max_pro_stange": 0,
    "fuell_vorschlaege": [
      {
        "gruppe": "206.0",
        "rest": 206.0,
        "stangen": 2,
        "vorschlaege": [
          {
            "teile": [
              {
                "teil_id": 6,
                "label": "Leiste → Schubladenschrank",
                "laenge": 200.0,
                "anzahl": 1
              }
            ],
            "rest": 3.2
          }
        ],
        "gewaehlt": 0,
        "manuell": false,
        "automatisch": 0
      }
    ]
  },
  "teil": {
    "id": 6,
    "projekt_id": 3,
    "label": "Leiste",
    "typ": "Stange",
    "material_id": 1,
    "laenge": 200.0,
    "breite": 0.0,
    "stueckzahl": 10,
    "gesaegt_anzahl": 0,
    "maserung": "egal",
    "status": "offen",
    "offen_anzahl": 10,
    "gesamt_anzahl": 10,
    "vorrat": 1,
    "herkunft_teil_id": 0
  }
}
POST /api/pdf Cut plan as PDF 200
Request
{
  "ergebnis": {
    "schnittplaene": [
      {
        "lagerstueck_id": 1,
        "lager_laenge": 6000.0,
        "lager_breite": 0.0,
        "stueck_nr": 1,
        "platzierungen": [
          {
            "teil_label": "Pfosten",
            "laenge": 1750.0,
            "breite": 0.0,
            "x": 0.0,
            "y": 0.0,
            "gedreht": false,
            "id": 13,
            "gesaegt": false,
            "ist_fuellteil": false
          },
          "… 5 weitere"
        ],
        "reste": [
          [
            3.1999999999989086
          ]
        ],
        "verschnitt_mm": 6.0,
        "verschnitt_prozent": 0.1,
        "defekte": [],
        "id": 3,
        "verbraucht": false,
        "stueck_code": "AP40-01",
        "lagerort": "Wandregal Alu",
        "ist_rest": false,
        "reste_stuecke": [],
        "reste_ziel": [
          "weg"
        ]
      },
      "… 1 weitere"
    ],
    "fehlende_teile": [],
    "gesamt_verschnitt_prozent": 0.3,
    "materialbedarf": [],
    "plan_id": 2,
    "projekt_id": 1,
    "material_id": 1,
    "saegeblatt_id": 1,
    "algorithmus": "nested",
    "erstellt": 1793530040.890228,
    "version": 2,
    "veraltet": false,
    "lager_fehlt": false,
    "fehlende_stuecke": [],
    "fuellteile": [
      {
        "teil_id": 6,
        "modus": "auffuellen",
        "menge": 0
      }
    ],
    "fuell_alle_reste": false,
    "vorrat_verfuegbar": [],
    "max_pro_stange": 0,
    "fuell_vorschlaege": [
      {
        "gruppe": "206.0",
        "rest": 206.0,
        "stangen": 2,
        "vorschlaege": [
          {
            "teile": [
              {
                "teil_id": 6,
                "label": "Leiste → Schubladenschrank",
                "laenge": 200.0,
                "anzahl": 1
              }
            ],
            "rest": 3.2
          }
        ],
        "gewaehlt": 0,
        "manuell": false,
        "automatisch": 0
      }
    ]
  },
  "projekt_name": "Werkstattregal",
  "material_name": "Alu Profil 40x40 Nut 8",
  "material_id": 1,
  "saegeblatt_name": "Kappsäge 2,8 mm",
  "is_1d": true
}
Response
File: application/pdf, 2,788 Bytes
Content-Disposition: attachment; filename="CutStock-Werkstattregal.pdf"
POST /api/plans/2/confirm-all Mark all pieces as cut 200
Response
{
  "schnittplaene": [
    {
      "lagerstueck_id": 1,
      "lager_laenge": 6000.0,
      "lager_breite": 0.0,
      "stueck_nr": 1,
      "platzierungen": [
        {
          "teil_label": "Pfosten",
          "laenge": 1750.0,
          "breite": 0.0,
          "x": 0.0,
          "y": 0.0,
          "gedreht": false,
          "id": 13,
          "gesaegt": true,
          "ist_fuellteil": false
        },
        "… 5 weitere"
      ],
      "reste": [
        [
          3.1999999999989086
        ]
      ],
      "verschnitt_mm": 6.0,
      "verschnitt_prozent": 0.1,
      "defekte": [],
      "id": 3,
      "verbraucht": true,
      "stueck_code": "AP40-01",
      "lagerort": "Wandregal Alu",
      "ist_rest": false,
      "reste_stuecke": [],
      "reste_ziel": [
        "weg"
      ]
    },
    "… 1 weitere"
  ],
  "fehlende_teile": [],
  "gesamt_verschnitt_prozent": 0.3,
  "materialbedarf": [],
  "plan_id": 2,
  "projekt_id": 1,
  "material_id": 1,
  "saegeblatt_id": 1,
  "algorithmus": "nested",
  "erstellt": 1793530040.890228,
  "version": 4,
  "veraltet": false,
  "lager_fehlt": false,
  "fehlende_stuecke": [],
  "fuellteile": [
    {
      "teil_id": 6,
      "modus": "auffuellen",
      "menge": 0
    }
  ],
  "fuell_alle_reste": false,
  "vorrat_verfuegbar": [],
  "max_pro_stange": 0,
  "fuell_vorschlaege": [
    {
      "gruppe": "206.0",
      "rest": 206.0,
      "stangen": 2,
      "vorschlaege": [
        {
          "teile": [
            {
              "teil_id": 6,
              "label": "Leiste → Schubladenschrank",
              "laenge": 200.0,
              "anzahl": 1
            }
          ],
          "rest": 3.2
        }
      ],
      "gewaehlt": 0,
      "manuell": false,
      "automatisch": 0
    }
  ]
}
POST /api/projects/3/take-stock/1 Take over the parts shelf stock as cut 200
Response
{
  "id": 3,
  "name": "Schubladenschrank",
  "anzahl": 1,
  "teile": [
    {
      "id": 6,
      "projekt_id": 3,
      "label": "Leiste",
      "typ": "Stange",
      "material_id": 1,
      "laenge": 200.0,
      "breite": 0.0,
      "stueckzahl": 10,
      "gesaegt_anzahl": 2,
      "maserung": "egal",
      "status": "offen",
      "offen_anzahl": 8,
      "gesamt_anzahl": 10,
      "vorrat": 0,
      "herkunft_teil_id": 0
    },
    "… 1 weitere"
  ],
  "plaene": [],
  "sammel": {},
  "art": "",
  "sammel_material_id": 0,
  "mitglieder": []
}
POST /api/projects/1/restart Cut the project again 200
Response
{
  "id": 1,
  "name": "Werkstattregal",
  "anzahl": 1,
  "teile": [
    {
      "id": 1,
      "projekt_id": 1,
      "label": "Pfosten",
      "typ": "Stange",
      "material_id": 1,
      "laenge": 1750.0,
      "breite": 0.0,
      "stueckzahl": 4,
      "gesaegt_anzahl": 0,
      "maserung": "egal",
      "status": "offen",
      "offen_anzahl": 4,
      "gesamt_anzahl": 4,
      "vorrat": 0,
      "herkunft_teil_id": 0
    },
    "… 1 weitere"
  ],
  "plaene": [],
  "sammel": {},
  "art": "",
  "sammel_material_id": 0,
  "mitglieder": []
}
DELETE /api/plans/3 Discard plan 204
Response
(no content, status 204)

Offcut bin

POST /api/waste Put scrap pieces in 201
Request
{
  "material": "AP40",
  "laenge": 180,
  "anzahl": 2
}
Response
[
  {
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "kuerzel": "AP40",
    "typ": "Stange",
    "laenge": 180.0,
    "breite": 0.0,
    "anzahl": 2
  }
]
GET /api/waste Contents of the offcut bin 200
Response
[
  {
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "kuerzel": "AP40",
    "typ": "Stange",
    "laenge": 180.0,
    "breite": 0.0,
    "anzahl": 2
  }
]
POST /api/waste/take Take one scrap piece out 200
Request
{
  "material": "AP40",
  "laenge": 180
}
Response
[
  {
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "kuerzel": "AP40",
    "typ": "Stange",
    "laenge": 180.0,
    "breite": 0.0,
    "anzahl": 1
  }
]
DELETE /api/waste?material=AP40 Empty the bin (without ?material= completely) 204
Response
(no content, status 204)

Journal

GET /api/journal?material=AP40&limit=3 Stock journal (?material, ?seit, ?limit) 200
Response
[
  {
    "id": 23,
    "zeit": 1793530040.950529,
    "art": "abfall_leer",
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "code": "",
    "laenge": 0.0,
    "breite": 0.0,
    "anzahl": 1,
    "quelle": "api",
    "notiz": ""
  },
  {
    "id": 22,
    "zeit": 1793530040.94593,
    "art": "abfall_ab",
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "code": "",
    "laenge": 180.0,
    "breite": 0.0,
    "anzahl": 1,
    "quelle": "api",
    "notiz": ""
  },
  {
    "id": 21,
    "zeit": 1793530040.937907,
    "art": "abfall_ein",
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "code": "",
    "laenge": 180.0,
    "breite": 0.0,
    "anzahl": 2,
    "quelle": "api",
    "notiz": ""
  }
]

Combined plan

GET /api/sammel/1 Combined plan status and candidates 200
Response
{
  "sammel_id": 0,
  "name": "",
  "kandidaten": [
    {
      "projekt_id": 4,
      "name": "Nachschub Leisten",
      "offen": 4,
      "bedarf": 800.0,
      "gesperrt": 0,
      "einzelplan": "",
      "mitglied": false
    },
    {
      "projekt_id": 3,
      "name": "Schubladenschrank",
      "offen": 12,
      "bedarf": 8600.0,
      "gesperrt": 0,
      "einzelplan": "",
      "mitglied": false
    },
    "… 1 weitere"
  ]
}
POST /api/sammel/compare Separate or combined? (comparison) 200
Request
{
  "material_id": 1,
  "projekt_ids": [
    1,
    3
  ],
  "blade_id": 1
}
Response
{
  "getrennt_bretter": 2,
  "gemeinsam_bretter": 2,
  "getrennt_menge": 6740.0,
  "gemeinsam_menge": 6740.0,
  "fehlende_getrennt": 14,
  "fehlende_gemeinsam": 13,
  "getrennt_reste": 1,
  "gemeinsam_reste": 1
}
POST /api/sammel Create a combined plan (material by code, projects by name) 201
Request
{
  "material": "AP40",
  "projekte": [
    "Werkstattregal"
  ]
}
Response
{
  "id": 5,
  "name": "Sammelplan Alu Profil 40x40 Nut 8",
  "anzahl": 1,
  "teile": [],
  "plaene": [],
  "sammel": {},
  "art": "sammel",
  "sammel_material_id": 1,
  "mitglieder": [
    1
  ]
}
POST /api/sammel Error: material already has a combined plan 409
Request
{
  "material": "AP40",
  "projekte": [
    "Werkstattregal"
  ]
}
Response
{
  "detail": {
    "code": "sammel_vorhanden",
    "message": "“Alu Profil 40x40 Nut 8” already has a combined plan",
    "sammel_id": 5
  }
}
PUT /api/sammel/AP40/members Change members 200
Request
{
  "projekte": [
    "Werkstattregal",
    "Schubladenschrank"
  ]
}
Response
{
  "id": 5,
  "name": "Sammelplan Alu Profil 40x40 Nut 8",
  "anzahl": 1,
  "teile": [],
  "plaene": [],
  "sammel": {},
  "art": "sammel",
  "sammel_material_id": 1,
  "mitglieder": [
    1,
    "… 1 weitere"
  ]
}
POST /api/sammel/optimize Optimize together 200
Request
{
  "material_id": 1,
  "projekt_ids": [
    1,
    3
  ],
  "blade_id": 1
}
Response
{
  "schnittplaene": [
    {
      "lagerstueck_id": 1,
      "lager_laenge": 6000.0,
      "lager_breite": 0.0,
      "stueck_nr": 4,
      "platzierungen": [
        {
          "teil_label": "Pfosten → Werkstattregal",
          "laenge": 1750.0,
          "breite": 0.0,
          "x": 0.0,
          "y": 0.0,
          "gedreht": false,
          "id": 30,
          "gesaegt": false,
          "ist_fuellteil": false
        },
        "… 5 weitere"
      ],
      "reste": [
        [
          5.9999999999990905,
          "… 1 weitere"
        ]
      ],
      "verschnitt_mm": 6.0,
      "verschnitt_prozent": 0.1,
      "defekte": [],
      "id": 6,
      "verbraucht": false,
      "stueck_code": "AP40-04",
      "lagerort": "Wandregal Alu",
      "ist_rest": false,
      "reste_stuecke": [],
      "reste_ziel": [
        "weg"
      ]
    },
    "… 1 weitere"
  ],
  "fehlende_teile": [
    "Pfosten → Werkstattregal",
    "… 12 weitere"
  ],
  "gesamt_verschnitt_prozent": 2.4,
  "materialbedarf": [
    {
      "laenge": 6000.0,
      "breite": 0.0,
      "anzahl": 3
    }
  ],
  "plan_id": 4,
  "projekt_id": 5,
  "material_id": 1,
  "saegeblatt_id": 1,
  "algorithmus": "greedy",
  "erstellt": 1793530041.27601,
  "version": 1,
  "veraltet": false,
  "lager_fehlt": false,
  "fehlende_stuecke": [],
  "fuellteile": [],
  "fuell_alle_reste": false,
  "vorrat_verfuegbar": [],
  "max_pro_stange": 0,
  "fuell_vorschlaege": []
}
GET /api/projects?art=sammel List combined plans (?art=sammel|alle) 200
Response
[
  {
    "id": 5,
    "name": "Sammelplan Alu Profil 40x40 Nut 8",
    "anzahl": 1,
    "teile": [],
    "plaene": [
      {
        "plan_id": 4,
        "material_id": 1,
        "erstellt": 1793530041.27601,
        "gesamt": 9,
        "gesaegt": 0,
        "angefangen": false
      }
    ],
    "sammel": {},
    "art": "sammel",
    "sammel_material_id": 1,
    "mitglieder": [
      1,
      "… 1 weitere"
    ]
  }
]
DELETE /api/projects/6 Delete a combined plan (cut parts stay in the projects) 204
Response
(no content, status 204)

Labels

POST /api/labels Labels as PDF 200
Request
{
  "labels": [
    {
      "title": "Alu Profil 40x40",
      "line1": "6000 mm",
      "line2": "Wandregal Alu",
      "id_text": "AP40-01"
    }
  ],
  "filename": "Etiketten"
}
Response
File: application/pdf, 1,692 Bytes
Content-Disposition: attachment; filename="CutStock-Etiketten.pdf"
POST /api/labels/pieces Labels for pieces without a label yet 200
Request
{
  "material": "AP40",
  "nur_ungedruckt": true,
  "startfeld": 5
}
Response
File: application/pdf, 1,776 Bytes
Content-Disposition: attachment; filename="CutStock-Alu Profil 40x40 Nut 8.pdf"
POST /api/labels/pieces Error: everything already labelled 404
Request
{
  "material": "AP40",
  "nur_ungedruckt": true
}
Response
{
  "detail": {
    "code": "keine_stuecke",
    "message": "No pieces match the selection"
  }
}

Backup

GET /api/backup Download backup (ZIP) 200
Response
File: application/zip, 6,711 Bytes
Content-Disposition: attachment; filename="cutstock_backup_2026-11-01.zip"
POST /api/restore Restore backup (replaces all data) 200
Request
multipart/form-data, field "file" = cutstock_backup.zip (application/zip)
Response
{
  "status": "ok"
}
POST /api/seed Create sample data (empty database only) 201
Response
{
  "status": "ok",
  "message": "Example data created"
}