Cargo Marshal
Dashboard
Cargo Marshal
Dashboard
Cargo Marshal Docs
EinführungPersistenz-API
Überblick

Einführung

Was Cargo Marshal ist, und für wen.

Überblick

Persistenz-API

Ein Project anlegen, Fracht importieren, eine Berechnung starten und den gespeicherten Load Plan lesen.

Die Persistenz-API ist REST unter /api/v1 auf dem Origin der App, zum Beispiel https://app.example.com/api/v1. Das OpenAPI-3.1-Dokument liegt unter /api/v1/spec.json, und /api/v1 selbst zeigt die Scalar-Referenz, in der Sie jede Route ausprobieren können. Das ist nicht der Cookie-Baum /rpc und nicht die Admin-Referenz unter /api-reference.

Jede Route, jedes Feld und jeder Fehlercode unten wird aus einem Vertrag erzeugt, daher entspricht die Spezifikation unter /api/v1/spec.json immer dem Verhalten des Servers. Erzeugen Sie Ihren Client daraus, statt Strukturen von Hand abzuschreiben.

Authentifizierung

Senden Sie bei jeder Anfrage einen API-Schlüssel der Organisation als Bearer-Token:

Authorization: Bearer sc_live_…

Das Geheimnis wird in der App unter API-Schlüssel einmal angezeigt, und gespeichert wird nur ein Hash. Inhaber und Admins können ihn rotieren oder widerrufen. Ein widerrufener oder rotierter Schlüssel funktioniert sofort nicht mehr. Ein Schlüssel handelt nur für seine Organisation: Ids einer anderen Organisation antworten mit 404, als gäbe es sie nicht.

Scopes

Jeder Schlüssel hat einen von zwei Scopes, den Sie beim Erstellen wählen:

  • Lesen und schreiben (read_write, der Standard) ruft jede Route auf.
  • Nur lesen (read) ruft nur die GET-Routen auf: ein Project, seine Fracht, einen Ladeplan oder eine Berechnung lesen und die Bibliotheken auflisten. Jede andere Route antwortet mit 403, code: "FORBIDDEN" und data.reason: "insufficient_scope", bevor etwas geändert wird.

Schlüssel, die vor den Scopes erstellt wurden, haben Lese- und Schreibzugriff. Beim Rotieren behält ein Schlüssel seinen Scope.

Ablauf

Ein Schlüssel läuft nie ab, außer Sie legen beim Erstellen einen Ablauf fest: in 30 Tagen, 90 Tagen oder einem Jahr (die API erlaubt höchstens zwei Jahre im Voraus). Ab diesem Zeitpunkt antwortet der Schlüssel wie ein widerrufener mit 401. Rotieren Sie einen Schlüssel mit Ablauf, erhält der neue Schlüssel eine neue Laufzeit gleicher Länge; so macht eine Rotation auch einen abgelaufenen Schlüssel wieder nutzbar.

Von wem der Schlüssel abhängt

Ein Schlüssel bleibt nur gültig, solange das Mitglied, das ihn erstellt hat, noch in der Organisation ist und noch Inhaber oder Admin ist, also eine Rolle hat, die API-Schlüssel verwalten darf. Verlässt dieses Mitglied die Organisation, wird es entfernt oder zu Mitglied oder Betrachter herabgestuft, antwortet jeder Schlüssel, den es erstellt hat, mit 401. Der Schlüssel wird nicht auf Nur-Lesen herabgestuft, sondern gesperrt. Ein Inhaber oder Admin erstellt dann einen neuen Schlüssel.

Ein fehlender, fehlerhafter, widerrufener, abgelaufener oder unbekannter Schlüssel, oder einer, dessen Ersteller die Bedingung nicht mehr erfüllt, antwortet mit 401, code: "UNAUTHORIZED" und dem Header WWW-Authenticate: Bearer realm="api". Die Antwort ist in jedem Fall gleich und verrät nicht, welche Prüfung fehlschlug; der Grund wird serverseitig im API-Audit-Protokoll festgehalten (siehe Audit-Protokoll).

