Slotting 1.0.0

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#

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}#

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

json
{
  "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}#

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):

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.

json
{"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#