Core Concepts

FractalPack models the end-to-end fulfillment workflow from catalog items through packed shipments. This guide explains each entity and how they relate.

Entity lifecycle

Items (catalog)     Containers (box/pallet library)
     │                        │
     ▼                        ▼
  Orders ──────────────► Pack ◄── Rules (business logic)
     │                    │
     ▼                    ▼
 Shipments          Pack Results
     │
     ▼
Fulfillment Plans (parcel vs LTL)
     │
     ▼
 Wave Plans (pick optimization)

Items

Items represent SKUs in your catalog with physical properties. Create items via POST /api/v1/items or manage them through CSV/bulk import.

{
  "id": "SKU-A100",
  "name": "Widget Pro",
  "length": 10,
  "width": 8,
  "height": 6,
  "weight": 2.5,
  "category": "electronics",
  "tags": ["fragile", "battery"]
}

Key properties:

Containers

Containers define the boxes, pallets, and equipment available for packing. Manage them via POST /api/v1/containers or load them into your Container Master for automatic use.

Boxes (fixed-size)

{
  "id": "BOX-MED",
  "containerType": "Box",
  "innerLength": 18,
  "innerWidth": 14,
  "innerHeight": 12,
  "weightMaxGross": 50,
  "baseCost": 1.25
}

Dynamic containers

Variable-size containers (e.g., cut-to-fit boxes) with min/max dimension ranges:

{
  "id": "DYNBOX-1",
  "minLength": 6, "maxLength": 24,
  "minWidth": 6,  "maxWidth": 18,
  "minHeight": 4, "maxHeight": 16,
  "maxWeight": 50
}

Pallets

{
  "id": "PLT-48x40",
  "length": 48,
  "width": 40,
  "maxHeight": 60,
  "palletWeight": 45,
  "maxWeight": 2500,
  "platformHeight": 6,
  "maxStackWeight": 1200
}

Equipment (trailers)

{
  "id": "TRAILER-53",
  "length": 636,
  "width": 98,
  "height": 108,
  "tareWeight": 15000,
  "maxWeight": 45000,
  "equipmentType": "DryVan"
}

Container Master fallback

When a pack request omits containers, FractalPack automatically loads your organization's Container Master -- all active boxes, pallets, and equipment. An explicit empty array ("containers": []) opts out of this fallback.

Orders

Orders represent customer orders with line items. Each line references a SKU with a quantity.

{
  "sourceOrderId": "ORD-2026-1234",
  "lines": [
    { "sku": "SKU-A100", "quantityOrdered": 3 },
    { "sku": "SKU-B200", "quantityOrdered": 1 }
  ]
}

Order statuses: pending -> processing -> packed -> shipped -> cancelled

Shipments

Shipments are packable units created from orders. Creating a shipment resolves order line SKUs against your Item Master to get physical dimensions, then prepares the items for packing.

curl -X POST https://api.fractalpack.com/api/v1/shipments \
  -H "X-Api-Key: fpk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "orderId": "ORD-2026-1234" }'

Shipment lifecycle: created -> optimized -> rated -> ready -> shipped / cancelled

Key operations:

Pack

The core algorithm endpoint. Send items and containers, get back a 3D packing solution.

POST /api/v1/pack

Options:

See the Quickstart for a complete pack request example.

Rules, Policies, and Selectors

Rules vs Policies vs Selectors

Rule — the unit of business logic. A rule defines a single constraint or preference: "keep hazmat and food in separate boxes," "never use a container heavier than 50 lb for fragile items." Rules are reusable — the same rule can belong to multiple policies.

PackingPolicy — a named bundle of rules plus an embedded Selector. A policy is the thing you attach context to. Instead of saying "this rule always fires," you say "this policy (and its rules) fires when the selector matches." Each organization gets an Org Default policy on day one that contains all existing rules and uses a wildcard selector — so your rules behave identically to before, with no changes required.

Selector — the activation predicate embedded on a policy. It declares when the policy should apply. A selector has up to four axes: container level (Box, Pallet, Equipment), shipping mode (Parcel, Ltl, Ftl), ship-to customer ID, and site/warehouse ID. A null or empty axis is a wildcard — it matches everything.

How activation works: When a pack request arrives, FractalPack evaluates every enabled policy's selector against the current context. All matching policies activate (union — order doesn't matter). Their rule ID lists are merged and deduplicated, and then that rule set is handed to the rules engine.

Request context:  { level: "Box", mode: "Ltl", site: "WH-EAST" }

Policy A selector: { levels: ["Box"], modes: null }       → matches (Box ✓, mode wildcard ✓)
Policy B selector: { levels: ["Pallet"] }                → no match (level mismatch)
Policy C selector: { sites: ["WH-EAST", "WH-WEST"] }    → matches (site ✓, other axes wildcard ✓)

Activated rule set: union of Policy A rules + Policy C rules (deduplicated)

Conflict resolution by rule kind:

