Erhalten Sie einen signierten POST, wenn eine Berechnung endet, prüfen Sie ihn und gehen Sie mit Wiederholungen um, damit Ihr System nicht abfragen muss.
Ein Webhook ist ein signierter HTTPS-POST, den Cargo Marshal an Ihren Server sendet, wenn eine Berechnung in Ihrer Organisation endet. Nutzen Sie ihn, um den nächsten Schritt Ihres Ablaufs zu starten, sobald ein Load Plan fertig ist, ohne GET /calculates/{id} abzufragen.
Bevor Sie beginnen
Webhooks brauchen den Enterprise-Plan (dasselbe apiAccess wie die
Persistenz-API) und die Rolle Owner oder
Admin, die Rollen, die API-Schlüssel verwalten. Außerdem brauchen Sie einen
öffentlichen HTTPS-Endpunkt, der den rohen Request-Body lesen kann.
Öffnen Sie Einstellungen → Webhooks, um einen Endpunkt zu registrieren, die Ereignisse zu wählen, die er erhält, und sein Signaturgeheimnis zu kopieren. Das Geheimnis beginnt mit whsec_. Es wird einmal angezeigt, beim Erstellen des Endpunkts und beim Rotieren. Speichern Sie es dort, wo Ihr Server es lesen kann.
Auf derselben Seite können Inhaber und Admins einen Endpunkt deaktivieren und löschen, das Geheimnis rotieren, ein Testereignis senden, das Zustellprotokoll der letzten 30 Tage lesen und ein Ereignis erneut zustellen.
Ein Endpunkt abonniert beliebige der drei Ereignistypen. Jede Berechnung in der Organisation endet in genau einem davon, egal ob die API oder ein Planner in der App sie gestartet hat.
| Ereignis | Wird ausgelöst, wenn |
|---|---|
calculate.completed | Die Berechnung fertig war und ihren Load Plan gespeichert hat. |
calculate.failed | Die Berechnung fehlgeschlagen ist oder die Zeit überschritten hat. failureCode sagt, was zutrifft. |
calculate.cancelled | Die Berechnung abgebrochen wurde, zum Beispiel von einem Planner in der App. |
Eine Berechnung mit Zeitüberschreitung kommt als calculate.failed mit failureCode: "timed_out" an. Ein eigenes Timeout-Ereignis gibt es nicht.
Ein Endpunkt erhält nur Ereignisse für Berechnungen, die nach seiner Aktivierung endeten. Ein neuer Endpunkt bekommt keine Vorgeschichte.
Die meisten Ereignisse kommen innerhalb von Sekunden nach dem Ende der Berechnung an. Gelegentlich wartet ein Ereignis auf den nächsten stündlichen Durchlauf.
Cargo Marshal sendet einen POST mit JSON-Body. Der Body ist mit Absicht klein. Er nennt Berechnung, Load Plan und Project und enthält keine Plandaten. Lesen Sie den Plan über die API.
{
"id": "evt_cm9x2k_completed",
"type": "calculate.completed",
"createdAt": "2026-10-11T09:00:00.000Z",
"data": {
"calculateId": "cm9x2k",
"loadPlanId": "cm9w7p",
"projectId": "cm9w1a",
"revision": 7,
"source": "api",
"failureCode": null
}
}| Feld | Bedeutung |
|---|---|
id | Die Ereignis-Id. Sie ist bei jeder Wiederholung des Ereignisses gleich. |
type | Einer der drei Ereignistypen oben. |
createdAt | Wann die Berechnung endete, als RFC-3339-Zeitstempel in UTC. |
data.calculateId | Der Berechnungslauf. Übergeben Sie ihn an GET /calculates/{id}. |
data.loadPlanId | Der Load Plan, zu dem der Lauf gehört. Übergeben Sie ihn an GET /load-plans/{id}. |
data.projectId | Das Project, das den Load Plan enthält. |
data.revision | Bei calculate.completed die revision, die der Lauf gespeichert hat. Bei den anderen Ereignissen die revision, von der der Lauf ausging. Verwerfen Sie ein Ereignis, das älter ist als der Plan, den Sie halten. |
data.source | api, wenn ein API-Schlüssel die Berechnung gestartet hat, app, wenn ein Planner es tat. |
data.failureCode | null, außer das Ereignis ist calculate.failed. Dann ist es der Fehlercode des Laufs oder timed_out. |
Das OpenAPI-Dokument unter /api/v1/spec.json listet dieselben Ereignisse unter webhooks, sodass Sie Typen für den Body erzeugen können.
Anfragen folgen der Spezifikation Standard Webhooks.
| Header | Wert |
|---|---|
webhook-id | Die Nachrichten-Id der Zustellung. Sie ist bei jeder Wiederholung gleich. |
webhook-timestamp | Unixzeit in Sekunden. |
webhook-signature | Eine oder mehrere durch Leerzeichen getrennte Signaturen, je als v1,<base64>. |
Prüfen Sie bei jeder Anfrage die Signatur, bevor Sie ihr vertrauen. Wer Ihre URL findet, kann einen POST dorthin senden.
Mit einer Standard-Webhooks-Bibliothek übergeben Sie den rohen Body und die Header. Parsen Sie das JSON nicht vorher und serialisieren Sie es nicht neu, denn die Signatur deckt genau die gesendeten Bytes ab.
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.CARGO_MARSHAL_WEBHOOK_SECRET!); // whsec_…
export async function POST(request: Request) {
const body = await request.text();
let event;
try {
event = wh.verify(body, Object.fromEntries(request.headers)) as {
id: string;
type: string;
data: { calculateId: string; loadPlanId: string; revision: number };
};
} catch {
return new Response("invalid signature", { status: 400 });
}
// Ereignis festhalten, dann schnell antworten.
return new Response(null, { status: 204 });
}Die Bibliothek lehnt außerdem einen Zeitstempel ab, der weit von der aktuellen Zeit abweicht. Das verhindert die Wiedergabe einer alten Anfrage.
So prüfen Sie ohne Bibliothek:
<webhook-id>.<webhook-timestamp>.<body> mit dem rohen Body.whsec_ und dekodieren Sie ihn aus Base64. Diese Bytes sind der HMAC-Schlüssel.v1,-Signatur in webhook-signature. Der Header kann mehrere enthalten, durch Leerzeichen getrennt. Akzeptieren Sie die Anfrage, wenn eine davon passt.Wenn Sie ein Geheimnis rotieren, signiert das alte weitere 24 Stunden. In dieser Zeit trägt jede Anfrage zwei v1,-Signaturen, eine je Geheimnis. Spielen Sie das neue Geheimnis innerhalb dieses Fensters auf Ihrem Server ein. Die Prüfung funktioniert die ganze Zeit, weil ein Treffer bei einer der beiden Signaturen genügt.
Die Zustellung erfolgt mindestens einmal und ohne feste Reihenfolge. Bauen Sie den Empfänger für beides.
webhook-id. Eine Wiederholung oder ein erneutes Senden nach einem Absturz trägt dieselbe webhook-id und denselben Body. Merken Sie sich die verarbeiteten Ids und überspringen Sie Wiederholungen.data.revision mit der revision, die Sie halten, und ignorieren Sie eine ältere.Antworten Sie innerhalb von 10 Sekunden mit einem beliebigen 2xx-Status. Erledigen Sie langsame Arbeit nach der Antwort, zum Beispiel indem Sie das Ereignis in Ihre eigene Warteschlange legen.
Als Fehler zählen:
2xx.Cargo Marshal liest von Ihrer Antwort nur die ersten 4 KiB, und das Zustellprotokoll speichert die ersten 1 KiB. Senden Sie einen kurzen Body oder keinen.
Eine fehlgeschlagene Zustellung wird nach diesem Plan wiederholt, mit etwa 10 % Jitter. Jede Wartezeit zählt ab dem Fehlschlag des vorherigen Versuchs.
| Versuch | Wartezeit davor |
|---|---|
| 2 | 1 Minute |
| 3 | 5 Minuten |
| 4 | 30 Minuten |
| 5 | 2 Stunden |
| 6 | 6 Stunden |
| 7 | 12 Stunden |
| 8 | 24 Stunden |
Acht Versuche sind das Limit. Nach dem letzten bleibt die Zustellung im Zustellprotokoll als fehlgeschlagen stehen, und Sie können sie unter Einstellungen → Webhooks erneut zustellen.
Ein Endpunkt, der drei Tage lang fehlschlägt, wird deaktiviert, und Cargo Marshal sendet nichts mehr an ihn. Jede 2xx-Antwort setzt die Zählung zurück. Die Inhaber und Admins der Organisation erhalten dann eine E-Mail. Beheben Sie den Fehler im Empfänger und aktivieren Sie den Endpunkt unter Einstellungen → Webhooks wieder.
Eine Organisation, die den Enterprise-Plan verlässt, behält ihre Endpunkte, aber es wird nichts zugestellt, bis der API-Zugriff zurückkehrt.
Senden Sie vom Endpunkt aus ein Testereignis, um Ihren Empfänger zu prüfen, bevor eine echte Berechnung läuft. Der Test nutzt denselben Signatur- und Zustellweg wie ein echtes Ereignis, mit type gleich webhook.test, und prüft damit auch Ihre Signaturprüfung. Beantworten Sie ihn wie jedes andere Ereignis mit 2xx.
Um ein Ereignis erneut zu senden, zum Beispiel nach einer Fehlerbehebung, stellen Sie es im Zustellprotokoll erneut zu. Eine erneute Zustellung behält dieselbe webhook-id, Ihre Entdoppelung greift also weiter.
Das Zustellprotokoll zeigt je Zustellung den letzten Versuch und die Zahl der Versuche. Es listet nicht jeden Versuch.
Cargo Marshal prüft die Endpunkt-URL beim Speichern und bei jeder Anfrage erneut:
https nutzen und darf weder Benutzernamen noch Passwort enthalten.https://203.0.113.7/, und localhost werden abgelehnt.Um Webhooks während der Entwicklung zu empfangen, machen Sie Ihren lokalen Server über einen Tunnel mit öffentlichem HTTPS-Hostnamen erreichbar.
Reihen Sie eine Berechnung über die API ein, warten Sie auf den Webhook und lesen Sie dann den Load Plan.
POST /load-plans/{loadPlanId}/calculate mit einem Idempotency-Key. Merken Sie sich die Lauf-id aus der 202-Antwort und kehren Sie zurück.webhook-id.data.calculateId dem Lauf zu, den Sie eingereiht haben. Ein Endpunkt hört auch von Berechnungen, die Planner in der App starten. Ignorieren Sie also die, die Sie nicht eingereiht haben, oder unterscheiden Sie sie über data.source.calculate.completed GET /load-plans/{id} mit data.loadPlanId für die Placements auf.calculate.failed den data.failureCode. Bei revision_conflict hat sich der Load Plan nach Ihrem Lesen geändert. Lesen Sie ihn erneut und reihen Sie eine neue Berechnung ein.GET /calculates/{id} bleibt verfügbar, wenn Sie einen Lauf selbst prüfen wollen. Die Seite Persistenz-API behandelt jede Route.