Skip to content

Reference

REST API

Everything the web app does, a bearer token can do.

MethodPathPurpose
GET/healthLiveness. No auth, no database.
POST/packRun the solver on a list of lines.
POST/orders/packRun the solver on a NormalizedOrder.
GET/cartonsList cartons.
POST/cartonsAdd a carton.
POST/cartons/bulkLoad or refresh cartons.
GET/cartons/:idOne carton.
PATCH/cartons/:idAmend a carton.
DELETE/cartons/:idRemove a carton.
GET/itemsList items.
POST/itemsAdd an item.
POST/items/bulkLoad or refresh items.
GET/items/:idOne item.
PATCH/items/:idAmend an item.
DELETE/items/:idRemove an item.
GET/jobsPack history, newest first.
GET/jobs/:idOne job with its full plan.
GET/usageCounts against entitlements.
GET/openapi.jsonThe machine-readable contract.

Base path /api/v1 on your own instance origin.

Authentication

Every request
Authorization: Bearer cpp_live_7Qk2nR4vT8xW1yZ3aB5cD6eF9gH0jK2mN4pQ6rS8
RuleDetail
Where keys come from/api-keys in the app. Shown once; only the SHA-256 is stored, with the first 12 characters kept as a visible prefix.
Scopesread covers every GET. write is additionally required for POST, PATCH, DELETE and both pack routes.
TenancyA key is bound to one organization. No route in the product spans organizations.
RevocationImmediate. Jobs the key created stay attributed to it.

Response envelope

Success
{
  "data": { "…": "the payload" },
  "meta": { "page": 1, "pageSize": 50, "total": 120 }
}
Failure
{
  "error": {
    "code": "carton_not_found",
    "message": "No carton matches that identifier.",
    "details": []
  }
}

No route returns a bare array or a bare object, so a client unwraps once. meta appears only on collections; details is always an array. GET /openapi.json is the one exception — an OpenAPI document wrapped in data is not an OpenAPI document.

Errors

StatuscodeCause
400invalid_jsonThe body is not JSON, or is not an object.
400validation_failedOne field per entry in details.
400invalid_queryA query parameter is out of range or misspelled.
401unauthorizedMissing, unknown or revoked token.
403forbiddenValid key, missing write scope.
404<resource>_not_foundNo such record in your organization. Another tenant’s id is a 404, never a 403.
402plan_limit_reachedA plan ceiling. details names the limit and plan.
409<resource>_sku_conflictThat SKU is taken. PATCH it instead.
429rate_limitedPer-key bucket empty. Honor Retry-After.
500internal_errorUnexpected. Nothing partial is persisted.
The one you will hit while building
400 Bad Request

{
  "error": {
    "code": "validation_failed",
    "message": "lines.0.quantity: Quantity must be 1 or more.",
    "details": [
      {
        "field": "lines.0.quantity",
        "code": "too_small",
        "message": "Quantity must be 1 or more."
      }
    ]
  }
}

Rate limits

CeilingBehavior
Requests per minuteToken bucket per key: 30 on Free, 300 on Pro. Over it is 429 with Retry-After in whole seconds. Retry and nothing is lost.
Pack jobs per month50 on Free, 25,000 on Pro, counted per UTC month across app, API, MCP and ERP. Exhausted is 402. Retrying will not help; reads keep working.
429 — retry · 402 — do not
429 Too Many Requests        402 Payment Required
Retry-After: 12

{ "error": {                 { "error": {
  "code": "rate_limited",      "code": "plan_limit_reached",
  "message": "Rate limit       "message": "…",
    exceeded. Retry in         "details": [
    12 seconds.",                { "limit": "packJobs",
  "details": []                    "plan": "FREE" }
} }                            ] } }

No header reports the remaining monthly allowance. Poll GET /usage instead.

Pagination

Query parameters on every collection
NameTypeReqDescription
pageinteger ≥ 1noOne-based. Default 1.
pageSizeinteger ≥ 1noDefault 50, clamped to 200. meta reports what you got.
qstringnoSubstring match; no wildcards.

Ordering is stable — SKU ascending for catalogs, newest first for jobs — so paging a collection that is being written to does not repeat or skip rows.

Pack

POST/api/v1/packscope: write

Solve an order and store the plan. Consumes one pack job.