API-Zugang gibt es nur mit Enterprise (apiAccess). Andere Pläne erhalten 403 mit code: "FORBIDDEN". Die Pläne, die heute verkauft werden, setzen dieses Flag nicht, bis der Enterprise-Katalog live ist.

Die API ist für Server-zu-Server-Aufrufe gedacht. Behalten Sie den Schlüssel auf Ihrem Server und rufen Sie die API nicht aus einem Browser auf. Cross-Origin-Anfragen aus dem Browser anderer Websites sind nicht erlaubt, und /api/v1 sendet und akzeptiert nie ein Cookie.

Was ein Schlüssel kann

MethodePfadWirkungErfolg
POST/projectsEin Project anlegen201
GET/projects/{projectId}Ein Project mit seinen Load-Plan-Köpfen lesen200
POST/projects/{projectId}/load-plansEinen Load Plan anlegen201
POST/projects/{projectId}/cargoFrachtzeilen importieren201
GET/projects/{projectId}/cargoDie Frachtzeilen des Projects auflisten200
PATCH/load-plans/{loadPlanId}/equipmentDas Equipment des Load Plans ändern200
POST/load-plans/{loadPlanId}/calculateEine Berechnung einreihen202
GET/calculates/{id}Einen Berechnungslauf lesen200
POST/calculates/{id}/cancelEinen Berechnungslauf abbrechen200
GET/load-plans/{id}Den gespeicherten Load Plan lesen, mit Placements200
POST/load-plans/{loadPlanId}/share-linksEinen rein lesenden Share Link erzeugen201
GET/libraries/cargoDie Cargo Library auflisten200
GET/libraries/equipmentEquipment-Typen auflisten, inkl. Sea- und Road-Typen200
  • Frachtzeilen liegen schon in Millimetern und Gramm vor. Eine Zeile kann einen groupKey tragen: Zeilen mit demselben Schlüssel sind eine Gruppe, die als ein Block in eine Unit geladen oder als Ganzes mit dem Grund group-does-not-fit nicht verladen wird. Ein Import von bis zu 5.000 Zeilen ist eine Transaktion: Er wird ganz oder gar nicht angehängt.
  • Equipment-Ids stammen aus GET /libraries/equipment, inklusive der eingebauten Sea- und Road-Typen.

Projects und Fracht lesen

GET /projects/{projectId} liefert die Metadaten des Projects, cargoLineCount und je Load Plan einen Kopf, der älteste zuerst: id, name, revision, stale, solvedAt (null vor der ersten Berechnung), createdAt und updatedAt. Fracht, Equipment und Placements sind nicht enthalten; Equipment und gespeichertes Ergebnis eines Load Plans liest GET /load-plans/{id}.

GET /projects/{projectId}/cargo liefert die Frachtzeilen des Projects seitenweise in Grid-Reihenfolge, also in der Reihenfolge, in der sie importiert oder in der App angeordnet wurden. Jeder Eintrag hat jedes Feld, das eine Importzeile nimmt, mit ausgefüllten Standardwerten (rotation: "free", stackable: true, fragile: false) und fehlenden Werten als null, dazu id, revision, sourceLibraryItemId (gesetzt, wenn die Zeile aus der Cargo Library stammt), createdAt und updatedAt. Um Fracht in ein anderes Project zu kopieren, lassen Sie diese zusätzlichen Felder weg und senden den Rest an POST /projects/{projectId}/cargo.

Einheiten und Formate

Längen sind ganze Millimeter (lengthMm, innerWidthMm, …), Gewichte ganze Gramm (weightG, maxPayloadG, …). Zeitpunkte sind RFC-3339-Strings in UTC, Kalenderdaten YYYY-MM-DD. Request-Bodies sind JSON mit Content-Type: application/json und höchstens 4 MB groß; ein größerer Body antwortet mit 413. Unbekannte Schlüssel im Request-Body ergeben 400. Antworten können neue Felder bekommen; ignorieren Sie Felder, die Sie nicht kennen.

