Reference
Every request and response field of the API, the error responses and the limits. The components' API is in
Embedding the components and in cartonization-components.d.ts.
Conventions#
- Units. Any consistent unit: use one length unit for every dimension (for example cm) and one weight unit for every
weight (for example kg). The service never converts. Dimensions must be between 0.01 and 1,000,000; if your values are
smaller, use a smaller unit (for example mm).
- Coordinates. x runs along the container's length, y along its width, z is up (height). The origin is the inner corner of
the container's floor. A placement's
length, width and height are the unit's dimensions in its chosen orientation.
- Ids. 1 to 100 characters, trimmed, unique within the request (per list), and they may not contain
#. Each unit of an
item is named <id>#<n> (n from 1), or just <id> when the item's quantity is 1.
- Optional fields can be left out.
null is not accepted where a value is expected.
POST /api/pack#
Packs items into containers with one, several or all algorithms, and returns the best solution and the ranking.
json{
"containers": [{ "id": "M", "length": 35, "width": 25, "height": 20, "maxWeight": 20, "quantity": 10, "cost": 0.8 }],
"items": [{ "id": "SKU1", "length": 12, "width": 9, "height": 10, "weight": 0.4, "quantity": 4, "rotation": "upright", "stackable": true }],
"options": { "minSupportRatio": 0.7, "timeLimitMs": 2000, "totalTimeLimitMs": 15000, "objective": "fill-rate", "heavyOnBottom": false, "seed": 42 },
"algorithm": "best",
"includeAllSolutions": false
}
| Field |
Type |
Default |
|
containers |
Container[] |
required |
1 to 100 container types |
items |
Item[] |
required |
At least one; at most 2000 units in total (the sum of the quantities) |
options |
Options |
|
|
algorithm |
string or string[] |
"best" |
"best" (or "all") runs every algorithm and returns the best; one algorithm id, or a list of ids, runs only those. An unknown id is a 400 |
includeAllSolutions |
boolean |
false |
Adds solutions: every algorithm's full solution. Send true when the page shows the ranking, so its rows can open the other solutions |
Container#
A box type, pallet type or bin location.
| Field |
Type |
Default |
|
id |
string |
required |
See ids |
length, width, height |
number |
required |
Inner dimensions, 0.01 to 1e6. For a pallet, height is the virtual stack height |
maxWeight |
number |
no limit |
The most the contents may weigh, above 0 |
tareWeight |
number |
0 |
The empty container's weight, added to grossWeight only, at least 0 |
quantity |
integer |
unlimited |
How many of this container exist (box stock is usually unlimited; rack bins are not), at least 1 |
cost |
number |
0 |
The judge under objective: "lowest-cost", a tie-breaker otherwise, at least 0 |
hazmat |
boolean |
false |
A hazmat-certified container. It holds only hazmat items, one hazard class per container |
hazmatClasses |
string[] |
any class |
The hazard classes it is certified for (1 to 20, each 1 to 20 characters). Only with hazmat: true |
maxOverhang |
number |
0 |
How far items may extend past each length/width edge, per side (pallets), 0 to 1e6 |
outerLength, outerWidth, outerHeight |
number |
inner size |
Multi-level only: the size when this container is packed at the next level. At least the inner dimensions |
baseHeight |
number |
|
Multi-level only: the deck height of an open container such as a pallet (see multi-level) |
rotation, stackable, maxLoad |
|
upright, true, no limit |
Multi-level only: how this container is handled when packed at the next level (as for items) |
Item#
| Field |
Type |
Default |
|
id |
string |
required |
See ids |
length, width, height |
number |
required |
0.01 to 1e6 |
weight |
number |
required |
Per unit, above 0 |
quantity |
integer |
1 |
Identical units, at least 1 |
rotation |
"any", "upright", "none" |
"any" |
any: the 6 axis-aligned orientations; upright: the height stays vertical (spins around z only); none: as given |
stackable |
boolean |
true |
false: nothing may be placed on top of it |
maxLoad |
number |
no limit |
Crush limit: the most weight that may rest on it, directly or through the items stacked above it. At least 0 |
hazmatClass |
string |
|
Hazard class, for example "3", 1 to 20 characters. A hazmat item goes only into a hazmat container certified for its class |
Options#
| Field |
Type |
Default |
|
minSupportRatio |
number, 0 to 1 |
0 |
The fraction of an item's base that must rest on the floor or on items below it. 0 for cartons with void fill; about 0.7 for pallets. An item above the floor always needs some support |
timeLimitMs |
integer, 10 to 10000 |
2000 |
The time budget of each metaheuristic (genetic-algorithm, simulated-annealing) |
totalTimeLimitMs |
integer, 100 to 60000 |
15000 |
The budget of a whole multi-algorithm run (see Limits) |
objective |
"fill-rate", "fewest-containers", "lowest-cost", "weight-utilization" |
"fill-rate" |
What "best" means (see Ranking) |
heavyOnBottom |
boolean |
false |
A hard rule: no item may rest on a lighter item |
seed |
integer |
42 |
The seed of the randomized algorithms, for reproducible results |
Algorithms#
GET /api/algorithms lists them with their names and descriptions.
| id |
Method |
extreme-points |
Extreme Points, first-fit decreasing (Crainic, Perboli & Tadei 2008) |
largest-area-fit-first |
LAFF: builds layers led by the item with the largest face (Gürbüz et al.) |
deepest-bottom-left-fill |
DBLF over corner points (Karabulut & İnceoğlu) |
best-fit-corner-points |
Best fit by contact area over corner points |
empty-maximal-spaces |
Empty Maximal Spaces, best fit by least leftover volume (Lai & Chan; Parreño et al.) |
guillotine-best-fit |
Guillotine cuts with best short side fit |
layer-wall-building |
Wall/layer building (George & Robinson; Boxologic) |
genetic-algorithm |
Permutation GA (OX1 crossover) over the item order. It tries 7 packers on the seed orders and searches with whichever packs best |
simulated-annealing |
Simulated annealing over the item order. Each order is packed with the two best packers from the seed phase; the energy follows the objective |
Every algorithm uses the same container selection: several box types, quantity limits, and right-sizing each container down
to the smallest box that still holds its contents.
Ranking and objectives#
The fewest unpacked units always come first: a solution that leaves items behind never wins. Then options.objective decides:
objective |
Primary criterion |
Then |
fill-rate (default) |
Highest overall fill rate (packed item volume ÷ volume of the containers used) |
Fewest containers, lowest cost |
fewest-containers |
Fewest containers |
Fill rate, cost |
lowest-cost |
Lowest total container cost |
Fill rate, containers |
weight-utilization |
Highest packed weight ÷ summed maxWeight of the used containers |
Containers, fill rate, cost |
With the default, fill rate comes before the container count, so the winner can occasionally use one more, smaller box than
another algorithm. The response reports the objective that was used. Under lowest-cost a container without cost counts
as free, so give every container type a cost when you use it.
Constraints#
- Hazmat. An item with
hazmatClass only goes into a container with hazmat: true, and hazmat containers hold only hazmat
items, one hazard class per container. hazmatClasses limits the classes a hazmat container is certified for; leave it out
to accept any class. A hazmat item with no certified container is returned in unpacked. Packed hazmat containers report
their hazmatClass. Classes match regardless of case and spacing ("class 8" = "Class 8") and are reported in upper case.
When hazmat groups share quantity-limited containers, the group with the fewest certified container types is packed first.
- Heavy on bottom (
options.heavyOnBottom). No item may rest on a lighter item.
- Crush limit (
item.maxLoad). The load is passed down through stacks and split by contact area.
- Overhang (
container.maxOverhang). Placements can then have negative x / y (relative to the nominal corner), and the
container's fill rate is measured against the overhang envelope, (length + 2·o) × (width + 2·o) × height.
- Support. Every item above the floor rests on something;
minSupportRatio sets how much of its base. A stackable: false
item has nothing on top of it.
Response#
json{ "best": { ... }, "ranking": [ ... ], "objective": "fill-rate", "skipped": [], "solutions": [ ... ] }
| Field |
Type |
|
best |
Solution |
The winning solution |
ranking |
RankingEntry[] |
Every algorithm that ran, best first |
objective |
string |
The objective used for the ranking |
skipped |
string[] |
Algorithms that didn't run because totalTimeLimitMs was used up |
solutions |
Solution[] |
Only with includeAllSolutions: true: every algorithm's full solution, best first |
license |
{ "trial": true } |
Only while the install runs on a trial license; absent with a production license. The components show a TRIAL mark for it (License) |
Solution#
| Field |
Type |
|
algorithm |
string |
The algorithm id |
containers |
PackedContainer[] |
The containers used, in order |
unpacked |
{ uid, itemId, reason }[] |
Units left out, with the reason. Not an error: the response is still 200 |
metrics |
SolutionMetrics |
|
elapsedMs |
number |
How long the algorithm ran |
PackedContainer#
| Field |
Type |
|
index |
integer |
Its position in the solution, from 1 |
containerId |
string |
The container type's id |
dimensions |
{ length, width, height } |
The container type's inner dimensions |
placements |
Placement[] |
The units in it |
itemVolume |
number |
The packed units' volume |
containerVolume |
number |
The container's volume (the overhang envelope's, when it has maxOverhang) |
fillRate |
number, 0 to 1 |
itemVolume / containerVolume |
netWeight |
number |
The contents' weight |
grossWeight |
number |
netWeight + tareWeight |
weightUtilization |
number or null |
netWeight / maxWeight; null when the container has no maxWeight |
hazmatClass |
string |
Only for hazmat containers: the hazard class of the contents |
Placement#
| Field |
Type |
|
uid |
string |
The unit, <itemId>#<n> (or the item id when its quantity is 1) |
itemId |
string |
The item's id |
x, y, z |
number |
The unit's corner nearest the origin (see coordinates) |
length, width, height |
number |
Its dimensions along x, y and z, in the chosen orientation |
weight |
number |
|
SolutionMetrics#
| Field |
Type |
|
containerCount |
integer |
|
packedUnits, unpackedUnits |
integer |
|
totalItemVolume, totalContainerVolume |
number |
Packed volume, and the volume of the containers used |
fillRate |
number, 0 to 1 |
totalItemVolume / totalContainerVolume |
totalCost |
number |
The sum of the used containers' cost |
weightUtilization |
number or null |
Packed weight ÷ the summed maxWeight of the used containers that have one; null when none has |
RankingEntry#
{ rank, algorithm, metrics, elapsedMs }: rank from 1, metrics as in SolutionMetrics. The ranking has no
placements; send includeAllSolutions: true to get every solution.
Unpacked reasons#
| Reason |
|
Item does not fit in any container type |
Too large in every allowed orientation |
Item exceeds the max weight of every container it fits in |
Too heavy |
No regular (non-hazmat) container available |
Only hazmat containers in the request (or none left) |
No hazmat container certified for hazard class <c> |
|
No container capacity left (container quantity exhausted) |
Every container's quantity is used up |
No hazmat container capacity left for hazard class <c> |
|
POST /api/capacity#
How many of one item fit in one container, and how many containers N units need. For example, how many cartons fit on a pallet:
json{ "container": { "id": "EUR", "length": 120, "width": 80, "height": 160, "maxWeight": 800 },
"item": { "id": "CTN", "length": 40, "width": 30, "height": 30, "weight": 9, "rotation": "upright" },
"demand": 1000 }
| Field |
Type |
Default |
|
container |
Container |
required |
Its quantity is ignored |
item |
Item |
required |
Its quantity is ignored |
demand |
integer, 1 to 2000 |
|
Also packs this many units, and returns how many containers they need |
algorithm, options |
|
"best" |
As for /api/pack |
The response:
| Field |
Type |
|
unitsPerContainer |
integer |
How many fit in one container (40 for the example) |
limitedBy |
"volume", "weight", "geometry", "unit-cap" |
volume / weight: that theoretical bound was reached; geometry: the shapes leave gaps; unit-cap: the search stopped at 2000 units, so the result is a lower bound |
capped |
boolean |
The theoretical bound is above 2000 units |
bounds |
{ volume, weight } |
The theoretical bounds; weight is null without maxWeight |
containersNeeded |
integer or null |
With demand: ceil(demand / unitsPerContainer) (25 for the example) |
solution |
Solution or null |
One full container |
demandSolution |
Solution or null |
With demand: the packing of all the units |
license |
{ "trial": true } |
Only on a trial license, as for POST /api/pack |
POST /api/pack/multi-level#
Packs through 1 to 3 nested levels in one request, for example items into cartons, then cartons onto pallets:
json{
"items": [
{ "id": "MUG", "length": 12, "width": 9, "height": 10, "weight": 0.4, "quantity": 30, "rotation": "upright" },
{ "id": "PAINT", "length": 10, "width": 10, "height": 12, "weight": 1.2, "quantity": 6, "hazmatClass": "3" }
],
"levels": [
{ "name": "Cartons", "algorithm": "best", "options": { "timeLimitMs": 500 },
"containers": [
{ "id": "BOX-M", "length": 30, "width": 25, "height": 20, "maxWeight": 20, "tareWeight": 0.3, "outerLength": 31, "outerWidth": 26, "outerHeight": 21 },
{ "id": "HZ-BOX", "length": 30, "width": 20, "height": 20, "hazmat": true, "hazmatClasses": ["3"] } ] },
{ "name": "Pallets", "options": { "minSupportRatio": 0.7, "heavyOnBottom": true },
"containers": [
{ "id": "EURO", "length": 120, "width": 80, "height": 150, "maxWeight": 1000, "tareWeight": 25, "maxOverhang": 2 },
{ "id": "HZ-PALLET", "length": 120, "width": 80, "height": 150, "hazmat": true } ] }
]
}
items are packed at level 1. Each level has its own containers (1 to 100), options, algorithm (unknown algorithms on
any level return 400) and an optional name (1 to 50 characters).
- Every container packed at level k becomes one item of level k+1, with id
<containerId>@<index>: footprint outerLength
/ outerWidth (default: the inner size + 2 × maxOverhang), height outerHeight (default: the inner height). For pallets,
set baseHeight (the deck height): the loaded pallet is then baseHeight + its actual load height tall at the next level,
not its virtual height. The other fields carried over are the weight (the container's gross weight: contents +
tareWeight), rotation (default upright), stackable (default true), maxLoad and the container's hazmatClass. So a
hazmat carton needs a hazmat pallet certified for its class.
- The response is
{ levels: [{ name, best, ranking, objective, skipped }], summary: [{ name, containerCount, fillRate, unpackedUnits }] }.
On a trial license the response and every level also have license: { trial: true }, so a level passed to the components
on its own still shows the TRIAL mark.
Units left unpacked at a level stay in that level's unpacked; if a level packs nothing, the later levels return an empty solution.
GET /api/algorithms and GET /health#
GET /api/algorithms: [{ id, name, description, kind }], where kind is constructive or metaheuristic.
GET /health: {"status":"ok"}, whatever the license says.
GET /v1/info#
The version and the license of this install, the same for every caller (it never contains the key, nor why a key was
rejected: that is only in the server log). The Status page shows it. Never blocked, also without a license.
json{"name":"cartonization","version":"1.0.0","license":{"status":"valid","trial":false,"source":"configured","allowed":{"cartonization":true},"products":["cartonization"],"licensee":"Acme Corp","id":"LIC-0007","expires":"2027-10-31","daysLeft":375,"maxInstances":2,"instanceId":"cz-3f9a51c0d2e47b8a16c5e0f1"}}
| Field |
Type |
|
license.status |
string |
valid, expired, missing (no key set) or invalid (the key was not accepted) |
license.source |
string |
configured (LICENSE_KEY / LICENSE_FILE), bundled (the evaluation license of the download) or none |
license.trial |
boolean |
An evaluation license: responses are marked and the components show a TRIAL mark |
license.allowed |
object |
{ "cartonization": true } when the packing API works now |
license.products |
string[] |
The products in the license: ["cartonization"] |
license.licensee, license.id |
string |
Who it is licensed to, and the license id to quote to support |
license.expires |
string |
YYYY-MM-DD, valid through the end of that day (UTC). Absent for a trial without expiry |
license.daysLeft |
number |
While valid: the days until it expires, the expiry day included |
license.maxInstances |
number |
The installations the license covers (part of the license terms, not enforced) |
license.instanceId |
string |
This installation's id (CARTONIZATION_INSTANCE_ID, or derived from the hostname) |
Fields that don't apply are absent.
Errors#
| Status |
When |
Body |
What to do |
| 400 |
Invalid input: schema violations, an unknown algorithm, too many units or container types, malformed JSON |
{ "error": "Invalid request", "issues": [...] }, or { "error": "<message>" } |
Fix the request; issues[].path points at the field |
| 402 |
No valid license (POST /api/pack, /api/capacity, /api/pack/multi-level only), checked before the body is read |
{ "error": "<message>", "code": "license_required" | "license_expired" | "product_not_licensed" } |
Install a valid license key (License); retrying doesn't help |
| 503 |
The solver is busy (SOLVER_MAX_CONCURRENT requests are already running) or a request ran past SOLVER_TIMEOUT_MS |
{ "error": "<message>" } |
Retry later with backoff, or lower totalTimeLimitMs |
| 500 |
Unexpected server error |
{ "error": "Internal server error" } |
Report it; the details are in the server log |
Each entry of issues has path (the field, as a list of keys and indexes, for example ["containers", 0, "length"]),
message and code, plus details that depend on the code (for example minimum):
json{"error":"Invalid request","issues":[{"origin":"number","code":"too_small","minimum":0.01,"inclusive":true,"path":["containers",0,"length"],"message":"dimensions must be at least 0.01 (use a smaller unit, e.g. mm)"}]}
- A 503 because the solver is busy carries
Retry-After: 1; a 503 because a request exceeded SOLVER_TIMEOUT_MS has no
Retry-After (retrying the same request won't help). The clients honor the header.
- Units left out of a packing are not an error: the response is 200, and
best.unpacked lists each unit with its reason.
Limits#
- At most 2000 units per request (the sum of the item quantities; for multi-level, the level-1 items), at most
100 container types (per level), and container types × units at most 50,000 (per level, counting the level-1 units).
The last cap bounds the work: the service tries every container type for every container it opens.
- Dimensions between 0.01 and 1e6 in any consistent unit; ids may not contain
#. Hazard classes are 1 to 20 characters;
maxLoad, maxOverhang and baseHeight are at least 0; outer dimensions follow the same 0.01 to 1e6 rule and must be at
least the inner dimensions. Request bodies up to 5 MB.
- Container types: any number of box sizes, pallet types or bin locations, each with an optional
quantity.
- Time:
timeLimitMs (10 to 10000, default 2000) bounds each metaheuristic, which also stops early once it stops improving.
totalTimeLimitMs (100 to 60000, default 15000) is the budget for a whole multi-algorithm run: the constructive algorithms run
first, and the metaheuristics share what is left (each gets at most timeLimitMs). Once it is used up, the remaining
algorithms are skipped (listed in skipped), but at least one always runs. For multi-level requests it applies per level.
A metaheuristic always finishes its first evaluation (one full packing), so on very large instances it can exceed a small
timeLimitMs by about one constructive run.
- Server timeouts: every request is solved in a worker thread, so the server stays responsive. A request still solving after
SOLVER_TIMEOUT_MS (default 200 s, above 3 levels × the 60 s maximum totalTimeLimitMs) returns 503, and so do requests beyond
SOLVER_MAX_CONCURRENT (default: the CPU count) running at the same time.
- Rough timings in "best" mode: typical orders and pallets take under 1 s plus the metaheuristic budget. A 2000-unit pallet
takes a few seconds in total.