Body
NameTypeReqDescription
linesPackLine[]yes1 to 500 lines.
lines[].skustringyesCatalog SKU, or any key when dimensions are inline.
lines[].quantityinteger 1–10000yesUnits of this SKU.
lines[].dimensions{ length, width, height }noOverrides the catalog for this line.
lines[].weightnumbernoPer unit. Send it whenever dimensions is sent.
lines[].namestringnoLabel; falls back to the catalog name.
lines[].fragilebooleannoOverrides the catalog flag for this line.
lines[].orientationLockenumnoOverrides the catalog lock for this line.
lines[].groupKeystringnoKeeps this line with others sharing the key.
objectiveMIN_CARTONS | MIN_COST | MIN_DIM_WEIGHT | BEST_FITnoDefaults to your organization setting. What each one does.
referencestring ≤ 120noYour order number. externalId is a synonym and wins if both are sent.
cartonIdsstring[] ≤ 200noRestrict the search to these cartons.
optionsPackOptionsnoSeven solver controls, clamped to your plan. Table.
savebooleannoDefault true. False returns the plan and writes nothing.
Request
curl -X POST "$CPP_URL/api/v1/pack" \
  -H "Authorization: Bearer $CPP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "SO-10422",
    "objective": "MIN_CARTONS",
    "options": { "allowMultipleCartons": true, "supportThreshold": 0.7 },
    "lines": [
      { "sku": "KTC-SKLT-12", "quantity": 2 },
      { "sku": "GLS-TMBL-4PK", "quantity": 1, "fragile": true },
      {
        "sku": "CUSTOM-PLAQUE",
        "quantity": 1,
        "name": "Engraved slate plaque",
        "dimensions": { "length": 11, "width": 8, "height": 0.75 },
        "weight": 3.4,
        "orientationLock": "FLAT"
      }
    ]
  }'
Response
200 OK

{
  "data": {
    "jobId": "clw3k1n2t0004v8hq7x1s9abc",
    "reference": "PJ-4F2A9C",
    "externalId": "SO-10422",
    "unknownSkus": [],
    "result": {
      "success": true,
      "reference": "SO-10422",
      "objective": "MIN_CARTONS",
      "unitSystem": "IMPERIAL",
      "cartons": [
        {
          "index": 1,
          "cartonId": "clw3jz8pa0001v8hq2m4d7xyz",
          "cartonSku": "RSC-12X10X6",
          "cartonName": "RSC 12 × 10 × 6",
          "cartonType": "BOX",
          "usable": { "length": 11.5, "width": 9.5, "height": 5.5 },
          "outer":  { "length": 12.25, "width": 10.25, "height": 6.25 },
          "placements": [
            {
              "itemId": "clw3jzabc0002v8hq9k3f1def",
              "sku": "KTC-SKLT-12",
              "name": "12 in stainless skillet",
              "sequence": 1,
              "position":   { "x": 0, "y": 0, "z": 0 },
              "dimensions": { "length": 11, "width": 8, "height": 0.75 },
              "rotation": 0,
              "orientation": "flat",
              "weight": 3.4,
              "layer": 0,
              "fragile": false,
              "color": "#6fa8dc"
            }
            /* … 3 more placements */
          ],
          "contentsWeight": 9.8,
          "tareWeight": 0.6,
          "grossWeight": 10.4,
          "dimWeight": 5.6458,
          "billableWeight": 10.4,
          "utilization": 0.7103,
          "usedVolume": 426.8,
          "usableVolume": 600.875,
          "voidVolume": 174.075,
          "cartonCost": 0.94,
          "instructions": [
            "1. Lay KTC-SKLT-12 12 in stainless skillet flat in the back-left corner on the floor of the carton (11.0 x 8.0 x 0.75 in)."
            /* … one line per step */
          ],
          "warnings": []
        }
      ],
      "unpacked": [],
      "summary": {
        "totalCartons": 1,
        "totalItems": 4,
        "packedItems": 4,
        "totalContentsWeight": 9.8,
        "totalGrossWeight": 10.4,
        "totalBillableWeight": 10.4,
        "totalCartonCost": 0.94,
        "averageUtilization": 0.7103,
        "totalVoidVolume": 174.075,
        "strategy": "volume-desc",
        "candidatesEvaluated": 84,
        "computeMs": 37
      },
      "warnings": [],
      "errors": []
    }
  }
}
OutcomeWhat you get
Unknown SKUListed in unknownSkus and left out of the plan. Still a 200 — a missing master-data row is not a malformed request.
Did not fitIn result.unpacked with a reason, and result.success is false. Still a 200 — the run happened.
save: falsejobId and reference are null, nothing is written, the allowance is untouched.
POST/api/v1/orders/packscope: write

