Cargo Marshal
Dashboard
Cargo Marshal
Dashboard
Cargo Marshal Docs
Create your first Load PlanAxle loadsSingle sign-on with OktaSingle sign-on with Microsoft Entra IDSingle sign-on with Google WorkspaceSSO break-glass accessWebhooks
Guides

SSO break-glass access

What happens when your identity provider is down while SSO is required, and how Cargo Marshal support restores access.

Guides

Webhooks

Get a signed POST when a Calculate ends, verify it, and handle retries, so your system does not have to poll.

A webhook is a signed HTTPS POST that Cargo Marshal sends to your server when a Calculate in your organization ends. Use it to start the next step of your workflow as soon as a Load Plan is ready, without polling GET /calculates/{id}.

Before you begin

Webhooks need the Enterprise plan (the same apiAccess as the persistence API) and the Owner or Admin role, the roles that manage API keys. You also need a public HTTPS endpoint that can read the raw request body.

Set up an endpoint

Open Settings → Webhooks to register an endpoint, choose the events it receives, and copy its signing secret. The secret starts with whsec_. It is shown once, when you create the endpoint and when you rotate it, so store it where your server can read it.

From the same page, owners and admins can disable and delete an endpoint, rotate the secret, send a test event, read the delivery log for the last 30 days, and redeliver an event.

Events

An endpoint subscribes to any of three event types. Every Calculate in the organization ends in exactly one of them, whether the API or a Planner in the app started it.

EventFires when
calculate.completedThe Calculate finished and stored its Load Plan.
calculate.failedThe Calculate failed or ran out of time. failureCode says which.
calculate.cancelledThe Calculate was cancelled, for example by a Planner in the app.

A Calculate that times out arrives as calculate.failed with failureCode: "timed_out". There is no separate timeout event.

An endpoint only receives events for Calculates that finished after it was enabled. A new endpoint gets no history.

Most events arrive within seconds of the Calculate ending. An event can occasionally wait for the next hourly sweep.

The request

Cargo Marshal sends a POST with a JSON body. The body is small on purpose. It names the Calculate, Load Plan, and Project, and carries no plan data. Read the plan through the 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
  }
}
FieldMeaning
idThe event id. It is the same on every retry of the event.
typeOne of the three event types above.
createdAtWhen the Calculate finished, as an RFC 3339 UTC timestamp.
data.calculateIdThe Calculate run. Pass it to GET /calculates/{id}.
data.loadPlanIdThe Load Plan the run belongs to. Pass it to GET /load-plans/{id}.
data.projectIdThe Project that holds the Load Plan.
data.revisionFor calculate.completed, the revision the run stored. For the other events, the revision the run started from. Drop an event older than the plan you hold.
data.sourceapi when an API key started the Calculate, app when a Planner did.
data.failureCodenull unless the event is calculate.failed. Then it is the run's failure code, or timed_out.

The OpenAPI document at /api/v1/spec.json lists the same events under webhooks, so you can generate types for the body.

Headers

Requests follow the Standard Webhooks specification.

HeaderValue
webhook-idThe delivery's message id. It is the same on every retry.
webhook-timestampUnix time in seconds.
webhook-signatureOne or more space-separated signatures, each written v1,<base64>.

Verify the signature

Check the signature on every request before you trust it. Anyone who finds your URL can send a POST to it.

With a Standard Webhooks library, pass the raw body and the headers. Do not parse and re-serialize the JSON first, because the signature covers the exact bytes.

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 });
  }

  // Record the event, then answer fast.
  return new Response(null, { status: 204 });
}

The library also rejects a timestamp that is far from the current time, which stops replays of an old request.

To verify without a library:

  1. Build the signed content as <webhook-id>.<webhook-timestamp>.<body>, using the raw body.
  2. Take the part of the secret after whsec_ and base64-decode it. Those bytes are the HMAC key.
  3. Compute HMAC-SHA256 of the signed content with that key, and base64-encode the result.
  4. Compare it, in constant time, with each v1, signature in webhook-signature. The header can hold several, separated by spaces. Accept the request if any one matches.