Equipment

PATCH /load-plans/{loadPlanId}/equipment führt eine Änderung mit dem Equipment des Load Plans zusammen:

  • equipmentTypeIds ist die vollständige Menge der gewählten Equipment-Typen aus der Bibliothek. Ein weggelassener Typ wird abgewählt.
  • maxUnitsByEquipmentTypeId setzt optional die Höchstzahl an Units pro gewähltem Typ (null heißt ohne Grenze). Ein gewählter Typ, den Sie weglassen, behält seine Grenze.
  • Equipment ohne Bibliothekstyp (importiert, oder dessen Typ gelöscht wurde) bleibt, außer seine Id steht in removeEquipmentIds. Eine Id, die nicht mehr am Plan hängt, wird ignoriert.
  • Gewählte Typen und verbleibendes Equipment dürfen zusammen 20 Zeilen nicht überschreiten.

Senden Sie expectedRevision mit der revision Ihres letzten Lesens. Hat sich der Load Plan inzwischen geändert, wird nichts gespeichert, und der Aufruf antwortet mit 412 und code: "PRECONDITION_FAILED"; data.conflicts nennt die Ressource mit expectedRevision und currentRevision. Lesen Sie erneut und wiederholen Sie den Aufruf. Die Antwort enthält die neue revision für Ihren nächsten Schreibaufruf.

Berechnung

Eine Berechnung nutzt denselben Optimierer wie Berechnen in der App und läuft im Hintergrund:

  1. GET /load-plans/{id} aufrufen und revision merken.
  2. POST /load-plans/{loadPlanId}/calculate mit {"expectedRevision": <revision>} senden. Gesperrte Placements bleiben immer erhalten. Hat jemand Stücke von Hand verschoben, ohne sie zu sperren, senden Sie "replaceUnlockedManual": true, damit der Lauf sie ersetzen darf; ohne das Flag antwortet der Aufruf mit 409, statt die manuelle Arbeit zu überschreiben. Ein gesperrtes Placement über dem Boden muss auf anderen gesperrten Placements stehen: Sonst antwortet der Aufruf mit 400 und einer Meldung, die das betroffene Stück nennt, weil der Solver nie ein freies Stück unter ein gesperrtes schiebt. Sperren dürfen auch keine Gruppe aufbrechen: Eine nur teilweise gesperrte Gruppe, gesperrte Stücke einer Gruppe in zwei Units oder eine Sperre im Block einer anderen Gruppe ergeben ebenfalls 400 mit der Gruppe oder den betroffenen Stücken. Die App sperrt und entsperrt eine Gruppe immer als Ganzes. Die Antwort ist 202 mit der Lauf-id und status: "queued".
  3. GET /calculates/{id} abfragen. status wechselt über queued und running (mit phase, processedItems und totalItems) zu completed, cancelled, failed oder timed_out. Ein fehlgeschlagener Lauf liefert einen failureCode, zum Beispiel revision_conflict, wenn sich der Load Plan nach Ihrem Lesen geändert hat.
  4. Ist der Lauf completed, lesen Sie GET /load-plans/{id} erneut für die Placements. Der Lauf selbst meldet nur die neue revision und solvedAt.

Im gespeicherten Ergebnis listet warnings Regelverstöße, nicht geladene Ladung und den Schwerpunkt-Hinweis. Achslast-Hinweise für Straßen-Equipment mit Achsprofil (axle-overload, axle-lift, steer-axle-underload, drive-axle-underload, gross-overload) stehen gesondert in axleWarnings; das Feld fehlt, wenn es keine gibt.

POST /calculates/{id}/cancel stoppt eine wartende oder laufende Berechnung. Ein abgebrochener Lauf schreibt keine Placements.

Pro Load Plan läuft eine Berechnung zur Zeit: Ein zweites Einreihen, während eine wartet oder läuft, antwortet mit 409. Eine Organisation darf 10 Berechnungen gleichzeitig wartend oder laufend haben; die elfte antwortet mit 429.

