Create a Project, import cargo, run a Calculate, and read the Load Plan through the API, with Postman or curl.
This guide takes you from an API key to a Load Plan and a Share Link in eight steps. Use it to check that your integration works end to end. The persistence API reference covers every route, field, and error.
Before you begin
API access needs the Enterprise plan and the Owner or Admin role, the roles that manage API keys. Sandbox or test keys do not exist yet. Every request acts on your real organization, so try the flow on a throwaway Project.
Open Settings → API keys and create a key with the Read & write scope. The secret starts with sc_live_ and is shown once. Copy it now.
Postman. Choose Import, then Link, and paste https://<your-app>/api/v1/postman.json. The server builds this collection from the same OpenAPI document as /api/v1/spec.json, so it always matches the API. Open the collection, then Variables, and paste your key into the current value of apiKey. baseUrl is already set. Every request inherits the bearer token. The requests that need an Idempotency-Key fill in a fresh one each time you send them.
curl. Set two variables and send the same requests as the steps below.
export ORIGIN="https://app.example.com"
export CARGO_MARSHAL_API_KEY="sc_live_…"The steps below show curl. In Postman, open the request with the same method and path, fill in the path variables, and send it.
curl -sS -X POST "$ORIGIN/api/v1/projects" \
-H "Authorization: Bearer $CARGO_MARSHAL_API_KEY" \
-H "Idempotency-Key: quickstart-project-1" \
-H "Content-Type: application/json" \
-d '{"name":"Hamburg 40HC","transportMode":"sea"}'The call answers 201 with the Project. Keep its id as PROJECT_ID.
A Project starts without a Load Plan.
curl -sS -X POST "$ORIGIN/api/v1/projects/$PROJECT_ID/load-plans" \
-H "Authorization: Bearer $CARGO_MARSHAL_API_KEY" \
-H "Idempotency-Key: quickstart-plan-1" \
-H "Content-Type: application/json" \
-d '{"name":"Plan A"}'The answer is 201 with id, revision, stale, and an empty equipment list. Keep the id as LOAD_PLAN_ID.
Lengths are millimetres and weights are grams. One request takes up to 5,000 lines and is appended whole or not at all.
curl -sS -X POST "$ORIGIN/api/v1/projects/$PROJECT_ID/cargo" \
-H "Authorization: Bearer $CARGO_MARSHAL_API_KEY" \
-H "Idempotency-Key: quickstart-cargo-1" \
-H "Content-Type: application/json" \
-d '{"lines":[{"name":"Euro pallet","quantity":12,"lengthMm":1200,"widthMm":800,"heightMm":1100,"weightG":250000}]}'The call answers 201 with { "count": 1 }.
Pick Equipment Types from the library. Built-in Sea and Road types are included.
curl -sS "$ORIGIN/api/v1/libraries/equipment?mode=sea&limit=10" \
-H "Authorization: Bearer $CARGO_MARSHAL_API_KEY"Take the id of a type from items, then read the Load Plan's current revision with GET /load-plans/$LOAD_PLAN_ID and send both:
curl -sS -X PATCH "$ORIGIN/api/v1/load-plans/$LOAD_PLAN_ID/equipment" \
-H "Authorization: Bearer $CARGO_MARSHAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expectedRevision":3,"equipmentTypeIds":["cm8x2k4f70001qz0h3m5v6b7n"]}'The answer carries the new revision and the equipment rows. If someone changed the Load Plan in between, the call answers 412 and saves nothing. Read again and retry.
Read the current revision once more, then enqueue. A retry with the same Idempotency-Key returns the first response instead of starting a second run.
curl -sS -X POST "$ORIGIN/api/v1/load-plans/$LOAD_PLAN_ID/calculate" \
-H "Authorization: Bearer $CARGO_MARSHAL_API_KEY" \
-H "Idempotency-Key: quickstart-calculate-1" \
-H "Content-Type: application/json" \
-d '{"expectedRevision":4}'The call answers 202:
{ "id": "cm9x2k", "status": "queued", "loadPlanId": "cm9w7p" }Keep id as CALCULATE_ID.
Poll the run until status is no longer queued or running:
curl -sS "$ORIGIN/api/v1/calculates/$CALCULATE_ID" \
-H "Authorization: Bearer $CARGO_MARSHAL_API_KEY"The run ends as completed, failed, cancelled, or timed_out. While it runs, phase, processedItems, and totalItems show progress. A failed run carries a failureCode. Poll every few seconds, not in a tight loop, because rate limits apply.
To skip polling, register a webhook endpoint and wait for calculate.completed. The webhooks guide covers setup and signature checks. The event names the Load Plan, so you read the plan in the next step.
curl -sS "$ORIGIN/api/v1/load-plans/$LOAD_PLAN_ID" \
-H "Authorization: Bearer $CARGO_MARSHAL_API_KEY"When resultStatus is solved, result holds units, placements, totals, warnings, and unloaded. Cargo that did not fit is listed in unloaded with a reason. Check warnings, and axleWarnings and stopWarnings when present, before you act on the plan.
curl -sS -X POST "$ORIGIN/api/v1/load-plans/$LOAD_PLAN_ID/share-links" \
-H "Authorization: Bearer $CARGO_MARSHAL_API_KEY" \
-H "Idempotency-Key: quickstart-share-1" \
-H "Content-Type: application/json" \
-d '{"expiresAt":"2026-12-31T23:59:59Z"}'The answer carries a view-only url. The url and token appear only on this response, so store them. A replay of the same request returns null for both.
Every error has the same JSON body. Branch on code, and on data.reason where it is set, never on message. Keep the x-request-id response header for support.
Retry a 429 after the number of seconds in Retry-After. Retry a 503 with backoff. A 412 means the Load Plan changed, so read its revision again. Budgets, error codes, and idempotency rules are in the reference.
/api/v1/spec.json, or browse and try every route in the reference at /api/v1.