API 1.4.0

Packing API guide

Send an order and stock; get each item's box-by-box plan.

1. Get started

Sign up; create a key in your dashboard. The base URL is https://stoapack.stoalogistics.com/api/v1/. Keep STOAPACK_API_KEY on your server. Only successful packing calls use plan balance.

This request packs two cubes into one box. It needs no AI call.

curl -X POST "https://stoapack.stoalogistics.com/api/v1/optimize" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $STOAPACK_API_KEY" \
  -d '{
  "items": [
    {
      "id": "cube",
      "width": 10,
      "height": 10,
      "depth": 10,
      "weight": 100,
      "quantity": 2
    }
  ],
  "warehouses": [
    {
      "id": "east",
      "inventory": {
        "cube": 2
      },
      "available_boxes": [
        {
          "id": "row",
          "width": 20,
          "height": 10,
          "depth": 10,
          "quantity": 1,
          "max_weight": 1000
        }
      ]
    }
  ],
  "unit": "cm",
  "weight_unit": "g",
  "ai_review": false
}'

Excerpt: one placement. Full response follows.

{
  "success": true,
  "solution": {
    "warehouses": [
      {
        "id": "east",
        "packing_solution": {
          "boxes": [
            {
              "box_id": "row",
              "dimensions": {
                "width": 20.0,
                "height": 10.0,
                "depth": 10.0
              },
              "packed_items": [
                {
                  "id": "cube",
                  "quantity": 1,
                  "position": {
                    "x": 0.0,
                    "y": 0.0,
                    "z": 0.0
                  },
                  "rotation": 0,
                  "dimensions": {
                    "width": 10,
                    "height": 10,
                    "depth": 10
                  },
                  "placed_dimensions": {
                    "width": 10.0,
                    "height": 10.0,
                    "depth": 10.0
                  },
                  "unit_id": "east:cube:physical-0"
                }
              ]
            }
          ],
          "unboxed_items": []
        }
      }
    ],
    "unavailable_items": [],
    "unfitted_outcomes": []
  }
}

box_id selects your stocked box. unit_id identifies one physical item; position and placed_dimensions describe its placement. Inspect unavailable_items and unfitted_outcomes even when success is true.

Checked response viewer

Paste response JSON to preview its first box. No API call or balance use.

Complete checked response from the isolated engine run.

The cubes sit at (0, 0, 0) and (10, 0, 0). They fill the box: utilization is 1.0. More checked orders.

Python and Node.js clients

Download the clients; save the cURL order as order.json and set these variables:

export STOAPACK_ORDER_FILE=order.json
export STOAPACK_BASE_URL=https://stoapack.stoalogistics.com
export STOAPACK_API_KEY=your-key
python packing.py
node packing.mjs

Python standard-library client · Node.js 18+ client.

2. What to send

Before your first request: set weight_unit explicitly. Without it, weight:1 means 1 gram, not 1 kilogram; use "weight_unit":"kg" for kilograms. The unit also applies to box max_weight and nesting mass limits. Synchronous requests accept at most 200 box instances: sum available_boxes[].quantity across every warehouse, not just distinct box types or boxes ultimately used. Supply relevant stock rather than your full carton catalogue. See limits and async jobs below.

Use POST /api/v1/optimize or /api/v1.1/optimize. Each warehouse owns its box stock. Use unique item and warehouse IDs, and unique box IDs within each warehouse.

Weights use weight_unit; lengths use unit. Defaults are grams and centimetres. Missing inventory means zero stock; item quantity defaults to one. Each box needs a positive max_weight. See the field reference.

Item fieldValues / defaultMeaning
orientation"any" (default) or "upright" "upright" keeps the item's height vertical; it may still turn on its base.

Choosing a goal

minimize_boxes_then_volume — recommended
The explicit name for the same package-count-first ranking as minimize_boxes; both prefer one Small over one Medium when feasible. Use this name for new integrations.
"optimization_goal": "minimize_boxes_then_volume"
// Six retail items: one stocked Small box when they fit.
minimize_boxes — default
Compatibility default; these goals are aliases. Re-selection tries smaller stocked boxes.
"optimization_goal": "minimize_boxes"
// Keep one package and reduce its box size when feasible.
minimize_volume
Prefer lower total box volume. Package count can differ; compare both before shipping.
"optimization_goal": "minimize_volume"
// Try smaller boxes for a low-volume order.
minimize_cost
Rank supplied box costs, not carrier rates. Use when carton purchase cost matters.
"optimization_goal": "minimize_cost"
// Box field example:
{"id": "small", "cost": 0.50}

