API reference
Every endpoint, request and response field, error code and limit of the API. How the bins are chosen is explained in
How slotting decides; the components' API is in Embedding the components and in
slotting-components.d.ts.
Conventions#
- JSON in and out,
Content-Type: application/json. Request bodies up to 25 MB. - Units. Lengths are in the layout's
unitand weights in itsweightUnit, also in wave requests. The service never converts. Dimensions are 0.01 to 1,000,000. - Ids. Layout and wave ids: 1 to 100 of
A-Z a-z 0-9 . _ : -, starting with a letter or digit. Wall, bin and order ids and SKUs: 1 to 100 characters after trimming, without control characters and without#. Encode ids in URL paths. - Wave ids are unique on the install, across layouts, so
/api/waves/{waveId}finds a wave without its layout. - Optional fields can be left out.
nullis not accepted where a value is expected. - Trial license. Responses of successful changes have a top-level
"license": { "trial": true }on an evaluation license (License). - Never blocked by the license: every
GET,/healthz,/v1/info,/docs/,/widget/,/and CORS preflights.
| Method | Path | |
|---|---|---|
GET |
/api/layouts |
List the layouts |
POST |
/api/layouts |
Create a layout |
GET, PUT, DELETE |
/api/layouts/{layoutId} |
Read, replace, delete a layout |
PATCH |
/api/layouts/{layoutId}/walls/{wallId} |
Move a wall |
GET |
/api/layouts/{layoutId}/bins |
Bin occupancy |
GET |
/api/layouts/{layoutId}/lights |
Light state |
POST |
/api/waves |
Create a wave |
GET |
/api/waves |
List the waves |
GET, DELETE |
/api/waves/{waveId} |
Read, delete a wave |
POST |
/api/waves/{waveId}/scan |
Scan |
POST |
/api/waves/{waveId}/confirm |
Confirm a put |
GET |
/api/waves/{waveId}/pending |
Puts awaiting confirmation |
POST |
/api/waves/{waveId}/undo |
Undo a scan |
GET |
/api/waves/{waveId}/orders/{orderId} |
One order |
POST |
/api/waves/{waveId}/orders/{orderId}/complete |
Complete an order short |
POST |
/api/waves/{waveId}/release |
Release an order's bins |
POST |
/api/waves/{waveId}/close |
Close a wave |
POST |
/api/lights/press |
Light button press |
GET |
/api/webhooks/dead-letters |
Undeliverable webhook events |
POST |
/api/plan |
Plan without storing |
POST |
/api/import/dxf, /csv, /grid, /photo |
Import a bin setup |
GET |
/healthz |
Health |
GET |
/v1/info |
Version, license, photo import |
Layouts#
GET /api/layouts#
The layouts of the install: [{ "id": "PUTWALL-A", "name": "Put wall A", "walls": 1, "bins": 6 }].
POST /api/layouts#
Creates a layout. The body is a Layout: walls with bins (id, x, y, width, height,
depth, and optional maxWeight, maxUnits, disabled and lightAddress). Answers 201 with the saved layout (ids trimmed).
409 duplicate_id if the id exists; 422 invalid_layout with issues if the layout is not valid (overlapping bins,
duplicate ids, limits).
GET, PUT, DELETE /api/layouts/{layoutId}#
GET: the layout.PUT: replaces it; the body is the whole layout (itsidmay be left out; if given, it must match the path). Walls and bins may be added; removing, shrinking or disabling a bin that an active wave holds answers409 layout_in_use.DELETE: deletes it and answers{ "id": "PUTWALL-A", "deleted": true }.409 layout_in_usewhile a wave of the layout is not closed (complete waves still hold bins: close them first).
PATCH /api/layouts/{layoutId}/walls/{wallId}#
Moves a wall (typically a mobile one): { "position": { "x": 1200, "y": 300 }, "rotation": 90 }, either field optional.
Allowed while waves run; it changes only the choice of bins from then on. Answers the updated wall.
GET /api/layouts/{layoutId}/bins#
The occupancy of every bin: an array of BinState.
| Field | Type | |
|---|---|---|
id, wallId |
string | The bin |
waveId, orderId |
string | The wave and order holding the bin (reserved or filled). Absent: the bin is free |
units |
number | Units in the bin |
fill |
number | Used share of the usable volume (0 to 1), from items with dimensions |
weight |
number | Weight in the bin |
GET /api/layouts/{layoutId}/lights#
The desired put-to-light state of the layout, whatever SLOTTING_LIGHTS is: an array of
LightState, in layout order. Bins that are not listed are off.
| Field | Type | |
|---|---|---|
binId, wallId |
string | The bin |
address |
string | The bin's lightAddress, else its id |
state |
string | put (put here) or complete (the order in this bin is complete: pack out) |
waveId, orderId |
string | The wave and order the light is for |
quantity, sku |
number, string | For put: what to put |
Waves#
POST /api/waves#
Creates and plans a wave. Answers 201 with { "wave": WaveState, "plan": Plan }.
{
"id": "WAVE-0815",
"layoutId": "PUTWALL-A",
"wallIds": ["A"],
"orders": [
{ "id": "ORD-1001", "priority": 1, "lines": [{ "sku": "MUG", "quantity": 2 }, { "sku": "POSTER", "quantity": 1, "item": { "weight": 0.2 } }] }
],
"items": { "MUG": { "length": 12, "width": 9, "height": 10, "weight": 0.4 } },
"options": { "allocation": "complete-first", "maxBinsPerOrder": 8, "fillFactor": 0.85, "wallChangePenalty": 100 }
}| Field | Type | Default | |
|---|---|---|---|
id |
string | required | The wave id, unique on the install. 409 duplicate_id if it exists |
layoutId |
string | required | |
wallIds |
string[] | every wall | Plan only into these walls (for example the carts docked at a station). 404 unknown_wall for an unknown id |
orders |
Order[] | required | |
items |
object | The item master: SKU → ItemDims. Used for lines without item |
|
options |
WaveOptions |
Order: id (unique in the wave), lines (at least one), priority (number, default 0; higher goes first).
Line: sku, quantity (a whole number, 1 to 1,000,000), item (ItemDims, overrides items[sku] for this line).
Several lines of one SKU in an order are added up (the first line's item applies).
ItemDims: length, width, height, weight, each optional. An item without all three dimensions has no volume
and skips the opening check (why that matters).
WaveOptions:
| Field | Default | |
|---|---|---|
allocation |
complete-first |
Which order gets a scanned unit: complete-first or fifo (details) |
maxBinsPerOrder |
8 | The most bins one order may use (1 to 64) |
fillFactor |
the layout's, else 0.85 | Usable share of a bin's volume, 0.1 to 1 |
wallChangePenalty |
1 m in the layout's unit | Extra distance counted when a cluster spans two walls |
compactness |
strict |
strict: an order waits for a compact group of bins rather than take scattered ones; relaxed: the best group the free bins allow (details) |
confirmPuts |
false |
Every put must be confirmed by a bin scan or a light button before it counts as done (details) |
Plan:
| Field | |
|---|---|
orders[] |
Per order, in planning sequence: orderId, status (planned or queued), bins (nearest first), puts (the expected puts: binId, wallId, sku, quantity), unplaceable (SKU → units that fit no bin of the walls) |
stats |
orders, planned, queued, binsUsed, unplaceableUnits |
WaveState (also GET /api/waves/{waveId}):
| Field | |
|---|---|
id, layoutId |
|
status |
active, complete (every order complete or released) or closed |
createdAt |
ISO time |
orders[] |
OrderState |
stats |
orders, ordersComplete, ordersQueued, unitsTotal, unitsPut, binsUsed |
OrderState:
| Field | |
|---|---|
id |
|
status |
queued (no bins yet), planned (bins reserved, nothing put), in_progress, complete, released (packed out, bins freed) |
bins |
The bins reserved for or used by the order (id, wallId), nearest first |
unitsTotal, unitsPut |
Units to put and put so far. Unplaceable units are not counted |
remaining |
SKU → units still to put (only SKUs with units left; unplaceable units are not included) |
short |
true when completed short |
unplaceable |
SKU → units that fit no bin of the wave's walls |
GET /api/waves#
The waves of the install, or of one layout with ?layoutId=PUTWALL-A:
{ "waves": [{ "id", "layoutId", "status", "createdAt", "closedAt", "stats" }] } (closedAt only for closed waves).
GET, DELETE /api/waves/{waveId}#
GET: the WaveState.?layoutId=is needed only if two layouts have a wave with this id.DELETE: deletes a closed wave and answers{ "id": "WAVE-0815", "deleted": true }. A wave that is not closed answers409 wave_active: close it first.
Every /api/waves/{waveId}... path takes ?layoutId= too; it is needed only when two layouts have a wave with this id.
Scanning#
POST /api/waves/{waveId}/scan#
One scan: which bin the unit goes into.
| Field | Type | Default | |
|---|---|---|---|
sku |
string | required | The scanned SKU |
quantity |
integer | 1 | Units in this scan. They go to one order, possibly over several of its bins |
orderId |
string | the allocation policy | Put for this order |
scanId |
string | generated | Your idempotency key: repeating a scanId returns the first answer and changes nothing. Without one the install numbers the scans <waveId>-<n> |
operator |
string | Free text kept with the scan (station, operator) |
ScanResult:
| Field | |
|---|---|
scanId, waveId, sku |
|
quantity |
Units actually put (less than requested when the order needed fewer) |
orderId |
The order the units went to |
bin |
Where to put them (id, wallId): the first entry of puts |
puts |
Every bin used by this scan, with binId, wallId, sku, quantity |
order |
The order's OrderState after the scan |
orderComplete |
true when this scan put the order's last unit |
orderBins |
When orderComplete: every bin holding the order's units, nearest first |
waveComplete |
true when every order of the wave is complete or released |
pending |
true when the wave confirms puts: this put waits for its confirmation, and orderComplete is false even for the last unit. Absent otherwise |
Put-aside answers: 422 sku_not_in_wave, 409 sku_fully_slotted, 409 no_bin_available (with details.orderId, the order
that needed the unit), 422 item_too_large. A closed wave answers 409 wave_closed.
POST /api/waves/{waveId}/confirm#
Confirms a pending put (waves with confirmPuts): the operator scanned the bin's label after putting the unit in.
| Field | Type | Default | |
|---|---|---|---|
binId |
string | required | The scanned bin |
scanId |
string | the oldest pending put into binId |
The scan being confirmed |
ConfirmResult:
| Field | |
|---|---|
scanId, waveId, orderId |
The confirmed scan and its order |
bin |
The confirmed bin (id, wallId) |
order |
The order's OrderState after the confirmation |
orderComplete |
true when this confirmed the order's last pending put |
orderBins |
When orderComplete: every bin holding the order's units, nearest first |
waveComplete |
true when every order of the wave is complete or released |
A put split over several bins needs one confirmation per bin. 409 wrong_bin when the scan's units went to other bins
(details.expected: those bins, id and wallId; nothing changes), 409 not_pending when nothing waits for a
confirmation there (or the scan has nothing pending any more), 404 unknown_scan for an unknown scanId, 409 wave_closed for a closed wave.
GET /api/waves/{waveId}/pending#
The puts of the wave still waiting for their confirmation, oldest scan first (an empty array for a wave without
confirmPuts):
| Field | |
|---|---|
scanId, waveId, orderId, sku |
The pending scan |
quantity |
The units of the whole scan |
operator |
The scan's operator, if it had one |
at |
When it was scanned (ISO time) |
bins |
The bins still to confirm, in the order of the scan's puts: id, wallId, quantity (the units that went into that bin) |
result |
The scan's ScanResult, as first answered |
POST /api/waves/{waveId}/undo#
{ "scanId": "..." }: takes that scan's units back out (for a pending put: cancels it). Answers the WaveState. 404 unknown_scan for an unknown or already
undone scan, 409 order_released when the scan's order was already released. After an undo, the same scanId is a new scan.
GET /api/waves/{waveId}/orders/{orderId}#
The order's OrderState.
POST /api/waves/{waveId}/orders/{orderId}/complete#
Completes the order short (units missing): short: true, and its reserved bins without units are freed. Answers the
OrderState.
POST /api/waves/{waveId}/release#
{ "orderId": "ORD-1001" } or { "binId": "A-01-01" }: the order was packed out, its bins are free. With binId, the
order holding that bin is released. Answers { "orderIds": [...], "bins": [{ "id", "wallId" }] }. Only complete orders:
409 order_not_complete otherwise, and 409 order_released for an order that was already released.
POST /api/waves/{waveId}/close#
Closes the wave: every bin it holds is freed, further scans answer 409 wave_closed. Answers the WaveState. Closing a
closed wave changes nothing. Closed waves older than SLOTTING_WAVE_RETENTION_HOURS (default 72) are deleted when the next
wave is created on the same layout.
Lights and webhooks#
POST /api/lights/press#
{ "layoutId": "PUTWALL-A", "address": "A-01-01" }: a button press on a bin's light, sent by a light gateway (or
the wall component's simulator). address is the bin's lightAddress, else its id. The bin decides
(Button presses):
- a pending put there: it is confirmed. Answers
{ "action": "confirmed", "result": ConfirmResult }; - the bin holds a
completeorder: the order is released. Answers{ "action": "released", "orderIds": [...], "bins": [...] }; - the same bin pressed again within
SLOTTING_PRESS_RELEASE_GUARD_MS(default 3000) of a press that confirmed a put there, or of its order completing: nothing changes. Answers200 { "action": "ignored", "reason": "duplicate" }or"too_soon"; - anything else:
409 not_pending, nothing changes.404 unknown_layoutfor an unknown layout.
GET /api/webhooks/dead-letters#
The webhook events given up after SLOTTING_WEBHOOK_MAX_ATTEMPTS, newest first: an array of
{ layoutId, event?, raw?, dropped?, reason, at }. event is the event given up; a corrupt outbox entry has raw (its
text, redacted and clamped) instead, and events dropped over SLOTTING_OUTBOX_MAX_PER_LAYOUT are one entry with
dropped (how many; the reason names the first and last ids) and no event. ?max= (1 to 1000, default 100) sets how
many; the install keeps the last 1000.
POST /api/plan#
Plans without storing anything, for what-if questions ("does this wave fit the wall?"):
{ "layout": Layout, "orders": Order[], "items"?, "options"?, "wallIds"? }. Answers the Plan, as in
POST /api/waves, as if the wall were empty.
POST /api/import/...#
Each import answers { "layout": Layout, "warnings": string[] }: a draft, never saved. The photo import also answers
provider and model. The bodies and options are in Bin setups:
| Path | Body |
|---|---|
/api/import/dxf |
{ "content": "<ASCII DXF>", "options": { "depth": 40, ... } } (options) |
/api/import/csv |
{ "content": "<CSV>", "options": { "unit": "cm", ... } } (columns and options) |
/api/import/grid |
The grid spec (fields) |
/api/import/photo |
{ "image": "<base64>", "mediaType": "image/jpeg", "hints": { ... } } (hints) |
Errors: 422 invalid_import (unreadable content, missing options; CSV row errors in details.errors), 422 invalid_layout (the draft isn't valid, for example overlapping rectangles), 503 ai_not_configured, 502 ai_failed.
GET /healthz#
{ "status": "ok", "store": "redis", "storeOk": true }. While the store can't be reached it answers 503 with
"status": "degraded" and "storeOk": false, so a load balancer takes the instance out of rotation. store is memory or redis. Never blocked.
GET /v1/info#
The version, the license, photo import, lights and webhooks of this install (it never contains a 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.
{"name":"slotting","version":"1.0.0","license":{"status":"valid","trial":false,"source":"configured","allowed":{"slotting":true},"products":["slotting"],"licensee":"Acme Corp","id":"LIC-0007","expires":"2027-10-31","daysLeft":375,"maxInstances":2,"instanceId":"sl-3f9a51c0d2e47b8a16c5e0f1"},"ai":{"configured":true,"provider":"anthropic","model":"claude-opus-5-5"}}| 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 | { "slotting": true } when changes through the API work now |
license.products |
string[] | The products in the license: ["slotting"] |
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 (SLOTTING_INSTANCE_ID, or derived from the hostname) |
ai.configured |
boolean | Photo import is available |
ai.provider, ai.model |
string | The configured provider and model. provider alone when its configuration is incomplete |
lights.driver |
string | The put-to-light driver: none, http, mqtt or simulator |
webhooks.configured |
boolean | Webhooks are configured |
webhooks.endpoints |
number | How many URLs receive them (the URLs themselves are never shown) |
Fields that don't apply are absent.
Errors#
Every error is { "error": "<message>", "code": "<code>" }, with issues for invalid input and details where noted.
| Status | code |
When |
|---|---|---|
| 400 | invalid_request |
The body or query doesn't match the schema, or isn't JSON. issues[] has path, message and code per problem |
| 402 | license_required, license_expired, product_not_licensed |
No valid license; checked before the body is read (License) |
| 404 | unknown_layout, unknown_wave, unknown_order, unknown_wall, unknown_scan |
The id doesn't exist |
| 409 | duplicate_id |
A layout or wave with this id exists |
| 409 | layout_in_use |
The change would take bins from a wave that holds them, or the layout has a wave that is not closed. details names the bin, wall or wave |
| 409 | wave_active |
Delete of a wave that is not closed |
| 409 | wave_closed |
The wave is closed |
| 409 | sku_fully_slotted |
Every order of the wave already has all units of the SKU |
| 409 | no_bin_available |
No bin can take the unit now (details.orderId: the order that needed it). Put it aside |
| 409 | order_not_complete |
Release of an order that isn't complete |
| 409 | order_released |
Release or undo on an order that was already released |
| 409 | wrong_bin |
A confirmation for a bin the scan's units didn't go to (details.expected: the bins that await it) |
| 409 | not_pending |
A confirmation or light press where nothing waits for a confirmation (and, for a press, no complete order) |
| 409 | limit_reached |
A cap of the install was reached (demo limits); details.limit is the setting (SLOTTING_MAX_LAYOUTS, SLOTTING_MAX_WAVES or SLOTTING_MAX_ORDERS_PER_WAVE), details.max its value |
| 409 | demo_protected |
The layout is the showcase's demo layout DEMO (SLOTTING_DEMO_SEED=1): it can't be replaced, deleted or have its walls moved |
| 403 | origin_not_allowed |
A change from a browser page on another origin, not listed in SLOTTING_CORS_ORIGINS (why) |
| 404 | not_found |
No such route |
| 413 | body_too_large |
The body is larger than 25 MB |
| 421 | host_not_allowed |
The Host header is not in SLOTTING_ALLOWED_HOSTS (every route) |
| 415 | unsupported_media_type |
The body is not application/json (also text/plain) |
| 422 | sku_not_in_wave |
No order of the wave has the SKU |
| 422 | item_too_large |
The unit fits no bin of the wave's walls |
| 422 | invalid_layout |
The layout isn't valid; issues lists the problems |
| 422 | invalid_import |
An import can't read its input |
| 502 | ai_failed |
The AI provider failed or timed out (photo import) |
| 503 | ai_not_configured |
No AI provider is configured (photo import) |
| 503 | store_busy, store_unavailable |
The layout was locked too long by other requests, or Redis can't be reached. With Retry-After |
| 500 | internal_error |
Unexpected; the details are in the server log |
Limits#
- Layouts: at most 200 walls and 10,000 bins; dimensions 0.01 to 1,000,000; coordinates within ±10,000,000.
- Imports: DXF and CSV up to 20 MB of text; DXF up to 1,000,000 entities; photos JPEG, PNG or WebP up to 15 MB, up to 2,000 bins per photo.
- Waves: at most 5,000 orders per wave, 500 lines per order, 50,000 entries in the item master; quantities 1 to 1,000,000.
- Request bodies up to 25 MB; the photo's base64 text up to 20 MB.
- Planning is fast: 1,000 orders on 1,000 bins plan in well under a second, 5,000 on 5,000 in a few seconds.