REST-API

Für Programme, Scanner und Maschinen

REST-API

Jede Funktion von CutStock ist per REST-API steuerbar – Stücke über ihren Code, Materialien über ihr Kürzel.

Jede Funktion der Oberflaeche ist auch per REST-API unter /api/ erreichbar -- die Oberflaeche selbst nutzt nichts anderes. Alle Endpoints akzeptieren und liefern JSON. Eine automatisch erzeugte, ausprobierbare Beschreibung aller Endpoints liefert CutStock unter /docs.

Fuer Programme, Scanner und Maschinen gilt:

  • Materialien gehen ueber ID oder Kuerzel (AP40), Stuecke ueber ID oder Code (AP40-07, vom Etikett), Lagerorte ueber ID oder Namen.
  • Fehler der Lager-Befehle haben einen festen Code: {"detail": {"code": "stueck_reserviert", "message": "...", "projekt": "..."}}.
  • Jede Lagerbuchung landet im Lagerjournal, mit Quelle api oder ui.

Authentifizierung

Wenn ein API-Key konfiguriert ist (Einstellungen-Seite), muessen externe Anfragen ihn als Bearer-Token mitschicken:

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

Anfragen von der eigenen Web-Oberflaeche (Same-Origin) und von localhost sind immer ohne Key erlaubt.

Endpoints

Methode Pfad Beschreibung
Materialien
GET /api/materials Alle Materialien auflisten
POST /api/materials Material anlegen
PUT /api/materials/{id} Material aktualisieren
DELETE /api/materials/{id} Material loeschen
Lagerbestand
GET /api/stock?material_id=N Lagerbestand auflisten (optionaler Filter)
POST /api/stock Lagereintrag anlegen
PUT /api/stock/{id} Lagereintrag aktualisieren
DELETE /api/stock/{id} Lagereintrag loeschen
GET /api/stock/{id}/pieces Einzelstuecke auflisten (Code, Lagerort, reserviert fuer)
GET /api/pieces/{id oder Code} Stueck suchen: Material, Mass, Lagerort, Reservierung
PUT /api/pieces/{id oder Code} Notiz eines Stuecks aendern
GET/POST/DELETE /api/materials/{id}/image Materialbild lesen, hochladen, entfernen
Lager (siehe unten)
GET /api/locations Lagerorte mit Anzahl Materialien und Stuecke
POST /api/locations Lagerort anlegen {"name": "Regal 3"}
PUT /api/locations/{id oder Name} Lagerort umbenennen
DELETE /api/locations/{id oder Name}?ziel=… Lagerort loeschen; genutzt → ziel noetig
GET /api/locations/{id oder Name}/contents Was dort liegt: Materialien mit Stuecken
PUT /api/materials/{id oder Kuerzel}/location Material einem Lagerort zuweisen
POST /api/stock/receive Zugang: Stuecke einbuchen, liefert die neuen Codes
POST /api/pieces/{id oder Code}/take Stueck entnehmen (mit Grund)
GET /api/waste Inhalt der Abfallkiste
POST /api/waste Abschnitte in die Abfallkiste legen
POST /api/waste/take Abschnitte aus der Abfallkiste nehmen
DELETE /api/waste?material=… Abfallkiste leeren (ganz oder je Material)
GET /api/journal?material=…&seit=… Lagerjournal, neueste Buchung zuerst
Defekte
GET /api/stock/{id}/defects Defekte eines Lagerstuecks
GET /api/pieces/{id}/defects Defekte eines Einzelstuecks
POST /api/pieces/{id}/defects Defekt hinzufuegen
PUT /api/defects/{id} Defekt aktualisieren
DELETE /api/defects/{id} Defekt loeschen
Saegeblaetter
GET /api/blades Saegeblaetter auflisten
POST /api/blades Saegeblatt anlegen
PUT /api/blades/{id} Saegeblatt aktualisieren
DELETE /api/blades/{id} Saegeblatt loeschen
Projekte
GET /api/projects Projekte auflisten
GET /api/projects/{id} Projekt abrufen
POST /api/projects Projekt anlegen
PUT /api/projects/{id} Projekt aktualisieren (Name, Anzahl Exemplare)
DELETE /api/projects/{id} Projekt loeschen
POST /api/projects/{id}/clone Projekt kopieren
POST /api/projects/{id}/copy-parts Teile in ein Projekt kopieren
POST /api/projects/{id}/restart Projekt neu starten (Fortschritt 0, Plaene weg, Lager bleibt)
Teile
GET /api/projects/{id}/parts Teile eines Projekts auflisten
POST /api/projects/{id}/parts Teil hinzufuegen
PUT /api/parts/{id} Teil aktualisieren (optional vorrat: Regal-Bestand setzen)
DELETE /api/parts/{id} Teil loeschen
Optimierung und Plaene
POST /api/optimize Optimieren und als Plan des Projekts fuer das Material speichern
GET /api/projects/{id}/plans/{material_id} Gespeicherten Plan abrufen (null, wenn keiner existiert)
POST /api/plan-pieces/{id} Stueck als gesaegt markieren/zuruecknehmen ({"gesaegt": true}), aktualisiert Fortschritt und Lager
POST /api/plans/{id}/confirm-all Alle offenen Stuecke des Plans als gesaegt markieren
DELETE /api/plans/{id} Plan verwerfen (gibt reservierte Stuecke frei)
GET /api/projects/{id}/fill-candidates/{material_id} Moegliche Fuellteile aus anderen Projekten
GET /api/sammel/{material_id} Sammelplan-Status und moegliche Projekte
POST /api/sammel/optimize Sammelplan anlegen/ersetzen und berechnen
GET /api/parts Teile aller Projekte (Reiter "Teile")
POST /api/cut-lists Schnittliste aus einzelnen Teilen anlegen
POST /api/sammel/compare Vergleich getrennt/gemeinsam (ohne Speichern)
POST /api/projects/{id}/take-stock/{material_id} Teile-Vorrat ("Regal") als gesaegt uebernehmen
Export
POST /api/pdf PDF-Schnittplan erzeugen
POST /api/labels Etiketten-PDF erzeugen
System
GET /api/version App-Version und Links
GET /api/license Lizenzstatus
GET /api/settings Einstellungen lesen
PUT /api/settings Einstellungen aendern
GET /api/backup Datenbank-Backup herunterladen (ZIP)
POST /api/restore Aus Backup wiederherstellen (ZIP)
GET /api/i18n/{lang} Uebersetzungen fuer eine Sprache