V1 selector axis limitation: The customers and sites axes match against raw IDs (CustomerInfo.Id and PackRequest.SiteId) — no master-data join is performed. Full master entities are a separate planned effort. See the packing policies recipe for details.


Rules

Rules inject business logic into the packing process without changing your API calls. They are evaluated server-side before every pack operation.

Common rule types:

Rules have an optional advisory applicableLevels field (Box, Pallet, Equipment). This tag is used for UI hints, lint warnings, and AI explainability — it is not enforced at evaluation time. To scope a rule to a specific container level, attach it to a policy whose selector targets that level.

Managing rules via the API

# List all rules
curl https://api.fractalpack.com/api/v1/rules \
  -H "X-Api-Key: fpk_test_your_api_key"

# Create a rule
curl -X POST https://api.fractalpack.com/api/v1/rules \
  -H "X-Api-Key: fpk_test_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Exclude refrigerated containers for dry goods",
    "ruleType": "BoxSetFilter",
    "enabled": true,
    "priority": 10,
    "conditions": {
      "operator": "and",
      "conditions": [
        { "field": "tags", "operator": "contains", "value": "dry-goods" }
      ]
    },
    "action": {
      "filterType": "Exclude",
      "containerIds": ["BOX-REFRIGERATED-1", "BOX-REFRIGERATED-2"]
    }
  }'

# Update a rule
curl -X PUT https://api.fractalpack.com/api/v1/rules/rule_abc123 \
  -H "X-Api-Key: fpk_test_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'

# Delete a rule
curl -X DELETE https://api.fractalpack.com/api/v1/rules/rule_abc123 \
  -H "X-Api-Key: fpk_test_your_api_key"

Per-request rule selection

By default every globally Enabled rule is evaluated automatically on every pack request. To restrict which rules apply for a single request, pass a ruleIds whitelist on the POST /v1/pack (or POST /v1/fulfillment-plans) body:

# Apply only the listed rules; everything else is skipped for this request.
curl -X POST https://api.fractalpack.com/api/v1/pack \
  -H "X-Api-Key: fpk_test_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [...],
    "containers": [...],
    "ruleIds": ["rule_abc123", "rule_def456"]
  }'

The whitelist is intersected with each rule's global enabled flag, so a rule that is enabled: false stays skipped even when listed. Pass ruleIds: [] to disable rules entirely for this one request. Omitting the field falls back to the historical "all enabled rules apply" behavior — your existing integrations need no changes.

The Load Planner's "Rules" sidebar card uses this same field under the hood — the user's per-plan checkbox state is sent as ruleIds on each Optimize click.

Inspecting which rules fired

When rules fire during a pack request, the response includes a firedRules array:

{
  "firedRules": [
    {
      "ruleId": "rule_abc123",
      "ruleName": "Fragile Separation",
      "ruleType": "IncompatibilityGroup",
      "summary": "Separated items into 2 compatibility groups"
    }
  ]
}

Consolidation

Consolidation combines multiple orders heading to the same destination into fewer shipments, reducing shipping costs.

  1. Evaluate (POST /api/v1/consolidation/evaluate) -- analyze orders for consolidation opportunities
  2. Create group (POST /api/v1/consolidation/groups) -- group orders together
  3. Pack group (POST /api/v1/consolidation/groups/{id}/pack) -- pack the consolidated group

Consolidation profiles control constraints: whether mixed orders can share a carton, consolidation level (carton vs pallet), and more.

Fulfillment Plans

Fulfillment plans evaluate the best shipping strategy for an order. The system compares parcel (small package) vs LTL (less-than-truckload) options and returns cost breakdowns for each.

curl -X POST https://api.fractalpack.com/api/v1/fulfillment-plans \
  -H "X-Api-Key: fpk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "orderIds": ["ORD-2026-1234"] }'

Response includes multiple options with cost comparisons. Select an option via POST /fulfillment-plans/{id}/select to create shipments automatically.

Wave Planning

Wave planning optimizes warehouse pick paths across multiple orders. Given a set of orders and warehouse layout, the algorithm groups orders into waves and sequences pick locations to minimize travel distance.

curl -X POST https://api.fractalpack.com/api/v1/wave-plan \
  -H "X-Api-Key: fpk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "orders": [...],
    "maxOrdersPerWave": 20,
    "algorithm": "nearest-neighbor-2opt"
  }'

Available algorithms: nearest-neighbor-2opt (default, higher quality) and nearest-neighbor (faster).


Stable identifiers

Some API values have permanent aliases to preserve backward compatibility. Sending the alias in a request works exactly the same as sending the canonical value — the alias is never echoed back.

Canonical value Permanent alias Notes
fpLayer ebAfit Packing algorithm. Changed in PR 2 (2026-04). Requests using "ebAfit" continue to work indefinitely.

When you send algorithm: "ebAfit", the API resolves it to fpLayer and the response always echoes "fpLayer". If you have dashboards or alerting filters on "ebAfit", update them to "fpLayer" — the alias is accepted on input but never appears in output or telemetry.