---
name: truck-packer-api
description: |
  Use this skill whenever the user wants to interact with Truck Packer — a 3D truck/container load planning platform. This includes creating, listing, updating, or deleting cases (box types), case categories, containers (trucks/trailers), packs (loading plans), or entities (3D-positioned items within a pack). Also trigger when the user mentions "truck packer", "load plan", "packing API", wants to import gear/inventory into Truck Packer, build single- or multi-truck load plans programmatically, or manage their Truck Packer data in any way. If the user references cases, containers, or packs in a logistics/touring context, this skill is likely what they need.
---

# Truck Packer REST API

You have access to the Truck Packer REST API. Use this reference to make API calls on behalf of the user via `curl` or similar HTTP tools in the shell.

## Authentication

All requests require a Bearer token. API keys start with `tp_` and are generated from **Settings → API Keys** in the Truck Packer app (available on the Business plan, and during the free trial).

```
Authorization: Bearer tp_<YOUR_API_KEY>
```

## Base URL

```
https://api.truckpacker.com/api/v1
```

## Conventions

- Dimensions are in **meters**, weights in **kilograms**
- Rate limit: 200 req/min, 300 burst
- `orgId` is inferred from the API key — never include it in request bodies
- Successful responses are wrapped: `{ "success": true, "data": ... }`. Creates return `201` with `{ "success": true, "data": { "_id": "..." } }`; batch creates return `{ "success": true, "data": { "ids": [...] } }`
- All error responses return `{ "success": false, "error": "..." }`
- **`canRotate3d` defaults to `false`** — unless the source data explicitly says an item can be tipped/rotated, assume it cannot. Most road cases, racks, and consoles must stay upright.
- Dimension fields follow the Y-up axes: `dx` = length, `dy` = height, `dz` = width

---

## Cases

Cases are box types that can be loaded into containers.

| Action        | Method   | Endpoint            |
|---------------|----------|---------------------|
| List all      | `GET`    | `/api/v1/cases`     |
| Get by ID     | `GET`    | `/api/v1/cases/:id` |
| Create        | `POST`   | `/api/v1/cases`     |
| Update        | `PUT`    | `/api/v1/cases/:id` |
| Delete        | `DELETE` | `/api/v1/cases/:id` |

### Create Case body

```json
{
  "name": "string (required)",
  "dx": "number (required) — length in meters",
  "dy": "number (required) — height in meters",
  "dz": "number (required) — width in meters",
  "canRotate3d": "boolean (required)",
  "categoryId": "string (required)",
  "description": "string (optional)",
  "manufacturer": "string (optional)",
  "weight": "number (optional) — kilograms"
}
```

Update accepts the same fields, all optional — include only what you want to change.

---

## Case Categories

Categories group cases by type with a color for visual identification.

| Action        | Method   | Endpoint                     |
|---------------|----------|------------------------------|
| List all      | `GET`    | `/api/v1/case-categories`    |
| Create        | `POST`   | `/api/v1/case-categories`    |
| Update        | `PUT`    | `/api/v1/case-categories/:id`|
| Delete        | `DELETE` | `/api/v1/case-categories/:id`|

### Create Category body

```json
{
  "name": "string (required)",
  "colorHex": "string (required) — e.g. '#FF4444'"
}
```

---

## Containers

Containers are the trucks, trailers, or shipping containers that cases are loaded into.

These endpoints manage your organization's reusable **library** of container definitions. Creating one here does not place it in a pack — to put a container into a load plan, create a `container` entity (see Entities).

| Action        | Method   | Endpoint                 |
|---------------|----------|--------------------------|
| List all      | `GET`    | `/api/v1/containers`     |
| Get by ID     | `GET`    | `/api/v1/containers/:id` |
| Create        | `POST`   | `/api/v1/containers`     |
| Update        | `PUT`    | `/api/v1/containers/:id` |
| Delete        | `DELETE` | `/api/v1/containers/:id` |

### Create Container body

```json
{
  "name": "string (required)",
  "type": "string (required) — one of: dry_container, boxcar, flatbed_trailer, step_deck_trailer, dry_van_trailer, reefer_van_trailer, box_truck, uld, pallet",
  "dx": "number (required) — length in meters",
  "dy": "number (required) — height in meters",
  "dz": "number (required) — width in meters",
  "description": "string (optional)",
  "code": "string (optional) — e.g. ISO 6346 (45G1) for sea, IATA ULD (PMC) for air",
  "payloadCapacity": "number (optional) — kilograms",
  "color": "string (optional) — hex color for the container in the 3D scene"
}
```

Update accepts the same fields, all optional. `orgId` cannot be updated.

---

## Packs

Packs are loading plans — workspaces where cases are arranged inside containers. A single pack can hold **multiple containers** (Business plan), so one plan can cover an entire multi-truck shipment instead of one truck per pack.