Eine Berechnung unterstützt höchstens 10.000 Cargo Lines, insgesamt 20.000 Stücke und 500 Units. Ladung über einer dieser Grenzen wird mit 400 abgelehnt, bevor ein Lauf eingereiht wird; die Meldung nennt die Grenze und Ihren Wert. Die Unit-Prüfung nutzt eine untere Schranke (Gewicht, Volumen und Bodenfläche über dem größten angebotenen Laderaum) und lehnt daher nur Ladung ab, die sicher mehr als 500 Units braucht.

Share Links

POST /load-plans/{loadPlanId}/share-links erzeugt einen Share Link, der wie jeder Share Link nur das Ansehen erlaubt. Der Body ist optional; senden Sie {"expiresAt": "2026-12-31T23:59:59Z"} für einen Link, der abläuft, oder lassen Sie ihn weg für einen, der nie abläuft. Hat der Load Plan noch kein gespeichertes Ergebnis, antwortet der Aufruf mit 400 und data.reason load_plan_not_calculated. Starten Sie zuerst eine Berechnung. Mit showLoadingSteps: false blenden Sie die schrittweise Beladung für Besucher aus; der Standard ist true. Das ist eine Darstellungsoption: Schrittseiten, Schrittbilder und die Spalte mit dem Ladeschritt entfallen, aber die Platzierungsliste im Bericht und das CSV behalten die Ladereihenfolge, und die Plandaten des Links enthalten weiter das vollständige Ergebnis.

token und url stehen nur in der Antwort, die den Link erzeugt hat: Gespeichert wird nur ein Hash des Tokens. Bewahren Sie beides auf, oder erzeugen Sie einen neuen Link, wenn es verloren ist.

Idempotenz

POST /projects, POST /projects/{projectId}/load-plans, POST /projects/{projectId}/cargo, POST /load-plans/{loadPlanId}/calculate und POST /load-plans/{loadPlanId}/share-links verlangen den Header Idempotency-Key: 1 bis 255 sichtbare ASCII-Zeichen ohne Leerzeichen (eine UUID passt). Ohne ihn antworten sie mit 400.

  • Eine Wiederholung mit demselben Schlüssel und demselben Body innerhalb von 24 Stunden liefert die erste Antwort mit demselben Status, statt ein zweites Project, einen zweiten Import, eine zweite Berechnung oder einen zweiten Share Link zu schreiben.
  • Derselbe Schlüssel mit einem anderen Body, oder während die erste Anfrage noch läuft, antwortet mit 409 und code: "CONFLICT".
  • Eine Anfrage, die fehlschlug, bevor etwas geschrieben wurde (zum Beispiel 400 oder 404), verbraucht den Schlüssel nicht; wiederholen Sie sie mit demselben Schlüssel.
  • Schlüssel gelten pro API-Schlüssel. Nach dem Rotieren beginnt der neue API-Schlüssel ohne gespeicherte Idempotency-Keys.
  • Ein wiederholter Share Link hat token und url auf null, weil das Token nie gespeichert wird.

PATCH für Equipment und POST /calculates/{id}/cancel nehmen keinen Idempotency-Key: Das erste schützt expectedRevision, und zweimal abbrechen wirkt wie einmal abbrechen.

Seitenweises Lesen

GET /libraries/cargo, GET /libraries/equipment und GET /projects/{projectId}/cargo liefern eine Seite: { "items": [...], "nextCursor": "…" }. Übergeben Sie nextCursor als cursor für die nächste Seite; auf der letzten Seite ist er null. Ein cursor, der kein Eintrag der Liste ist, antwortet mit 404. Bei den Bibliotheken setzt limit die Seitengröße, 1 bis 100 (Standard 20), und query filtert nach einem Teilstring, ohne auf Groß- und Kleinschreibung zu achten. Bei Frachtzeilen ist limit 1 bis 500 (Standard 100), und totalCount zählt alle Zeilen des Projects.

Die Equipment-Liste filtert außerdem nach mode (sea oder road) und nach ids, das auch für eine einzelne Id die Klammer-Schreibweise verlangt:

