Packing API
Submit cargo, get back an optimized, physically valid load plan — placements, loading meters, linear feet and billed pallet positions. The same engine behind the app, validated continuously against real dispatcher layouts. Sandbox access on every plan; production access on Enterprise.
Quick start
- Sign in → click your name → API → generate a key.
- Submit a pack job (this one runs in sandbox — free on every plan):
curl -X POST https://hansatic.com/api/v1/pack \
-H "Authorization: Bearer hk_live_..." \
-H "Content-Type: application/json" \
-d '{
"vehicle": "CURTAINSIDER_136",
"objective": "ldm",
"sandbox": true,
"items": [{
"name": "Machine crate",
"length": 2300, "width": 750, "height": 1400,
"weight": 300, "quantity": 7,
"stackable": false, "tiltable": false
}]
}'{
"job_id": "5e3617d1-abe4-4549-b7e6-d18c6baf814f",
"status": "queued",
"sandbox": true,
"status_url": "https://hansatic.com/api/v1/jobs/5e3617d1-..."
}Poll the status_url (same auth header) until status is "done":
{
"job_id": "5e3617d1-...",
"status": "done",
"result": {
"placed_count": 7,
"unplaced_count": 0,
"units": "metric",
"metrics": {
"ldm_m": 5.25,
"span_mm": 5250,
"linear_ft": 17.2,
"pallet_positions_eur": 14,
"total_weight_kg": 2100,
"overlaps": 0
},
"items": [
{ "id": "...", "name": "Machine crate", "placed": true,
"x": 8370, "y": 0, "z": 0,
"rotation_deg": 90, "tilt_deg": 0, "...": "..." }
],
"plan_url": "https://hansatic.com/en/app/plans/..."
}
}Endpoints
POST /api/v1/pack
Validates and queues a packing job. Returns 202 with a job_id. Supports an Idempotency-Key header — retries with the same key return the original job instead of packing twice.
GET /api/v1/jobs/{id}
Job status and, when finished, the full result. Results are retained for 30 days; production plans live in your workspace permanently.
GET /api/v1/openapi.json
The machine-readable OpenAPI 3.1 contract — import it into Postman, code generators, or point your AI coding tool at it. Vehicle codes are injected from the live engine, so it is always current. Open the spec ↗
Response fields
The result object on a finished job:
| Field | Type | Description |
|---|---|---|
| placed_count / unplaced_count | integer | How many items fit / did not fit. |
| units | "metric" | "imperial" | The units every number below is expressed in (echoes the request). |
| metrics.ldm_m | number | Loading meters — the cargo block extent (max end − min start). |
| metrics.linear_ft | number | The same extent in linear feet (US LTL). |
| metrics.span_mm | integer | The extent in mm, always metric. |
| metrics.pallet_positions_eur | integer | null | Billed EUR pallet positions. null when the layout is too large to measure. |
| metrics.total_weight_kg | number | Placed weight, always kg. |
| metrics.overlaps | integer | Interpenetrating pairs — always 0 for a valid layout. |
| items[].placed | boolean | Whether the item made it into the layout. |
| items[].x / y / z | number | null | Position in request units: x from the door end, y from the left wall, z off the floor. All three are null when placed is false. |
| items[].rotation_deg / tilt_deg | 0 | 90 | Orientation: rotation about the vertical axis; tilt = laid on its side. |
| items[].length / width / height / weight | number | The item as submitted, echoed in request units. |
| plan_url | string | null | The created plan in the app. null for sandbox jobs. |
Versioning & stability
The contract is versioned in the path (/api/v1/). Within v1, changes are additive only — new optional request fields, new response fields, new vehicle codes and error codes may appear, and clients should ignore fields they don't recognize. Existing fields never change meaning or type, and nothing is removed. A breaking change means a new version path, with v1 maintained in parallel for at least 12 months and deprecation notice by email to every workspace with an active key.
Request fields
| Field | Type | Description |
|---|---|---|
| vehicle | string | A vehicle code from the tables below. Or pass custom_vehicle_id (a vehicle from your workspace fleet). |
| objective | "ldm" | "pallets" | Minimize loading meters, or minimize billed pallet positions. Default "ldm". |
| units | "metric" | "imperial" | How item dimensions and weights are read AND returned: mm/kg (default) or in/lb. Positions in the response follow the same units. |
| items | array, required | The cargo manifest (fields below). |
| sandbox | boolean | Test mode: full result, nothing persisted. Available on every plan. |
| name | string | Plan name (production mode). |
| ldm_limit_m | number | Restrict the load to the first N loading meters (trucks). |
| budget_ms | number | Optimization time budget (production; default 30000, max 300000). |
| callback_url | https URL | POSTed the result when the job finishes, signed (see Webhooks). |
Item fields
| Field | Type | Description |
|---|---|---|
| length / width / height | number, required | Item dimensions, read in the request's units (mm by default, inches with "units": "imperial"). |
| weight | number, required | Item weight (kg by default, lb with "units": "imperial"). |
| quantity | integer, default 1 | Expands into individual units. |
| name | string, optional | Shown in the plan and the response. |
| stackable | boolean, default true | May other cargo rest on top of this item. |
| max_stack | number, optional | Max weight this item can bear on top (same units as weight). |
| fragile | boolean, default false | Fragile items are placed on top layers only. |
| tiltable | boolean, default true | May the item be laid on its side. |
| destination_stop | integer, default 1 | Delivery stop for multi-stop LIFO loading. |
Vehicle codes
Generated from the live engine specs. Custom vehicles from your workspace fleet work via custom_vehicle_id.
Trucks & trailers
| Code | Vehicle | Interior (L×W×H) | Payload |
|---|---|---|---|
| CURTAINSIDER_136 | 13.6m Curtainsider | 13.62 × 2.48 × 2.70 m | 24 t |
| BOX_136 | 13.6m Box Trailer | 13.62 × 2.46 × 2.68 m | 24 t |
| MEGA_136 | 13.6m Mega Trailer | 13.62 × 2.48 × 3.00 m | 22.5 t |
| MEGA_CURTAIN_136 | 13.6m Mega Curtainsider | 13.62 × 2.48 × 3.00 m | 22 t |
| FLATBED_136 | 13.6m Flatbed | 13.62 × 2.48 × 3.00 m | 25 t |
| REEFER_136 | 13.6m Reefer | 13.32 × 2.44 × 2.54 m | 22 t |
| CURTAINSIDER_75 | 7.5m City Curtainsider | 7.50 × 2.48 × 2.70 m | 12 t |
| SWAPBODY_715 | 7.15m Swap Body | 7.15 × 2.48 × 2.70 m | 12 t |
| SWAPBODY_745 | 7.45m Swap Body | 7.45 × 2.48 × 2.70 m | 12 t |
| DRY_VAN_53 | 53ft Dry Van | 16.15 × 2.59 × 2.74 m | 20 t |
| FLATBED_53 | 53ft Flatbed | 16.15 × 2.59 × 3.66 m | 22 t |
| REEFER_53 | 53ft Reefer | 15.85 × 2.46 × 2.59 m | 18.5 t |
| CURTAINSIDER_125_CN | 12.5m Curtainsider (CN) | 12.50 × 2.50 × 2.80 m | 27 t |
| FLATBED_40_IN | 40ft Flatbed (IN) | 12.19 × 2.50 × 3.50 m | 26 t |
| BOX_138_BR | 13.8m Box Van (BR) | 13.80 × 2.50 × 2.90 m | 28 t |
| CURTAINSIDER_12_ZA | 12m Curtainsider (ZA) | 12.00 × 2.48 × 2.70 m | 28 t |
Containers
| Code | Container | Interior (L×W×H) | Payload |
|---|---|---|---|
| 20DC | 20' Dry Container | 5.89 × 2.35 × 2.39 m | 21.727 t |
| 20HC | 20' High Cube | 5.89 × 2.35 × 2.70 m | 21.58 t |
| 40DC | 40' Dry Container | 12.03 × 2.35 × 2.39 m | 26.63 t |
| 40HC | 40' High Cube | 12.03 × 2.35 × 2.70 m | 26.58 t |
| 45HC | 45' High Cube | 13.56 × 2.35 × 2.70 m | 25.68 t |
| 20OT | 20' Open Top | 5.89 × 2.35 × 2.39 m | 21.64 t |
| 40OT | 40' Open Top | 12.03 × 2.35 × 2.39 m | 26.58 t |
| 20RF | 20' Reefer | 5.44 × 2.29 × 2.27 m | 21 t |
| 40RF | 40' Reefer | 11.58 × 2.29 × 2.21 m | 25.6 t |
| 40RFHC | 40' Reefer High Cube | 11.58 × 2.29 × 2.54 m | 25.88 t |
| 53DC | 53' Domestic | 16.15 × 2.59 × 2.90 m | 26 t |
Access levels & limits
| Sandbox (every plan) | Production (Enterprise) | |
|---|---|---|
| Calls | 25 successful / month | 5,000 successful / month |
| Items per manifest | 20 | 5,000 |
| Concurrent jobs | 1 | 10 |
| Optimization budget | 10 s | 30 s default, up to 300 s |
| Persistence | Nothing stored | Every pack creates a real plan in your workspace |
Only successful packs count — failed validation and failed jobs never bill against a quota. Both counters reset on the 1st (UTC). Sandbox results are complete and uncrippled — the limits are the gate, not the output. Need more than 5,000 packs a month? Talk to us about volume and OEM pricing.
Webhooks
Pass callback_url to receive the result via POST when the job completes. Every callback carries X-Hansatic-Signature: an HMAC-SHA256 of the raw body with your key's webhook secret (shown once at key creation). Verify it:
const crypto = require('crypto');
function verify(rawBody, signatureHeader, webhookSecret) {
const expected = crypto.createHmac('sha256', webhookSecret)
.update(rawBody).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(signatureHeader));
}Delivery is one attempt with a 5s timeout — the status_url always remains the source of truth.
Errors
All errors are { "error": { "code", "message", ... } } — machine-readable code, human-readable message, and field when a specific input is at fault.
| Code | HTTP | Meaning |
|---|---|---|
| invalid_key | 401 | Missing, malformed, or revoked API key. |
| invalid_json | 400 | Body is not valid JSON. |
| invalid_vehicle | 400 | Unknown vehicle code / custom_vehicle_id. |
| invalid_items | 400 | An item field is missing, non-numeric, or out of range. |
| suspicious_dimensions | 400 | All dimensions under 100mm — likely inches sent as mm. Pass "units": "imperial". |
| invalid_units | 400 | units must be "metric" or "imperial". |
| invalid_objective | 400 | objective must be "ldm" or "pallets". |
| invalid_callback | 400 | callback_url must be https. |
| too_many_items | 400 | Manifest exceeds the item limit for your access level. |
| upgrade_required | 402 | Production mode requires the Enterprise plan. Use sandbox, or upgrade. |
| sandbox_limit_reached | 402 | Monthly sandbox allowance used — resets on the 1st. |
| quota_exceeded | 402 | Monthly production quota of 5,000 packs used — resets on the 1st. Higher volume: contact us. |
| too_many_jobs | 429 | Concurrent job limit reached. Poll running jobs and retry. |
| not_found | 404 | No such job for this workspace. |
Questions, higher limits, or a connector you need (Zapier, Excel, MCP)? hello@hansatic.com