| Action         | Method   | Endpoint                    |
|----------------|----------|-----------------------------|
| List all       | `GET`    | `/api/v1/packs`             |
| Get by ID      | `GET`    | `/api/v1/packs/:id`         |
| Get entities   | `GET`    | `/api/v1/packs/:id/entities`|
| Create         | `POST`   | `/api/v1/packs`             |
| Delete         | `DELETE` | `/api/v1/packs/:id`         |

### Create Pack body

```json
{
  "name": "string (optional)",
  "folderId": "string (optional)"
}
```

Deleting a pack permanently removes all its entities, export views, and thumbnails.

---

## Entities

Entities are scene graph nodes within a pack — cases, containers, groups, and text labels positioned in 3D space. A pack can hold at most **600 entities**; a batch create that would exceed this returns `400`.

Structure is expressed by `parentId` alone

| Action         | Method | Endpoint                          |
|----------------|--------|-----------------------------------|
| Get by pack    | `GET`  | `/api/v1/entities?packId=PACK_ID` |
| Batch create   | `POST` | `/api/v1/entities:batchCreate`    |
| Batch update   | `POST` | `/api/v1/entities:batchUpdate`    |
| Batch delete   | `POST` | `/api/v1/entities:batchDelete`    |

All entities in a single batch request must belong to the same pack.

### Batch Create body

```json
{
  "entities": [
    {
      "name": "string (required)",
      "type": "string (required) — case | container | group | text",
      "packId": "string (required)",
      "visible": "boolean (required)",
      "position": "{ x, y, z } (required) — meters",
      "quaternion": "{ x, y, z, w } (required) — use {0,0,0,1} for no rotation",
      "size": "{ x, y, z } (required) — meters",
      "parentId": "string (optional) — the container entity this item is loaded into",
      "caseData": "{ weight?, manufacturer?, canRotate3d, categoryId } (required for case type)",
      "containerData": "{ type, payloadCapacity?, code?, description?, color? } (required for container type — dimensions come from the entity's size, not from containerData)",
      "groupData": "{ colorHex } (required for group type)",
      "textData": "{ text, fontSize, color, textAlign, outlineWidth, outlineColor } (required for text type — all six fields required; textAlign is one of left | center | right | justify)"
    }
  ]
}
```

Each entity must include the type-specific data object that matches its `type` (a `case` needs `caseData`, a `container` needs `containerData`, etc.). Entities missing their type-specific data are dropped from the batch.

### Loading cargo into a container

Set a case's `parentId` to the ID of a `container` entity in the same pack. A case with no `parentId` sits loose in the scene, outside any truck.

- `position` is **relative to the parent** when one is set — a case at `{x: 0.3, y: 0.15, z: 0.2}` sits 0.3m in from the container's origin corner, not from the world origin.
- The parent must already exist in the pack. You cannot reference an entity created in the same batch, so **create the containers in one request, then their cargo in a second** using the returned IDs.

### Batch Update body

```json
{
  "packId": "string (required)",
  "entities": [
    {
      "id": "string (required)",
      "name": "string",
      "visible": "boolean",
      "parentId": "string | null",
      "position": "{ x, y, z }",
      "quaternion": "{ x, y, z, w }",
      "size": "{ x, y, z }",
      "...other optional fields"
    }
  ]
}
```

**Moving cargo between containers:** set `parentId` to another `container` entity's ID in the same pack, or to `null` to unload it into the open scene. Returns `400` if the new parent is in a different pack, is the entity itself, or would create a cycle.

### Batch Delete body

```json
{
  "packId": "string (required)",
  "ids": ["ENTITY_ID_1", "ENTITY_ID_2"]
}
```

Children are deleted recursively.

---

## Coordinate System