curl -sS "$ORIGIN/api/v1/libraries/equipment?ids[]=eqt_a&ids[]=eqt_b" \
  -H "Authorization: Bearer $CARGO_MARSHAL_API_KEY"

Limits

LimitGeltungBudget
Anfragenpro Client-IP, vor der Anmeldung300 pro Minute
Anfragenpro API-Schlüssel120 pro Minute
Berechnungen einreihenpro API-Schlüssel30 pro 10 Minuten
Gleichzeitige Berechnungenpro Organisation10 wartend oder laufend

Eine Anfrage über einem Limit antwortet mit 429, code: "TOO_MANY_REQUESTS" und einem Retry-After-Header in Sekunden (auch in data.retryAfter, wenn das Limit ihn kennt). Warten Sie so lange, bevor Sie es erneut versuchen. Eine wiederholte Berechnung (derselbe Idempotency-Key) verbraucht das Berechnungsbudget nicht.

Die Limits pro Schlüssel fallen geschlossen aus: Ist der Limiter in Produktion nicht erreichbar, antwortet der Aufruf mit 503, statt durchgelassen zu werden. Wiederholen Sie 503 mit Backoff.

Fehler

Jeder Fehler antwortet mit demselben JSON-Body:

{
  "defined": true,
  "code": "BAD_REQUEST",
  "status": 400,
  "message": "Input validation failed",
  "data": {
    "reason": "invalid_input",
    "issues": [
      {
        "path": ["body", "lines", 0, "lengthMm"],
        "message": "Too small: expected number to be >=1",
        "code": "too_small"
      }
    ]
  }
}

Entscheiden Sie anhand von code und, wo gesetzt, data.reason; message ist englischer Text für Menschen und kann sich ändern. defined: true heißt, der Fehler ist einer, den die Spezifikation für diese Route dokumentiert, mit data in der dokumentierten Form; defined: false ist ein unerwarteter Fehler, meist ein 500.

StatuscodeWann
400BAD_REQUESTUngültige Eingabe (data.reason invalid_input, data.issues listet höchstens 50 fehlgeschlagene Prüfungen), Ladung, die der Solver nicht annimmt, oder ein Share Link auf einem nicht berechneten Load Plan (load_plan_not_calculated)
401UNAUTHORIZEDFehlender, ungültiger, widerrufener oder abgelaufener API-Schlüssel, oder sein Ersteller ist nicht mehr Inhaber oder Admin
403FORBIDDENDer Plan der Organisation enthält keinen API-Zugang, oder ein Schlüssel mit Nur-Lesen hat eine schreibende Route aufgerufen (insufficient_scope)
404NOT_FOUNDDie Id gibt es in der Organisation des Schlüssels nicht, oder der Pfad ist keine Route
405—Den Pfad gibt es für andere Methoden; der Header Allow nennt sie
409CONFLICTWiederverwendeter Idempotency-Key, eine bereits laufende Berechnung oder nicht gesperrte manuelle Placements
409PLAN_LIMITEin Tariflimit für Projekte, Berechnungen oder Share Links
412PRECONDITION_FAILEDexpectedRevision ist veraltet
413—Der Body ist größer als 4 MB
429TOO_MANY_REQUESTSEin Limit ist erreicht; siehe Retry-After
503SERVICE_UNAVAILABLEDer Limiter oder ein anderer Dienst ist nicht erreichbar; mit Backoff wiederholen

Die Spezifikation nennt pro Route genau, welche dieser Codes sie liefern kann.

Ein Tariflimit antwortet mit 409, code: "PLAN_LIMIT" und data, das das Limit nennt:

{
  "reason": "plan-limit",
  "resource": "solves",
  "limit": 10,
  "planName": "Free"
}

resource ist projects, solves oder share-links.

Jede Antwort trägt einen Header x-request-id. Nennen Sie ihn, wenn Sie den Support kontaktieren; Sie können auch eine eigene x-request-id senden, die dann übernommen wird.

Audit-Protokoll