Optimierungsanfrage

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
}

Algorithmus-Optionen: greedy, nested, ga. reste_zuerst, fuellteile und fuell_alle_reste sind optional (Fuellteil-Modus auffuellen oder fest; ohne fuellteile bleibt die Auswahl des gespeicherten Plans).

Die Antwort ist der gespeicherte Plan: plan_id, schnittplaene (je Brett u. a. stueck_code, lagerort, platzierungen mit id und gesaegt), fehlende_teile, gesamt_verschnitt_prozent, materialbedarf (zusaetzlich benoetigtes Material -- leer wenn alles passt) sowie Hinweise veraltet und lager_fehlt.

Lager-Befehle

Zugang -- liefert die Codes fuers Beschriften:

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

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

breite nur bei Platten, ist_rest: true fuer Reststuecke (liegen dann am Reste-Ort).

Stueck entnehmen:

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

grund: verbraucht, verkauft, defekt oder abfall (= in die Abfallkiste, wenn eingeschaltet). Ist das Stueck von einem gespeicherten Plan reserviert, kommt 409 mit code: "stueck_reserviert" und dem Projekt; mit "trotzdem": true wird es dennoch entnommen. Der Plan zeigt danach lager_fehlt und fehlende_stuecke.

Material einem Lagerort zuweisen:

PUT /api/materials/AP40/location
{"lagerort": "Wandregal Alu", "lagerort_reste": "Restekiste Alu"}

lagerort_reste: "" = Reste liegen beim Material. Beim Anlegen/Aendern eines Materials ueber /api/materials wird ein unbekannter Lagerort-Name automatisch angelegt.

Lagerort loeschen, der noch genutzt wird:

DELETE /api/locations/Regal%203            → 409 {"code": "lagerort_in_benutzung", "materialien": [...]}
DELETE /api/locations/Regal%203?ziel=Lager → 204, die Materialien liegen jetzt in "Lager"

Abfallkiste (ohne Codes, nur Material + Mass + Anzahl):

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

Lagerjournal: GET /api/journal?material=AP40&seit=1767225600&limit=100 -- je Eintrag zeit (Unix-Zeit), 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, Masse, anzahl, quelle (ui/api), notiz.

Antworten (Beispiele)

So sehen die Antworten aus – erzeugt mit dem echten CutStock und einer Beispiel-Datenbank. Lange Listen sind gekürzt („… n weitere“). Zahlen sind immer Millimeter, zeit ist Unix-Zeit. Ein Klick klappt ein Beispiel auf.

Materialien

GET /api/materials Alle Materialien 200
Antwort
[
  {
    "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": ""
  }
]

Lager

