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

  1. Sign in → click your name → API → generate a key.
  2. 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:

FieldTypeDescription
placed_count / unplaced_countintegerHow many items fit / did not fit.
units"metric" | "imperial"The units every number below is expressed in (echoes the request).
metrics.ldm_mnumberLoading meters — the cargo block extent (max end − min start).
metrics.linear_ftnumberThe same extent in linear feet (US LTL).
metrics.span_mmintegerThe extent in mm, always metric.
metrics.pallet_positions_eurinteger | nullBilled EUR pallet positions. null when the layout is too large to measure.
metrics.total_weight_kgnumberPlaced weight, always kg.
metrics.overlapsintegerInterpenetrating pairs — always 0 for a valid layout.
items[].placedbooleanWhether the item made it into the layout.
items[].x / y / znumber | nullPosition 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_deg0 | 90Orientation: rotation about the vertical axis; tilt = laid on its side.
items[].length / width / height / weightnumberThe item as submitted, echoed in request units.
plan_urlstring | nullThe 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

FieldTypeDescription
vehiclestringA 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.
itemsarray, requiredThe cargo manifest (fields below).
sandboxbooleanTest mode: full result, nothing persisted. Available on every plan.
namestringPlan name (production mode).
ldm_limit_mnumberRestrict the load to the first N loading meters (trucks).
budget_msnumberOptimization time budget (production; default 30000, max 300000).
callback_urlhttps URLPOSTed the result when the job finishes, signed (see Webhooks).

Item fields

FieldTypeDescription
length / width / heightnumber, requiredItem dimensions, read in the request's units (mm by default, inches with "units": "imperial").
weightnumber, requiredItem weight (kg by default, lb with "units": "imperial").
quantityinteger, default 1Expands into individual units.
namestring, optionalShown in the plan and the response.
stackableboolean, default trueMay other cargo rest on top of this item.
max_stacknumber, optionalMax weight this item can bear on top (same units as weight).
fragileboolean, default falseFragile items are placed on top layers only.
tiltableboolean, default trueMay the item be laid on its side.
destination_stopinteger, default 1Delivery 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

CodeVehicleInterior (L×W×H)Payload
CURTAINSIDER_13613.6m Curtainsider13.62 × 2.48 × 2.70 m24 t
BOX_13613.6m Box Trailer13.62 × 2.46 × 2.68 m24 t
MEGA_13613.6m Mega Trailer13.62 × 2.48 × 3.00 m22.5 t
MEGA_CURTAIN_13613.6m Mega Curtainsider13.62 × 2.48 × 3.00 m22 t
FLATBED_13613.6m Flatbed13.62 × 2.48 × 3.00 m25 t
REEFER_13613.6m Reefer13.32 × 2.44 × 2.54 m22 t
CURTAINSIDER_757.5m City Curtainsider7.50 × 2.48 × 2.70 m12 t
SWAPBODY_7157.15m Swap Body7.15 × 2.48 × 2.70 m12 t
SWAPBODY_7457.45m Swap Body7.45 × 2.48 × 2.70 m12 t
DRY_VAN_5353ft Dry Van16.15 × 2.59 × 2.74 m20 t
FLATBED_5353ft Flatbed16.15 × 2.59 × 3.66 m22 t
REEFER_5353ft Reefer15.85 × 2.46 × 2.59 m18.5 t
CURTAINSIDER_125_CN12.5m Curtainsider (CN)12.50 × 2.50 × 2.80 m27 t
FLATBED_40_IN40ft Flatbed (IN)12.19 × 2.50 × 3.50 m26 t
BOX_138_BR13.8m Box Van (BR)13.80 × 2.50 × 2.90 m28 t
CURTAINSIDER_12_ZA12m Curtainsider (ZA)12.00 × 2.48 × 2.70 m28 t

Containers

CodeContainerInterior (L×W×H)Payload
20DC20' Dry Container5.89 × 2.35 × 2.39 m21.727 t
20HC20' High Cube5.89 × 2.35 × 2.70 m21.58 t
40DC40' Dry Container12.03 × 2.35 × 2.39 m26.63 t
40HC40' High Cube12.03 × 2.35 × 2.70 m26.58 t
45HC45' High Cube13.56 × 2.35 × 2.70 m25.68 t
20OT20' Open Top5.89 × 2.35 × 2.39 m21.64 t
40OT40' Open Top12.03 × 2.35 × 2.39 m26.58 t
20RF20' Reefer5.44 × 2.29 × 2.27 m21 t
40RF40' Reefer11.58 × 2.29 × 2.21 m25.6 t
40RFHC40' Reefer High Cube11.58 × 2.29 × 2.54 m25.88 t
53DC53' Domestic16.15 × 2.59 × 2.90 m26 t

Access levels & limits

Sandbox (every plan)Production (Enterprise)
Calls25 successful / month5,000 successful / month
Items per manifest205,000
Concurrent jobs110
Optimization budget10 s30 s default, up to 300 s
PersistenceNothing storedEvery 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.

CodeHTTPMeaning
invalid_key401Missing, malformed, or revoked API key.
invalid_json400Body is not valid JSON.
invalid_vehicle400Unknown vehicle code / custom_vehicle_id.
invalid_items400An item field is missing, non-numeric, or out of range.
suspicious_dimensions400All dimensions under 100mm — likely inches sent as mm. Pass "units": "imperial".
invalid_units400units must be "metric" or "imperial".
invalid_objective400objective must be "ldm" or "pallets".
invalid_callback400callback_url must be https.
too_many_items400Manifest exceeds the item limit for your access level.
upgrade_required402Production mode requires the Enterprise plan. Use sandbox, or upgrade.
sandbox_limit_reached402Monthly sandbox allowance used — resets on the 1st.
quota_exceeded402Monthly production quota of 5,000 packs used — resets on the 1st. Higher volume: contact us.
too_many_jobs429Concurrent job limit reached. Poll running jobs and retry.
not_found404No such job for this workspace.

Questions, higher limits, or a connector you need (Zapier, Excel, MCP)? hello@hansatic.com