Authentication
Get access & a key
To call the /v2 API you need an account and an API key. Sign up in the web app, then create a key there. The application's own API keys documentation has the current steps. The key is scoped to your account (not to an individual person) and is what every /v2 request authenticates with.
The API key
Send the key on every request as the X-Api-Key header:
curl https://api.loadoptimizer.ai/v2/jobs/01975cba-67b9-7b7c-a2f1-3d29e4a01c44 \
-H "X-Api-Key: ck_live_..."
| Fact | Detail |
|---|---|
| Header | X-Api-Key, with a key that starts with ck_live_ |
| Where you create it | In the web app. See its API keys documentation for where. The full key is shown once, immediately after creation; after that only a hint remains (ck_live_… plus the last 4 characters). Creating and revoking keys is available on every plan, so you can build and wire up your integration first. Until your plan includes API access, calls made with the key are refused with the 403 below |
| Missing, invalid or revoked | 401, with an empty body |
| Valid, but the plan has no API access | 403 problem+json with code: "customer_api_not_entitled". Its detail names where to upgrade |
| Revoking | Takes effect immediately on the instance that handles the next request; other instances catch up within minutes |
| Rotating | Create a new key, then revoke the old one. If your plan only allows one active key at a time, you must revoke first, then create: creating while already at the limit is rejected |
| Scope of a read | Every job read is account-scoped. A job id that belongs to another account returns 404 job_not_found, never 403 |
Key management itself (creating, listing and revoking keys) is a web-app action, not an API call: it lives behind the portal's own sign-in, and a customer API key cannot invoke it.
Errors
| Status | Meaning |
|---|---|
401 |
Missing, invalid or revoked key. Empty body. |
403 |
The key is valid, but the account's plan doesn't include the customer API. code: "customer_api_not_entitled", with the upgrade link in detail. |
404 |
Unknown job id, or a job that belongs to a different account. The two look identical: job_not_found. |
See Errors & troubleshooting for the full code registry.
Account limits
Your plan carries limits on job size and on throughput. Three of them are per-job ceilings on how big a single request may be, and each counts something different. Get this wrong and a job you thought was well inside your plan is turned down:
| Per-job ceiling | What it counts |
|---|---|
| Cargo units per job | Total cargo units: every line's amount, summed. One line with amount: 500 counts as 500, not as 1. |
| Carrier entries per job | The number of entries in carriers[]: how many distinct carriers you offer, regardless of how many instances of each you allow. |
| Carrier instances per job | The quantity values in carriers[], summed. A carrier that omits quantity (or sends null) has no fixed count to add, so it counts as one instance, even though the solver may go on to use several. Omitting quantity is therefore the cheapest way to offer a carrier against this ceiling. |
The other two are about throughput, not job size: one caps how many of your jobs may be queued or running at the same time, the other is a monthly job quota, of which you have one per problem type.
Four of the five resolve per problem type. The three per-job ceilings above and the monthly quota are each looked up for the problemType the job declares, so one and the same request can sit inside your plan as a LoadPlanning job and outside it as a MixedPalletizing one. The monthly quotas are genuinely separate pots: mixed palletizing meters its own, so load plans never use it up and it never uses up theirs. Only the concurrent-job cap is account-wide, since a job that is queued or running occupies the same capacity whichever problem it poses. Mixed palletizing has one further condition: your plan has to include it in the first place. If it does not, the submit is refused with 403 and code: "mixed_palletizing_not_entitled" before any ceiling is consulted. See Anatomy of a job for the field itself.
Separate from all five, and the one an integration meets first, is a request rate limit: 300 requests per minute per account across every /v2 route. It counts requests, not jobs, so a tight poll loop can reach it while your job counts sit comfortably inside the plan. Polling every couple of seconds, as Async & validate suggests, stays far below it.
What happens when you hit a ceiling. A single job that asks for too many cargo units, carrier entries or carrier instances is turned down with 422; that response has no machine-readable code, so the reason is in its detail text only. Throughput is 429, with the code saying which limit you hit: "too_many_active_jobs" when too many of your jobs are queued or running at once, "monthly_quota_exceeded" when the monthly quota for that job's problem type is used up, and "rate_limit_exceeded" when you exceeded the request rate. All three are retryable. Only rate_limit_exceeded tells you how long to wait, in a Retry-After header; for the other two, back off on a schedule of your own choosing. See Errors & troubleshooting.