POST /api/stock/receive Zugang: Stücke einbuchen 201
Anfrage
{
  "material": "AP40",
  "laenge": 6000,
  "anzahl": 4
}
Antwort
{
  "stock_id": 1,
  "codes": [
    "AP40-01",
    "AP40-02",
    "… 2 weitere"
  ],
  "lagerort": "Wandregal Alu"
}
POST /api/stock/receive Zugang eines Rests 201
Anfrage
{
  "material": "AP40",
  "laenge": 740,
  "anzahl": 1,
  "ist_rest": true
}
Antwort
{
  "stock_id": 2,
  "codes": [
    "AP40-05"
  ],
  "lagerort": "Restekiste Alu"
}
GET /api/stock?material_id=1 Lagerzeilen eines Materials 200
Antwort
[
  {
    "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
  }
]
GET /api/stock/1/pieces Einzelstücke einer Lagerzeile 200
Antwort
[
  {
    "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
  },
  {
    "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
  },
  "… 2 weitere"
]
GET /api/pieces/AP40-02 Stück über seinen Code 200
Antwort
{
  "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
}

Lagerorte

GET /api/locations Alle Lagerorte 200
Antwort
[
  {
    "id": 3,
    "name": "Plattenwagen",
    "materialien": 1,
    "stuecke": 0
  },
  {
    "id": 2,
    "name": "Restekiste Alu",
    "materialien": 1,
    "stuecke": 1
  },
  "… 1 weitere"
]
POST /api/locations Lagerort anlegen 201
Anfrage
{
  "name": "Regal 3"
}
Antwort
{
  "id": 4,
  "name": "Regal 3",
  "materialien": 0,
  "stuecke": 0
}
GET /api/locations/Wandregal Alu/contents Was an einem Ort liegt 200
Antwort
[
  {
    "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
          },
          "… 3 weitere"
        ]
      }
    ]
  }
]
PUT /api/materials/AP40/location Material zuweisen 200
Anfrage
{
  "lagerort": "Wandregal Alu",
  "lagerort_reste": "Restekiste Alu"
}
Antwort
{
  "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"
}
DELETE /api/locations/Wandregal Alu Fehler: Lagerort noch genutzt 409
Antwort
{
  "detail": {
    "code": "lagerort_in_benutzung",
    "message": "Location is still used by materials",
    "materialien": [
      "Alu Profil 40x40 Nut 8"
    ]
  }
}

Projekte

POST /api/projects Projekt anlegen 201
Anfrage
{
  "name": "Werkstattregal",
  "anzahl": 1
}
Antwort
{
  "id": 1,
  "name": "Werkstattregal",
  "anzahl": 1,
  "teile": [],
  "plaene": [],
  "sammel": {},
  "art": ""
}
POST /api/projects/1/parts Teil hinzufügen 201
Anfrage
{
  "label": "Pfosten",
  "typ": "Stange",
  "material_id": 1,
  "laenge": 1800,
  "stueckzahl": 4
}
Antwort
{
  "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
}

Optimierung

