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.
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.
Jeder Schlüssel hat einen von zwei Scopes, den Sie beim Erstellen wählen:
read_write, der Standard) ruft jede Route auf.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.
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.
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.
| Methode | Pfad | Wirkung | Erfolg |
|---|---|---|---|
POST | /projects | Ein Project anlegen | 201 |
GET | /projects/{projectId} | Ein Project mit seinen Load-Plan-Köpfen lesen | 200 |
POST | /projects/{projectId}/load-plans | Einen Load Plan anlegen | 201 |
POST | /projects/{projectId}/cargo | Frachtzeilen importieren | 201 |
GET | /projects/{projectId}/cargo | Die Frachtzeilen des Projects auflisten | 200 |
PATCH | /load-plans/{loadPlanId}/equipment | Das Equipment des Load Plans ändern | 200 |
POST | /load-plans/{loadPlanId}/calculate | Eine Berechnung einreihen | 202 |
GET | /calculates/{id} | Einen Berechnungslauf lesen | 200 |
POST | /calculates/{id}/cancel | Einen Berechnungslauf abbrechen | 200 |
GET | /load-plans/{id} | Den gespeicherten Load Plan lesen, mit Placements | 200 |
POST | /load-plans/{loadPlanId}/share-links | Einen rein lesenden Share Link erzeugen | 201 |
GET | /libraries/cargo | Die Cargo Library auflisten | 200 |
GET | /libraries/equipment | Equipment-Typen auflisten, inkl. Sea- und Road-Typen | 200 |
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.GET /libraries/equipment, inklusive der eingebauten Sea- und Road-Typen.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.
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.
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.removeEquipmentIds. Eine Id, die nicht mehr am Plan hängt, wird ignoriert.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.
Eine Berechnung nutzt denselben Optimierer wie Berechnen in der App und läuft im Hintergrund:
GET /load-plans/{id} aufrufen und revision merken.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".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.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.
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.
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.
409 und code: "CONFLICT".400 oder 404), verbraucht den Schlüssel nicht; wiederholen Sie sie mit demselben Schlüssel.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.
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"| Limit | Geltung | Budget |
|---|---|---|
| Anfragen | pro Client-IP, vor der Anmeldung | 300 pro Minute |
| Anfragen | pro API-Schlüssel | 120 pro Minute |
| Berechnungen einreihen | pro API-Schlüssel | 30 pro 10 Minuten |
| Gleichzeitige Berechnungen | pro Organisation | 10 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.
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.
| Status | code | Wann |
|---|---|---|
400 | BAD_REQUEST | Ungü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) |
401 | UNAUTHORIZED | Fehlender, ungültiger, widerrufener oder abgelaufener API-Schlüssel, oder sein Ersteller ist nicht mehr Inhaber oder Admin |
403 | FORBIDDEN | Der Plan der Organisation enthält keinen API-Zugang, oder ein Schlüssel mit Nur-Lesen hat eine schreibende Route aufgerufen (insufficient_scope) |
404 | NOT_FOUND | Die 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 |
409 | CONFLICT | Wiederverwendeter Idempotency-Key, eine bereits laufende Berechnung oder nicht gesperrte manuelle Placements |
409 | PLAN_LIMIT | Ein Tariflimit für Projekte, Berechnungen oder Share Links |
412 | PRECONDITION_FAILED | expectedRevision ist veraltet |
413 | — | Der Body ist größer als 4 MB |
429 | TOO_MANY_REQUESTS | Ein Limit ist erreicht; siehe Retry-After |
503 | SERVICE_UNAVAILABLE | Der 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.
Der Server legt eine API-Audit-Zeile (Schlüssel, Operation, Status, Ressourcen-Id) an für:
Idempotency-Key, ob er gelingt oder scheitert;404 für die Id einer anderen Organisation;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.
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.
/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.
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.