Rotate the secret

When you rotate a secret, the old one keeps signing for 24 hours. During that time each request carries two v1, signatures, one per secret. Deploy the new secret to your server within that window. Verification keeps working throughout, because a match on either signature is enough.

Delivery guarantees

Delivery is at least once and not ordered. Build the receiver for both.

  • Dedupe on webhook-id. A retry, or a resend after a crash, carries the same webhook-id and the same body. Record the ids you have handled and skip repeats.
  • Drop stale events. Two events for the same Load Plan can arrive out of order. Compare data.revision with the revision you hold and ignore an older one.

Responding

Answer with any 2xx status within 10 seconds. Do slow work after you respond, for example by putting the event on your own queue.

These count as failures:

  • A status outside 2xx.
  • A redirect. Cargo Marshal does not follow one, so register the final URL.
  • No answer within 10 seconds.
  • A connection or TLS error.

Cargo Marshal reads only the first 4 KiB of your response, and the delivery log keeps the first 1 KiB. Return a short body or none.

Retries

A failed delivery retries on this schedule, with about 10 % jitter. Each wait counts from the failure of the previous attempt.

AttemptWait before it
21 minute
35 minutes
430 minutes
52 hours
66 hours
712 hours
824 hours

Eight attempts is the limit. After the last one the delivery stays failed in the delivery log, and you can redeliver it from Settings → Webhooks.

When an endpoint keeps failing

An endpoint that fails for three days is disabled, and Cargo Marshal stops sending to it. Any 2xx response resets the count. The organization's owners and admins get an email when it happens. Fix the receiver, then enable the endpoint again in Settings → Webhooks.

An organization that leaves the Enterprise plan keeps its endpoints, but nothing is delivered until API access returns.

Test and redeliver

Send a test event from the endpoint to check your receiver before a real Calculate runs. The test uses the same signing and delivery path as a real event, with its type set to webhook.test, so it exercises your signature check too. Answer it with a 2xx like any other event.

To send an event again, for example after you fixed a bug, redeliver it from the delivery log. A redelivery keeps the same webhook-id, so your dedupe still applies.

The delivery log shows the last attempt and the attempt count for each delivery. It does not list every attempt.

Network rules

Cargo Marshal checks the endpoint URL when you save it and again on every request:

  • The URL must use https and must not contain a username or password.
  • The port must be 443 or 1024 and above.
  • The host must be a name. An IP address, such as https://203.0.113.7/, and localhost are rejected.
  • The name must not resolve to a private, loopback, link-local, or cloud metadata address. One such address among the results is enough to refuse the request.

To receive webhooks while you develop, expose your local server through a tunnel that gives you a public HTTPS hostname.

Recommended flow

Queue a Calculate through the API, wait for the webhook, and then fetch the Load Plan.

  1. POST /load-plans/{loadPlanId}/calculate with an Idempotency-Key. Keep the run id from the 202 response, then return.
  2. In your receiver, verify the signature and dedupe on webhook-id.
  3. Match data.calculateId to the run you queued. An endpoint also hears about Calculates that Planners start in the app, so ignore the ones you did not queue, or use data.source to tell them apart.
  4. On calculate.completed, call GET /load-plans/{id} with data.loadPlanId for the placements.
  5. On calculate.failed, read data.failureCode. For revision_conflict, the Load Plan changed after you read it. Read it again and queue a new Calculate.

GET /calculates/{id} stays available when you want to check on a run yourself. The persistence API page covers every route.

SSO break-glass access

What happens when your identity provider is down while SSO is required, and how Cargo Marshal support restores access.

On this page

Set up an endpointEventsThe requestHeadersVerify the signatureRotate the secretDelivery guaranteesRespondingRetriesWhen an endpoint keeps failingTest and redeliverNetwork rulesRecommended flow