POST /api/optimize Optimieren (Antwort = gespeicherter Plan) 200
Anfrage
{
  "project_id": 1,
  "material_id": 1,
  "blade_id": 1,
  "algorithm": "nested"
}
Antwort
{
  "schnittplaene": [
    {
      "lagerstueck_id": 1,
      "lager_laenge": 6000.0,
      "lager_breite": 0.0,
      "stueck_nr": 1,
      "platzierungen": [
        {
          "teil_label": "Pfosten",
          "laenge": 1800.0,
          "breite": 0.0,
          "x": 0.0,
          "y": 0.0,
          "gedreht": false,
          "id": 1,
          "gesaegt": false,
          "ist_fuellteil": false
        },
        "… 4 weitere"
      ],
      "reste": [
        [
          108.79999999999927,
          "… 1 weitere"
        ]
      ],
      "verschnitt_mm": 108.8,
      "verschnitt_prozent": 1.8,
      "defekte": [],
      "id": 1,
      "verbraucht": false,
      "stueck_code": "AP40-01",
      "lagerort": "Wandregal Alu",
      "ist_rest": false,
      "reste_stuecke": [],
      "reste_ziel": [
        "abfallkiste"
      ]
    },
    "… 1 weitere"
  ],
  "fehlende_teile": [],
  "gesamt_verschnitt_prozent": 2.0,
  "materialbedarf": [],
  "plan_id": 1,
  "projekt_id": 1,
  "material_id": 1,
  "saegeblatt_id": 1,
  "algorithmus": "nested",
  "erstellt": 1793471343.0604548,
  "version": 1,
  "veraltet": false,
  "lager_fehlt": false,
  "fehlende_stuecke": [],
  "fuellteile": [],
  "fuell_alle_reste": false,
  "vorrat_verfuegbar": [],
  "max_pro_stange": 0,
  "fuell_vorschlaege": []
}
POST /api/plan-pieces/1 Stück als gesägt abhaken 200
Anfrage
{
  "gesaegt": true
}
Antwort
{
  "plan": {
    "schnittplaene": [
      {
        "lagerstueck_id": 1,
        "lager_laenge": 6000.0,
        "lager_breite": 0.0,
        "stueck_nr": 1,
        "platzierungen": [
          {
            "teil_label": "Pfosten",
            "laenge": 1800.0,
            "breite": 0.0,
            "x": 0.0,
            "y": 0.0,
            "gedreht": false,
            "id": 1,
            "gesaegt": true,
            "ist_fuellteil": false
          },
          "… 4 weitere"
        ],
        "reste": [
          [
            108.79999999999927,
            "… 1 weitere"
          ]
        ],
        "verschnitt_mm": 108.8,
        "verschnitt_prozent": 1.8,
        "defekte": [],
        "id": 1,
        "verbraucht": true,
        "stueck_code": "AP40-01",
        "lagerort": "Wandregal Alu",
        "ist_rest": false,
        "reste_stuecke": [
          {
            "code": "AP40-01",
            "laenge": 4197.2,
            "breite": 0.0,
            "lagerort": "Restekiste Alu"
          }
        ],
        "reste_ziel": [
          "abfallkiste"
        ]
      },
      "… 1 weitere"
    ],
    "fehlende_teile": [],
    "gesamt_verschnitt_prozent": 2.0,
    "materialbedarf": [],
    "plan_id": 1,
    "projekt_id": 1,
    "material_id": 1,
    "saegeblatt_id": 1,
    "algorithmus": "nested",
    "erstellt": 1793471343.0604548,
    "version": 2,
    "veraltet": false,
    "lager_fehlt": false,
    "fehlende_stuecke": [],
    "fuellteile": [],
    "fuell_alle_reste": false,
    "vorrat_verfuegbar": [],
    "max_pro_stange": 0,
    "fuell_vorschlaege": []
  },
  "teil": {
    "id": 1,
    "projekt_id": 1,
    "label": "Pfosten",
    "typ": "Stange",
    "material_id": 1,
    "laenge": 1800.0,
    "breite": 0.0,
    "stueckzahl": 4,
    "gesaegt_anzahl": 1,
    "maserung": "egal",
    "status": "offen",
    "offen_anzahl": 3,
    "gesamt_anzahl": 4,
    "vorrat": 0,
    "herkunft_teil_id": 0
  }
}

Lager

POST /api/pieces/AP40-02/take Fehler: Stück ist reserviert 409
Anfrage
{
  "grund": "verbraucht"
}
Antwort
{
  "detail": {
    "code": "stueck_reserviert",
    "message": "AP40-02 is reserved by “Werkstattregal”",
    "projekt": "Werkstattregal"
  }
}
POST /api/pieces/AP40-05/take Stück entnehmen 200
Anfrage
{
  "grund": "verkauft",
  "notiz": "an Nachbar"
}
Antwort
{
  "code": "AP40-05",
  "grund": "verkauft",
  "reserviert_fuer": "",
  "abfallkiste": false
}

Abfallkiste

POST /api/waste Abschnitte hineinlegen 201
Anfrage
{
  "material": "AP40",
  "laenge": 180,
  "anzahl": 2
}
Antwort
[
  {
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "kuerzel": "AP40",
    "typ": "Stange",
    "laenge": 180.0,
    "breite": 0.0,
    "anzahl": 2
  }
]
GET /api/waste Inhalt der Abfallkiste 200
Antwort
[
  {
    "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 Einen Abschnitt herausnehmen 200
Anfrage
{
  "material": "AP40",
  "laenge": 180
}
Antwort
[
  {
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "kuerzel": "AP40",
    "typ": "Stange",
    "laenge": 180.0,
    "breite": 0.0,
    "anzahl": 1
  }
]

Journal

GET /api/journal?material=AP40&limit=3 Lagerjournal 200
Antwort
[
  {
    "id": 14,
    "zeit": 1793471343.102995,
    "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": 13,
    "zeit": 1793471343.089545,
    "art": "abfall_ein",
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "code": "",
    "laenge": 180.0,
    "breite": 0.0,
    "anzahl": 2,
    "quelle": "api",
    "notiz": ""
  },
  {
    "id": 12,
    "zeit": 1793471343.084173,
    "art": "entnahme",
    "material_id": 1,
    "material": "Alu Profil 40x40 Nut 8",
    "code": "AP40-05",
    "laenge": 740.0,
    "breite": 0.0,
    "anzahl": 1,
    "quelle": "api",
    "notiz": "verkauft · an Nachbar"
  }
]

System

GET /api/version Version 200
Antwort
{
  "version": "2026.10.70",
  "repo": "orohde/CutStock",
  "github_url": "https://github.com/orohde/CutStock",
  "website_url": "https://worldgate.de/cutstock/",
  "releases_api": "https://api.github.com/repos/orohde/CutStock/releases/latest",
  "docker": false
}