The same solver, taking a whole NormalizedOrder and returning an absolute deepLink.

Body shape, signing and the deep-link pattern: ERP integration.

Cartons

GET/api/v1/cartonsscope: read

Paginated, SKU ascending. Filters: page, pageSize, q, active.

POST/api/v1/cartonsscope: write

Counts against the carton-type ceiling. A duplicate SKU is 409.

Field groupRule
Requiredsku name type innerLength innerWidth innerHeight maxWeight tareWeight cost padding quantityOnHand reorderPoint maxBulge fillFactor
Booleansactive and flexible are read as false when absent — the same schema backs the app’s HTML forms. Send "active": true explicitly.
Cross-fieldTwice padding must leave space on every axis, and no outer dimension may be under its inner counterpart.
Types and rangesPer field in the column reference and in the OpenAPI document.
Request
curl -X POST "$CPP_URL/api/v1/cartons" \
  -H "Authorization: Bearer $CPP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sku": "MLR-10X13",
    "name": "Padded mailer 10 × 13",
    "type": "MAILER",
    "innerLength": 10, "innerWidth": 13, "innerHeight": 1,
    "maxWeight": 8, "tareWeight": 0.1, "cost": 0.31, "padding": 0,
    "quantityOnHand": 2600, "reorderPoint": 500,
    "active": true,
    "flexible": true, "maxBulge": 1.5, "fillFactor": 0.85
  }'
Response
201 Created

{
  "data": {
    "id": "clw3jz8pa0001v8hq2m4d7xyz",
    "sku": "MLR-10X13",
    "name": "Padded mailer 10 × 13",
    "type": "MAILER",
    "innerLength": 10, "innerWidth": 13, "innerHeight": 1,
    "outerLength": null, "outerWidth": null, "outerHeight": null,
    "maxWeight": 8, "tareWeight": 0.1, "cost": 0.31, "padding": 0,
    "quantityOnHand": 2600, "reorderPoint": 500,
    "active": true,
    "flexible": true, "maxBulge": 1.5, "fillFactor": 0.85,
    "notes": null,
    "createdAt": "2026-09-28T09:14:02.511Z",
    "updatedAt": "2026-09-28T09:14:02.511Z"
  }
}
GET/api/v1/cartons/:idscope: read

404 when the id is not yours.

PATCH/api/v1/cartons/:idscope: write

Send only what changes; the merged record is held to the full create rules.

DELETE/api/v1/cartons/:idscope: write

204. Prefer active: false when the size may come back.

Items

GET/api/v1/itemsscope: read

Filters: page, pageSize, q, active, category.

POST/api/v1/itemsscope: write

Counts against the item ceiling. A duplicate SKU is 409.

Field groupRule
Requiredsku name length width height weight maxStackWeight orientationLock
Booleansfragile, stackable and active are read as false when absent. That matters most for stackable: omit it and nothing may be placed on the item.
Types and rangesThe column reference
GET/api/v1/items/:idscope: read
PATCH/api/v1/items/:idscope: write
DELETE/api/v1/items/:idscope: write

Historic jobs keep their own snapshot of the SKU and its dimensions.

Bulk load

POST/api/v1/cartons/bulk · /api/v1/items/bulkscope: write

Up to 1,000 rows, validated individually, written in one transaction.

Body
NameTypeReqDescription
mode"CREATE_ONLY" | "UPSERT"yesWhat happens to a SKU you already have.
rowsCartonInput[] | ItemInput[]yesWhole records — the same shape the POST routes take.
Request
curl -X POST "$CPP_URL/api/v1/items/bulk" \
  -H "Authorization: Bearer $CPP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "UPSERT",
    "rows": [
      {
        "sku": "GLS-TMBL-4PK", "name": "Tumbler, 4 pack",
        "length": 8, "width": 8, "height": 6, "weight": 3.1,
        "fragile": true, "stackable": false, "maxStackWeight": 0,
        "orientationLock": "THIS_SIDE_UP", "active": true
      },
      {
        "sku": "ACC-CBL-6", "name": "6 ft cable",
        "length": 4.5, "width": 4.5, "height": 1.1, "weight": 0.21,
        "stackable": true, "maxStackWeight": 0,
        "orientationLock": "FREE", "active": true
      }
    ]
  }'