Truck Packer uses a **Y-up** coordinate system:
- **X** = length (left/right, along the trailer's long axis)
- **Y** = height (up/down, vertical)
- **Z** = width/depth (front/back)

Both `size` and `position` use this convention. The `position` field is the entity's **center point**, so to place an entity on the ground, set `position.y = size.y / 2`.

## Default Container

Unless the user specifies otherwise, always add a **53' dry van trailer** to new packs. Standard dimensions: 16.154m (L) x 2.591m (W) x 2.743m (H).

```python
trailer_entity = {
    "name": "53' Dry Van Trailer",
    "type": "container",
    "packId": pack_id,
    "visible": True,
    "position": {"x": 16.154 / 2, "y": 2.743 / 2, "z": 2.591 / 2},
    "quaternion": {"x": 0, "y": 0, "z": 0, "w": 1},
    "size": {"x": 16.154, "y": 2.743, "z": 2.591},
    "containerData": {"type": "dry_van_trailer"}
}
```

## Multiple Containers in One Pack

A pack can hold as many containers as the shipment needs — a three-truck tour load is one pack with three `container` entities, not three packs. Build it in two passes:

1. `POST /entities:batchCreate` with one `container` entity per truck. Space them apart along the X axis in **world** coordinates (e.g. trailer length + a few meters of gap) so they don't overlap in the 3D scene. Keep the returned IDs.
2. `POST /entities:batchCreate` with the cases, each carrying `parentId` set to the ID of the truck it rides in. Their `position` is relative to that truck, so you can run the same packing algorithm per container starting from `{0, 0, 0}` in each.

Run the packing strategy below **per container**. When a container is full, don't force it — move the remaining items to the next container rather than stacking beyond what the truck holds. If items are left over after the last container, add another one.

## Parsing Pull Sheets / Manifests

### Weight calculation

Many vendor manifests (e.g. Clair Global) list the weight of the **empty case** on the "Piece" line, not the loaded weight. The items indented below a piece are what goes inside it. When building entities, the weight for each piece should be the **sum of the piece's own weight plus all sub-items listed beneath it**, up until the next piece. This gives the realistic loaded weight for packing and weight distribution.

### Identifying pieces

A "Piece" line is any row that has dimensions (L, W, H). Rows without dimensions are sub-items that belong to the piece above them. Items with a "Piece #X:" prefix in the description are the physical cases/containers that get loaded onto the truck. Sub-items (indented or without dimensions) are contents that live inside the piece and contribute to its total weight.

### Dolly handling in pull sheets

Vendors like Clair Global often list dollies as separate line items with their own piece number and dimensions. These are not standalone cargo — a dolly is a wheeled platform that goes under a specific speaker or rack for transport. When parsing, identify dolly items (anything with "dolly" in the name) and pair them with their parent item by matching model numbers or name prefixes. In the load plan, the dolly should be placed on the floor with its parent item stacked directly on top, forming a single logical unit. Don't place dollies as independent floor items scattered around the truck.

---

## Packing Strategy — Pack Like a Human

When creating a pack from scratch, always generate a fully positioned load plan (not just entities at origin). The goal is a realistic pack that a crew could actually load. Follow these principles:

### 1. Build rows across the trailer width (Z axis)

Pack cases into **rows** that span the width of the trailer (Z axis, ~2.59m for a 53' dry van). Each row is a cross-section of the truck at a given X position. Fill a row across Z before advancing along X to the next row. Keep cases flush against each other within a row — no wasted gaps.

### 2. Smart stacking — floor first, stack only when necessary

**Default behavior: everything on the floor.** Place all items on the trailer floor in a single layer first. Do NOT stack similar cases by default — subwoofers, racks, road cases, etc. all go on the floor unless they physically won't fit.

**Overflow stacking (last resort only):** If the single-layer floor plan extends past the end of the trailer (total X depth exceeds trailer length), THEN start stacking similar cases to reclaim floor space. Only stack cases of the same type or very similar footprint on top of each other — e.g. two subwoofers, two half-packs (~24"×48" / ~0.61m×1.22m), or two quarter-packs (~24"×24" / ~0.61m×0.61m). This is a space-recovery measure, not a default packing behavior.

**Auto-stackable small items**: Items with a footprint smaller than roughly a quarter-pack case (~24"×24" / ~0.61m×0.61m) ARE stackable by default regardless of overflow. This includes pelican cases, small road cases, accessory boxes, tops, lids, and similar. Items that aren't particularly tall (under ~0.5m / 20" height) can also lay across multiple cases beneath them — they don't need a single matching footprint underneath. Think of how a crew would toss a CO12 top grip across one or two nearby stacks, or set a CP218 top on a convenient stack. That's the behavior to replicate.

**Large items**: Big road cases, racks, and consoles should never stack on smaller footprints. Don't put a 4' case on top of a 2' case. They may only stack on same-size or larger footprints, and only during the overflow stacking pass.

### 2b. Dollies go UNDER their parent items — not standalone

Vendors like Clair Global often list dollies as separate line items on a pull sheet. A dolly is not a standalone piece of cargo — it's a wheeled base that a speaker cabinet, rack, or distro sits on for rolling. When building a load plan, place the dolly on the floor first, then place its parent item directly on top of it. The dolly's Y position is `dolly.size.y / 2` (on the ground), and the item riding on it gets `position.y = dolly.size.y + item.size.y / 2`.

Match dollies to their parent items by name similarity — for example, a "Stakrak Dolly" goes under the "Stakrak Distro", a "CS218 Dolly" goes under "CS218" speakers. If the dolly name contains a model prefix that matches another item, pair them. When in doubt, look at the pull sheet grouping — dollies are usually listed near the items they belong to.

### 3. Tight rows, minimal gaps

Cases should be **flush against each other** — no spacing between items in a row. The truck is moving down the road; you want everything snug so nothing shifts. Each row fills across Z (trailer width) with zero gap, and the next row starts immediately at the back of the deepest item in the previous row.

### 4. Flat rows for strapping

Each row should present a **flat face** along the X axis so load bars / straps sit cleanly. Group items with similar X-depth into the same row. Don't mix a 1.7m-deep console with a 0.6m-deep rack in the same row.

### 5. Group by category

Keep similar types of gear together when possible — all racks in one section, all workboxes together, all pelican cases together, etc. This mirrors how crews actually load and makes it faster to find things on-site.

### 6. Load bars every 2.4m – 4.8m (8' – 16')

After every 2.4m to 4.8m of packed depth along the X axis, leave a small visual gap (~0.05m) to represent where a load bar or strap would go. This is standard practice to prevent cargo shift during transit.

### Packing algorithm

Before placing anything, do a pre-processing pass:

**Pre-pass: pair dollies with their parent items.** Scan all items for anything with "dolly" in the name. Match each dolly to its parent item by model prefix or name similarity (e.g. "Stakrak Dolly" → "Stakrak Distro"). Treat each dolly+parent as a single combined unit for placement — the dolly goes on the floor, the parent rides on top. Remove paired dollies from the main item list so they aren't placed separately.

**Pre-pass: classify stackability.** Mark items as auto-stackable if their footprint is under ~0.61m×0.61m (24"×24") or they're short (under ~0.5m tall). These will be placed on top of floor items after the main layout. All other items default to floor placement.

Then sort the remaining items by category, then by X-depth (similar depths together for flat rows), then by weight (heaviest first). Place floor items:

1. For each item (including dolly+parent combos), try to fit it in the current row across Z
2. If z_cursor + item_width > trailer_width, the row is full — advance x_cursor by the row's depth and start fresh
3. Insert a load bar gap (~0.05m) every 2.4–4.8m along X
4. Place flush: z_cursor += item_width (no gap)

After floor items are placed, do a stacking pass for auto-stackable small items — place each one on top of the nearest floor item (or stack) that can support it, preferring items in the same category. Small items can span across two adjacent cases if needed.

**Overflow check:** After placing everything (floor + small-item stacking), check if the total packed X depth exceeds the trailer length. If it does, perform an **overflow stacking pass**: identify groups of same-type cases (e.g. multiple subwoofers, multiple half-packs, multiple quarter-packs) and stack duplicates on top of each other to free floor space. Only stack cases with matching or very similar footprints. Re-run the floor layout with the freed space. Repeat until the load fits or no more valid stacking options remain.

If the load still doesn't fit after stacking, or the packed weight exceeds the container's `payloadCapacity`, **add another container to the pack** and continue the layout there from `{0, 0, 0}` rather than overfilling the first one.

The priorities: tight rows, zero gaps, dollies under their parents, everything on the floor unless it won't fit, small items stacked on top, overflow stacking of similar cases only when needed, group by category, flat faces for strapping, load bars at intervals.

---

## Common Workflows

### Import cases from an external system
1. `POST /case-categories` — create categories first
2. `POST /cases` — create cases referencing those category IDs

### Build a load plan programmatically
1. `POST /packs` — create a pack
2. `POST /entities:batchCreate` — add the container entities (a 53' dry van trailer by default), and keep the returned IDs
3. Parse the pull sheet: sum sub-item weights into each piece's total, set `canRotate3d: false` by default
4. `POST /entities:batchCreate` — add the cases with `parentId` set to the container they ride in, positioned with the packing strategy above (rows, stacking, category grouping, load bars)

### Build a multi-truck load plan
1. `POST /packs` — create one pack for the whole shipment
2. `POST /entities:batchCreate` — add every container entity in one request, spaced apart along X in world coordinates
3. Split the items across containers (fill one, move to the next when it's full or at payload capacity)
4. `POST /entities:batchCreate` — add each container's cases with that container's `parentId`, positioned relative to it
5. `POST /entities:batchUpdate` — rebalance later by changing a case's `parentId` to a different container

### Sync inventory
1. `GET /cases` — fetch current cases
2. `PUT /cases/:id` — update changed ones
3. `POST /cases` — create new ones
4. `DELETE /cases/:id` — remove deleted ones

---

## Error Codes

| Status | Meaning                                                                 |
|--------|-------------------------------------------------------------------------|
| 400    | Bad request — malformed JSON, missing required parameter, or per-pack entity limit exceeded |
| 401    | Missing or invalid API key                                              |
| 404    | Resource not found                                                      |
| 422    | Validation error (check required fields and types)                      |
| 429    | Rate limit exceeded (includes a `retryAfter` value)                     |
| 500    | Server error                                                            |
