---
title: API quickstart
description: 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](/overview/persistence-api) 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.

## Create an API key

1. ### Create the key

    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.

2. ### Choose your client

    **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.

    ```bash
    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.

## Run the flow

1. ### Create a Project

    ```bash
    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`.

2. ### Create a Load Plan

    A Project starts without a Load Plan.

    ```bash
    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`.

3. ### Import cargo

    Lengths are millimetres and weights are grams. One request takes up to 5,000 lines and is appended whole or not at all.

    ```bash
    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 }`.

4. ### Set the equipment

    Pick Equipment Types from the library. Built-in Sea and Road types are included.

    ```bash
    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:

    ```bash
    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.

5. ### Queue a Calculate

    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.

    ```bash
    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`:

    ```json
    { "id": "cm9x2k", "status": "queued", "loadPlanId": "cm9w7p" }
    ```

    Keep `id` as `CALCULATE_ID`.

6. ### Wait for the result

    Poll the run until `status` is no longer `queued` or `running`:

    ```bash
    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](/overview/persistence-api#rate-limits) apply.

    To skip polling, register a webhook endpoint and wait for `calculate.completed`. The [webhooks guide](/guides/webhooks) covers setup and signature checks. The event names the Load Plan, so you read the plan in the next step.

7. ### Read the Load Plan

    ```bash
    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.

8. ### Create a Share Link

    ```bash
    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.

## Handle errors and limits

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](/overview/persistence-api#rate-limits).

## What's next

- Generate a typed client from `/api/v1/spec.json`, or browse and try every route in the reference at `/api/v1`.
- Receive Calculate results by [webhook](/guides/webhooks) instead of polling.
- Read the [persistence API reference](/overview/persistence-api) for scopes, key expiry, pagination, and versioning.