StoaPack finds a good packing plan quickly. It doesn't guarantee the best possible one. For custom_instructions, supported rules are item separation, box assignment, volume fill limit. Use exact IDs: “Pack the glass in box type B40.” Same-box grouping and placement rules are unsupported. If we can't apply your instructions, the request fails. We never quietly ignore them.

Free text is an input format, not a promise to enforce arbitrary prose. Unsupported instructions return 400; an upstream AI-stage failure returns 502. ai_review:true is a separate, optional review: it does not add support for otherwise unenforceable rules. Provider choice is honoured; a failed OpenAI call is not silently retried through Gemini.

Set enable_nesting:true to put items inside declared containers, one level deep. Supply is_container, max_nested_weight, and internal_dimensions with length, width, height. If a bundle is too heavy or too large, we retry its units without nesting. See nesting_fallbacks in that warehouse's packing result. Hazmat containers are skipped. Nesting with custom instructions returns 400.

When nesting is enabled, nesting_summary reports containers_used, total_items_nested, containers_with_hazmat_skipped and the coordinate convention. The skipped count counts declared hazmat container item rows, not their quantities or warehouse copies; it is present as zero when none are skipped.

3. Read the result

A unit is one physical item. Each unit ends up in one place:

  • Packed: in solution.warehouses[].packing_solution.boxes[].packed_items.
  • Nested: inside a packed item's nested_contents.
  • Unboxed: shipped as-is when you send requires_box:false; listed in solution.warehouses[].packing_solution.unboxed_items.
  • Unfitted: in stock, but left out; see solution.unfitted_outcomes.
  • Unavailable: out of stock; see solution.unavailable_items.

Lists overlap; count units once. Inspect all outcomes, including on 200. Packed units have IDs, positions and placed sizes. Positions mark the corner nearest the box origin. Unfitted reason codes distinguish size, mass, exhausted box stock and fill limits. Use reason for machine decisions. The legacy outcome_reason is readable text for most outcomes but holds the literal hazmat_unit_quantity_limit code for an over-limit sealed hazmat unit. We retain these mixed historical values for compatibility. Optional details supplies a human-readable explanation of that quantity limit; it is not guaranteed on other outcomes and must not be parsed as a code. Outcome masses echo the request's weight_unit, which is also attached to each mass-bearing row. Check metadata.optimization_goal for the goal actually applied. Nested positions are inside the container before rotation; apply the container's rotation to draw them in the box.

Above the box floor, at least 75% of a unit's base must contact coplanar unit tops. This support rule can change box choice or leave units unfitted; it does not establish stability or load-bearing capacity. Each box's pack_sequence gives physical-unit packing order. Plans made before 18 October 2026 may include placements with less than 75% support. The unsupported_placement warning flags base contact below 50%; it is not a stability assessment.

4. Errors and retries

Unknown fields return 400 unknown_field with the field path. Out-of-range dimensions or masses return 400 numeric_range before packing.

StatusCodeRetryable?What to do
400validation_errorNoFix fields or unsupported instructions.
401 / 403request_error, jobs_not_approvedNoCheck key, balance and access.
413 / 422body_too_large, unsupported_coverageNoReduce size or correct unsupported coverage.
422work_budget_exceededNoSplit the order; don't repeat unchanged.
422review_requirements_not_metNoInspect and change the request.
429rate_limitedYesWait for Retry-After.
502provider_errorYesRetry later with backoff.
503capacity_full, othersRead retryableRetry only when true; honor Retry-After.

For jobs_worker_unavailable, no observed worker means non-retryable. A missed recent heartbeat can be retryable. Follow retryable.

{"success":false,"status":503,"error":"capacity_unavailable","code":"capacity_full","retryable":true}

Check status and Content-Type before parsing: app errors are JSON, but proxy errors may be HTML. Keep X-Request-ID for support. See timing and rate metadata.

5. Limits

One optimize endpoint serves parcels and pallets. Pallet jobs and pallet nesting are not supported.

Pallets

Use box.kind:"pallet" and pallet_options for full or mixed layers. Keep height vertical with orientation:"upright". See the contract 1.4.0 pallet reference and checked request.