Der Server legt eine API-Audit-Zeile (Schlüssel, Operation, Status, Ressourcen-Id) an für:

  • jeden schreibenden Aufruf, auch eine Wiederholung mit demselben Idempotency-Key, ob er gelingt oder scheitert;
  • jeden lesenden Aufruf, der in der Operation scheitert, zum Beispiel ein 404 für die Id einer anderen Organisation;
  • jede Ablehnung eines bekannten Schlüssels: Rate-Limit (429), Nur-Lesen-Scope (403), Plan ohne API-Zugang (403) sowie ein abgelaufener Schlüssel oder einer, dessen Ersteller die Bedingung nicht mehr erfüllt (401).

Erfolgreiche Lesezugriffe (GET-Routen mit 200) werden nicht festgehalten: Sie ändern nichts, und Integrationen fragen sie regelmäßig ab. Ebenfalls nicht festgehalten werden Anfragen, die am Eingabeschema scheitern (400 mit invalid_input), unbekannte oder widerrufene Schlüssel und Anfragen, die das Limit pro IP ablehnt.

Was ein Schlüssel nicht kann

Ein Schlüssel kann keine Placements und keinen Solve-Snapshot senden: Der Body der Berechnung nimmt nur die erwartete Revision und das Flag für manuelle Placements, und Placements werden immer auf dem Server berechnet. Admin, Abrechnung, Chat, Anmeldung und Dateispeicher bleiben außerhalb dieser API.

Versionierung und Abkündigung

/api/v1 ist stabil. Innerhalb von v1 gibt es nur additive Änderungen: neue Routen, neue optionale Eingabefelder, neue Antwortfelder, neue Werte für code oder data.reason bei einem bestehenden Status. Bauen Sie Clients so, dass sie unbekannte Antwortfelder ignorieren.

Eine inkompatible Änderung (eine Route oder ein Feld entfernen oder umbenennen, eine optionale Eingabe zur Pflicht machen, einen Typ oder einen Erfolgsstatus ändern) erscheint unter einer neuen Version, /api/v2, und v1 funktioniert daneben weiter. Jede Änderung der Spezifikation wird vor der Auslieferung auf inkompatible Änderungen geprüft.

Bevor eine Route oder ein Feld entfernt wird, ist sie in der Spezifikation mit deprecated: true markiert. Jede Antwort einer abgekündigten Route, ob erfolgreich oder nicht, trägt:

  • Deprecation: true oder das Datum der Abkündigung in der Form @<Unix-Sekunden> nach RFC 9745, zum Beispiel Deprecation: @1790812800.
  • Sunset: das Datum der Entfernung als HTTP-Datum (RFC 8594), zum Beispiel Sunset: Thu, 01 Apr 2027 00:00:00 GMT, sobald es feststeht.
  • Link: <…>; rel="deprecation": die Seite, die die Umstellung erklärt, sobald sie veröffentlicht ist.

Das Sunset-Datum liegt mindestens 6 Monate nach der Ankündigung. Protokollieren Sie diese Header in Ihrem Client, damit eine Abkündigung nicht unbemerkt bleibt. Heute ist keine v1-Route abgekündigt.

Beispiel

curl -sS -X POST "$ORIGIN/api/v1/projects" \
  -H "Authorization: Bearer $CARGO_MARSHAL_API_KEY" \
  -H "Idempotency-Key: project-4401" \
  -H "Content-Type: application/json" \
  -d '{"name":"Hamburg 40HC"}'

Ersetzen Sie $ORIGIN durch die App-URL. Der Aufruf antwortet mit 201 und dem neuen Project.

Einführung

Was Cargo Marshal ist, und für wen.

Auf dieser Seite

AuthentifizierungScopesAblaufVon wem der Schlüssel abhängtWas ein Schlüssel kannProjects und Fracht lesenEinheiten und FormateEquipmentBerechnungShare LinksIdempotenzSeitenweises LesenLimitsFehlerAudit-ProtokollWas ein Schlüssel nicht kannVersionierung und AbkündigungBeispiel