Response
200 OK

{
  "data": {
    "mode": "UPSERT",
    "counts": { "total": 2, "created": 1, "updated": 1, "skipped": 0, "failed": 0 },
    "results": [
      { "index": 0, "sku": "GLS-TMBL-4PK", "status": "updated" },
      { "index": 1, "sku": "ACC-CBL-6",    "status": "created" }
    ]
  }
}
BehaviorDetail
Per rowcreated, updated, skipped or failed, with the submitted index.
Repeated SKUSkipped rather than applied twice; the report names the row that won.
Plan ceilingChecked against the total the load would leave behind, so re-upserting costs nothing.
Bigger filesUse the importer. Column reference.

Pack jobs

GET/api/v1/jobsscope: read

Summaries only, newest first. A page of full plans would be megabytes.

Query
NameTypeReqDescription
externalIdstringnoExact match on your own order id.
qstringnoMatches our reference and your external id.
statusSUCCESS | PARTIAL | FAILEDnoFilter by outcome.
sourceWEB | API | MCP | INTEGRATIONnoWhich door the run came through.
page · pageSizeintegernoAs every collection.
Request
curl "$CPP_URL/api/v1/jobs?externalId=SO-10422" \
  -H "Authorization: Bearer $CPP_KEY"
Response
200 OK

{
  "data": [
    {
      "id": "clw3k1n2t0004v8hq7x1s9abc",
      "reference": "PJ-4F2A9C",
      "externalId": "SO-10422",
      "source": "API",
      "status": "SUCCESS",
      "objective": "MIN_CARTONS",
      "unitSystem": "IMPERIAL",
      "cartonCount": 1,
      "itemCount": 4,
      "packedCount": 4,
      "totalBillableWeight": 10.4,
      "totalCost": 0.94,
      "averageUtilization": 0.7103,
      "computeMs": 37,
      "createdAt": "2026-09-28T09:14:02.511Z"
    }
  ],
  "meta": { "page": 1, "pageSize": 50, "total": 1 }
}
GET/api/v1/jobs/:idscope: read

Adds deepLink, the requested lines, and the full stored result.

FieldMeaning
deepLinkAbsolute URL of the visual plan on your instance.
linesThe requested lines as they were solved — snapshotted, so catalog edits do not rewrite history.
resultThe stored PackResult, byte-identical to the run.
Retention30 days on Free; kept indefinitely on Pro.

Usage

GET/api/v1/usagescope: read

Counts and limits as parallel objects, so a client can meter each row. null means unlimited.

Response
200 OK

{
  "data": {
    "period": "2026-09",
    "plan": { "id": "PRO", "name": "Pro" },
    "usage": {
      "cartonTypes": 24, "items": 412, "seats": 6,
      "apiKeys": 3, "packJobsThisMonth": 1840
    },
    "limits": {
      "cartonTypes": 1000, "items": null, "seats": 25,
      "apiKeys": 10, "packJobsPerMonth": 25000
    },
    "solver": { "maxCartonsPerShipment": 50, "maxStrategies": 10 },
    "api": { "rateLimitPerMinute": 300 },
    "features": { "webhooks": true, "erpIntegrations": true, "historyRetentionDays": null }
  }
}

Webhooks

ElementValue
Eventspack.completed · pack.failed · carton.low_stock
Envelope{ id, event, createdAt, data }
SignatureX-CartonPackPro-Signature: t=<unix>,v1=<hex> over t + "." + body. Scheme and a verifier.
PayloadsPer event, with delivery rules: ERP integration.
Managed at/integrations. A Pro entitlement.

OpenAPI

GET/api/v1/openapi.jsonno auth

OpenAPI 3.1, generated per request so the servers URL is this deployment's origin.

Every schema, every enum and every ceiling on this page, machine-readable — generate a client instead of writing one. Open the document.

Health

GET/api/v1/healthno auth

For load balancers. Touches no data, so it stays 200 through a database outage.

Response
{ "data": { "status": "ok", "version": "1.0.0", "time": "2026-09-28T09:14:02.511Z" } }
REST API · Carton Pack Pro