Per synchronous requestParcel maximumPallet maximumIf exceeded
Body bytes262,144 (256 KiB)1,048,576413
Item types4040422
Item units801,000422
Warehouses55422
Box types2525422
Box stock200200422
(units)² × (number of box types, minimum 1)100,000100,000422
Packing time2.0 seconds422
Each dimension, in cm or in as requested0.01–10000400 outside range
Box volume, in cubic request unitsAt least 0.001400 below minimum
Mass after conversionZero, or 0.001 g–1,000,000,000 kg400 outside range

Rates and capacity · Readiness.

6. Hazardous materials

Known terminal unfitted reasons: too_large_for_any_box, too_heavy_for_any_box, no_box_stock, exceeds_box_fill_limit, no_upright_fit, exceeds_load_height, packing_constraints_or_capacity, and hazmat_unit_quantity_limit (one sealed unit exceeds a supported hazmat quantity limit). Read solution.unfitted_outcomes[].reason; retain unknown future codes. Legacy unfitted_reason text is retained, not a substitute for the machine code. Inventory-unavailable outcomes are separate.

Missing net quantity: ground requests are not rejected merely because hazmat_net_quantity is missing. Where quantity coverage exists, the engine estimates from declared mass/density with confidence-based safety margins; this is not measured net content. Where coverage is unsupported, quantity checks remain unverified and quantity_limits_checked is false. Warnings beginning MISSING_NET_QUANTITY name each affected item ID. Air liquid quantity checks require explicit per-unit net volume in mL or L; missing volume returns 422 unsupported_coverage, not a ground fallback.

Quantity-driven distribution: packages with hazmat_quantity_split report the item ID, requested quantity, maximum units per package, actual packed quantity and package count. Their placements carry package_split_reason:"hazmat_package_quantity_limit" and existing unit_id values. This means the quantity cap was enforced across multiple packages; geometry may also constrain packing. It never means the contents of one sealed unit were divided.

Package quantity metadata: hazmat_details.quantity_info contains confidence, safety_margin_applied, and gross_hazmat_weight_kg. Air packages also contain q_value and q_value_limit. These are package values, not the original per-unit net declaration. See the typed OpenAPI schemas.

Send hazmat_un and actual per-unit net quantity with its unit. Different dangerous-good item types use separate boxes, even with the same class or UN number. Eligible units split at supported package limits; an over-limit sealed unit is an unfitted outcome, not split into smaller units. See current UN and transport coverage. Use classification_un_numbers for known classification, ground_quantity_check_un_numbers and air_quantity_check_un_numbers for enforced transport quantity coverage, and distinguish quantity-reference-only data from supported transport. One unsupported air UN fails the whole request: preflight each declared UN; the API does not silently omit that item. Unsupported air or sea coverage returns 422; unknown ground UN numbers stay isolated with warnings. Lithium by air is unsupported. Declare state of charge as 0–100 percent; this value is not verified as compliance.

Hazmat checks are decision support. See what's covered →

7. Glossary

Definitions of units, stock, utilization and outcomes.

Before you integrate

Built by Stoa Logistics. Call from your server.

Plans and free offer · Company, data and AI use.

Async appendix

Box-stock limit: the default synchronous box_instances limit is 200, summed over warehouses[].available_boxes[].quantity across all warehouses. It is not a count of box types or only the boxes ultimately used. See the limits table above and deployment capabilities before submitting larger requests.

Backoff: rate exhaustion returns 429; shared HTTP/packing capacity saturation and infrastructure unavailability return 503. Both transient responses include Retry-After; honor it and use bounded backoff. 422 work limits require a smaller request or an approved async job, not an unchanged retry. A second production key does not isolate execution capacity.

Sandbox boundary: a dedicated test key on the production service still shares production capacity. An isolated sandbox requires a separate test service/worker and isolated data; no public sandbox provisioning endpoint is currently offered.

Async is not available without a ready dedicated worker and account approval. Check capability worker_available. When available, submit to POST /api/v1/packing-jobs with an Idempotency-Key header. Poll GET /api/v1/packing-jobs/{id}; use DELETE on that path to cancel. Follow Retry-After while queued or running. Only complete successful jobs use balance. Sync never switches to jobs automatically. See the OpenAPI reference for the order wrapper, deadlines and async limits.

Packing images (optional)

Add render for SVG box views, steps or packing sheets. Use a signed render_ref for separate SVG/PNG downloads. See image options, sizes and errors.