Anatomy of a job
Every guide builds part of a job request. This page is the whole shape in one place: what the top-level keys are, which are required, and where each is explained in depth.
Top-level structure
A POST /v2/jobs body (JobRequest) has exactly six top-level keys:
| Key | Required | What it is | Guide |
|---|---|---|---|
carriers |
✅ | The carriers available for this job. Supply side. | Core concepts |
cargo |
✅ | The cargo to load (the lines/entries). Demand side. | Quickstart |
units |
No | Per-kind units for the job (default m/kg). | Units |
configuration |
No | The primary objective, the two supported secondary switches, and the group-separation activation. | below |
name |
No | Optional caller-supplied label, <= 256 chars, echoed back on the status and result responses. No effect on packing. null is accepted and has the same effect as omitting the key: no name is stored. |
below |
problemType |
No | Which problem this job poses: "LoadPlanning" (default) or "MixedPalletizing". The only field that selects it. |
below |
carriers and cargo are the only required keys; both must be non-empty.
Sizes and weights have an upper bound. Every dimension must be at most 100 m and every weight at most 100000 kg, measured after conversion from the units you declared. The accepted number therefore depends on your units block: a 100 m limit is 100000 in mm, 100 in m and 328.08 in ft. Both bounds sit an order of magnitude above any trailer, container or air pallet, so the usual reason to meet one is a value sent in the wrong unit: 2400 meaning millimetres while units.length says m. A value past the bound is rejected with 400 against the field you sent, for example cargo[0].geometry.dimensionX.
A carrier or cargo entry
Every entry in carriers[] / cargo[] is defined inline: there's nothing else to reference and no override list to learn. This is the full request shape; every field below is verified against the box-packing subset this API actually implements:
{
"units": { "length": "m", "weight": "kg" }, // optional, these are the defaults
"carriers": [
{
"externalId": "EUR-PALLET", // required, non-empty, <= 128 (duplicates allowed)
"name": "Euro pallet", // optional label, <= 256 chars
"geometry": { "type": "box", "dimensionX": 1.2, "dimensionY": 0.8, "dimensionZ": 1.5 },
"quantity": null, // null = unlimited instances; else > 0
"tareWeight": 25.0, // optional, >= 0, in units.weight - the empty carrier
"category": "pallet" // optional, exactly "pallet" | "container"; omitted -> filled in from problemType, NOT the problem-type selector
}
],
"cargo": [
{
"externalId": "SKU-123", // OPTIONAL echo label, <= 128, NOT unique
"description": "Box A", // optional, <= 512 chars
"color": "#ffffff", // optional free-form string, <= 64 chars
"amount": 2, // required, >= 1 - number of identical units
"weight": 5.0, // >= 0 - per single unit, in units.weight; required, so send it even when it is 0
"geometry": { "type": "box", "dimensionX": 0.3, "dimensionY": 0.4, "dimensionZ": 0.2 },
"constraints": { // optional
"allowStackingOnTop": true, // default false
"allowedRotations": ["Z"] // "X" | "Y" | "Z"; omitted -> ["Z"]; [] -> no rotation
},
"category": "box", // optional, non-empty <= 64 when present - a display hint, NOT the problem-type selector
"groups": ["fragile", "route-a"] // optional labels; a label only moves cargo if the job declares it in "groups", below
}
],
"configuration": { // optional
"objective": "MinimizeCarriers", // only this primary objective is accepted
"secondaryObjectives": [ // optional; at most one entry per type
{ "type": "CenterLoadMass" }, // centre the load's mass
{ "type": "consolidation", "direction": "-X" } // packing axis: "-X" | "-Y" | "Z"
],
"groupSeparation": { "mode": "segmented" } // optional; omit for none. See below
},
"groups": { // optional; the groups this job declares
"route-a": { "packingOrder": 1 }, // 1 packs nearest the x = 0 end
"route-b": { "packingOrder": 2 }
},
"name": "Rotterdam morning run", // optional label, <= 256 chars, trimmed; PUT /jobs/{id}/name to change or clear
"problemType": "LoadPlanning" // optional, "LoadPlanning" (default) | "MixedPalletizing", case-insensitive - the only field that selects the problem type
}
This is the canonical body the other guides build on. The field rules:
| Field | Rule |
|---|---|
geometry.type |
Must be exactly "box" (case-sensitive). Anything else → 400. |
geometry.dimensionX/Y/Z |
All three required, > 0. X = length, Y = width, Z = height. |
carriers[].externalId |
Effectively required and non-empty (<= 128). Duplicates across carriers are allowed. |
carriers[].quantity |
null or omitted = unlimited: the solver adds instances until the cargo runs out or the carrier fits nothing more, so only the carriers it actually used come back. An explicit value must be > 0 and is used in full: all N instances appear in the result's carriers[], so any instance past the point where the cargo ran out comes back with an empty placements[]. summary.carriersUsed counts only carriers with at least one placement, so it can be lower than the length of carriers[]: check a carrier's placements[] before you draw it. |
carriers[].tareWeight |
Optional, >= 0, in units.weight, the carrier's own empty weight, stored with the job. What the result reports per carrier is kpis.loadedWeight: the weight of the placements in it. |
cargo[].externalId |
Optional. null, "" and duplicates are all valid. Echo label, never a join key. |
cargo[].amount |
Line quantity: amount: 5 yields up to 5 placements that all reference the same line. |
cargo[].groups |
Free labels, any number of them. A label moves cargo only if the job also declares it in the top-level groups block and sets configuration.groupSeparation; see Separating groups along the carrier. A line may name at most one declared group. Comes back as group (singular) on placements and unplaced items either way. |
constraints.allowedRotations |
Axes the box may turn 90° about (case-insensitive). Omitted → ["Z"]. [] → locked in the submitted orientation. Unknown tokens → 400. |
configuration.objective |
Omitted, null or "" → MinimizeCarriers. Any other value → 400. |
configuration.secondaryObjectives[].type |
"CenterLoadMass" or "consolidation" (case-insensitive). At most one entry per type: two consolidation entries → 400, no last-one-wins. A null entry → 400. |
configuration.secondaryObjectives[].direction |
Required for consolidation: "-X", "-Y" or "Z". "+X"/"+Y" → 400. Alongside "CenterLoadMass" it's accepted and ignored. |
configuration.tolerance |
Not supported: sending it at all → 400. It is rejected outright rather than being silently ignored, so you never get a plan that quietly disregarded a tolerance you asked for. See Optimization objectives. |
problemType |
Optional, case-insensitive: "LoadPlanning" (the default when omitted) or "MixedPalletizing". This is the only field that selects the problem type; a carrier's category is a display hint and plays no part in it. MixedPalletizing needs the feature on your plan and draws on its own monthly quota; without it, submitting one gets 403. |
Worth calling out explicitly: rotation is controlled only through allowedRotations: there is no separate boolean tilt switch; cargo has no name (use description); carriers have name but no description; and the job itself has an optional name too, distinct from both: see below.
A mixed palletizing job
The body above plans a load. Building mixed pallets needs no other schema: the same six keys, with problemType declaring the problem, and the rest of the difference is in what you are asking for rather than in what you send. This is the job the runnable clients in Quickstart submit:
{
"units": { "length": "m", "weight": "kg" },
"problemType": "MixedPalletizing", // the one field that selects the problem
"name": "Store 104 mixed pallets",
"carriers": [
{
"externalId": "EUR-PALLET",
"name": "EUR pallet, 1.8 m build height",
"category": "pallet", // the only value this job accepts; may be omitted
"geometry": { "type": "box", "dimensionX": 1.2, "dimensionY": 0.8, "dimensionZ": 1.8 },
"quantity": null, // unlimited: the solver decides how many pallets
"tareWeight": 25
}
],
"cargo": [
{
"externalId": "SKU-4001", "description": "Beverage crate",
"amount": 24, "weight": 12.5,
"geometry": { "type": "box", "dimensionX": 0.4, "dimensionY": 0.3, "dimensionZ": 0.25 },
"constraints": { "allowStackingOnTop": true, "allowedRotations": ["Z"] },
"groups": ["store-104"]
},
{
"externalId": "SKU-4002", "description": "Cereal case",
"amount": 18, "weight": 4.0,
"geometry": { "type": "box", "dimensionX": 0.6, "dimensionY": 0.4, "dimensionZ": 0.3 },
"constraints": { "allowStackingOnTop": true },
"groups": ["store-104"]
},
{
"externalId": "SKU-4003", "description": "Egg tray",
"amount": 6, "weight": 2.2,
"geometry": { "type": "box", "dimensionX": 0.3, "dimensionY": 0.3, "dimensionZ": 0.15 },
"constraints": { "allowStackingOnTop": false, "allowedRotations": [] },
"groups": ["store-104"]
}
],
"configuration": {
"objective": "MinimizeCarriers",
"secondaryObjectives": [ { "type": "CenterLoadMass" } ]
}
}
Five choices in that body are what make it a palletizing job rather than a load plan:
| Choice | Why it is there |
|---|---|
problemType: "MixedPalletizing" |
The only field that selects the problem class. Remove it and this exact body is a valid load-planning job, which is also the cheapest way to compare the two: submit it twice, once with the field and once without. |
category: "pallet" |
The only value a MixedPalletizing job accepts on a carrier; container is rejected with 400. Omitting it fills in pallet from problemType and echoes it back, so writing it out documents the intent without changing the outcome. See Building load units. |
quantity: null on the pallet |
Unlimited, so the solver adds pallets until the cargo runs out. That is what turns the answer into how many pallets does this order need. An explicit N answers the narrower question of whether the order fits N pallets, and returns all N instances including any that ended up empty. |
Three cargo lines |
The mix is the point. One line describes a single-SKU pallet, which this same body can express but which is not the problem worth declaring. |
Per-line constraints |
allowStackingOnTop defaults to false, so the two sturdy lines opt in and the trays do not. That, plus allowedRotations: [] on the trays, is what settles the build order. See Constraints. |
Two properties of such a job are not visible in the body at all. It needs the feature on your plan: without it the submit is refused with 403 and code: "mixed_palletizing_not_entitled", before any size limit is consulted. And it meters its own monthly quota, separate from the load-planning one, as do the three per-job ceilings; one problem type running out says nothing about the other. See Account limits and the code registry.
Nothing else about the job changes: it queues, polls, fails and exports exactly as a load-planning job does, and GET /jobs/{id} echoes the problemType back. The field is case-insensitive on input and the echo is always the canonical spelling, so "mixedpalletizing" goes in and "MixedPalletizing" comes back.
The configuration block
Three things live here, and all three are optional:
objective: the only primary objective this API accepts isMinimizeCarriers, which packs all the cargo using the fewest carriers. Leave it out, or sendnull/"", and it defaults to the same thing. See Optimization objectives.secondaryObjectives: not ordered tie-breakers here, but two independent switches.CenterLoadMasscentres the load's mass once the solve is done.consolidationpicks which axis the solver packs along from the first step. The solver always packs along an axis (the default is-X), so leavingconsolidationout is not "no consolidation", just the default axis. List order has no effect, and each type may appear at most once.groupSeparation: turns the job's declared groups into real sections of the carrier, so each group loads and unloads as one block. It works together with the top-levelgroupsblock; both are covered in Separating groups along the carrier.
Naming a job
name is a free-text label for the job itself, separate from carriers[].name and unrelated to cargo[].description. Set it at submit time, or afterward with PUT /jobs/{id}/name, which can also change or clear it (there is no separate DELETE). It has no effect on the packing; it's simply echoed back on GET /jobs/{id} and GET /jobs/{id}/result. Leading and trailing whitespace is trimmed, and a value that is only whitespace is stored as no name at all: sending " ", sending null, and omitting the key from the PUT body all have the same effect.
Coordinate frame
X is longitudinal (length), Y is lateral (width), Z is vertical (up), fixed for every carrier. Each carrier has its own frame whose origin corner is x = 0, y = 0, z = 0: z = 0 is the floor, and the default -X consolidation packs cargo toward x = 0. Everything is in units.length, and rotations are always in degrees. The same frame and the same diagram apply here as in Coordinate frame; for what a placement's x/y/z, sizeX/Y/Z and orientationR* mean, how to plot one, and a worked example, see Reading a placement in Core concepts.
Once you've got this shape, jump to the API Reference for every field.