Packfit API
One endpoint. Send items and cartons, receive a packing plan, the chosen cartons and the billable weight. New here? Start with the quickstart.
Authentication
Every request carries a bearer key. Keys are environment-scoped: pf_test_… for development, pf_live_… for production. We store only an HMAC of your key — a lost key cannot be recovered, only replaced.
Authorization: Bearer pf_live_xxxxxxxxxxxxxxxxxxxxxxxxPOST /v1/pack
Units are integer millimetres and integer grams. Integers keep the packing deterministic; floating-point dimensions would let two servers disagree about whether an item fits.
Request
| items[]required | array | The units to pack. See item fields below. |
| cartons[]required | array | Your available carton range. The packer picks from these only — it never invents a box. |
| dimWeight | object | divisorCm3PerKg (e.g. 5000) and optional roundUpToKg (default 0.5). Omit to skip billable-weight calculation. |
| maxCartons | integer | Cap on parcels, default 50. Items that no longer fit are reported in unpacked. |
| rateCards[] | array | Carrier tariffs (carrierId, divisorCm3PerKg, weight brackets, optional surchargePerParcelCents and size limits). Supplied, the packer minimises real money and returns the cheapest carrier per parcel; omitted, it minimises billable weight plus a per-parcel allowance. |
| effort | enum | auto (default), fast or thorough. Controls how many packing strategies are evaluated. auto goes thorough up to 60 units. |
Item fields
| skurequired | string | Your identifier. Echoed back on every placement. |
| lengthMm / widthMm / heightMmrequired | integer | Outer dimensions of one unit. |
| weightGrequired | integer | Weight of one unit. |
| quantity | integer | Defaults to 1. Expanded internally into individual units. |
| nonStackable | boolean | Nothing may be placed on top of this item. |
| thisSideUp | boolean | Restricts rotation to yaw — the item is never laid on its side. |
| maxStackWeightG | integer | Crush limit: total weight allowed to rest above this item. |
| hazmat | enum | ADR class, e.g. class3_flammable_liquids. Incompatible classes are separated into different cartons. |
| tempRegime | enum | ambient | chilled | frozen. Regimes never share a carton. |
| warehouse | string | Source location, default default. Items from different warehouses never share a carton — each becomes its own entry in shipments. |
Carton fields
| idrequired | string | Your carton identifier, returned as cartonId. |
| innerLengthMm / innerWidthMm / innerHeightMmrequired | integer | Usable inner dimensions. |
| maxPayloadGrequired | integer | Maximum contents weight, excluding tare. |
| tareWeightG | integer | Empty carton weight, added to the gross and billable weight. |
| costCents | integer | Packaging cost, summed into packagingCostCents. |
| enabled | boolean | Set false to exclude a carton without removing it from your catalogue. |
Response
{
"cartons": [
{
"cartonId": "M",
"placements": [
{ "sku": "PAN", "x": 0, "y": 0, "z": 0,
"lengthMm": 240, "widthMm": 240, "heightMm": 60,
"weightG": 1450, "orientation": 0 }
],
"grossWeightG": 6690,
"contentWeightG": 6470,
"fillRate": 0.64,
"billableWeightG": 7000,
"tempRegime": "ambient",
"hazmatClasses": [],
"valueCents": 0
}
],
"unpacked": [],
"packagingCostCents": 55,
"totalGrossWeightG": 6690,
"totalBillableWeightG": 7000,
"inputFingerprint": "12zzv44c",
"trace": ["12 units expanded, sorted by decreasing volume", "opened carton M for PAN"]
}x, y, z is the minimum corner of the item inside the carton; lengthMm/widthMm/heightMm are the dimensions after rotation, so you can render or instruct a packer directly. inputFingerprint is a stable hash of the request — identical input always yields an identical plan, so it is safe to use as a cache key.
Errors
| 400 | bad request | Body was not valid JSON. |
| 401 | unauthorized | Missing, malformed, unknown or revoked key. |
| 402 | payment required | Monthly quota exhausted on a plan without overage. Upgrade or wait for the next period. |
| 405 | method not allowed | Use POST. |
| 422 | unprocessable | Valid JSON but not a packable problem — schema violation such as a negative dimension. Quota is not consumed. |
| 429 | too many requests | Per-key rate limit hit. Honour Retry-After. |
| 503 | unavailable | Deployment not fully configured (database or billing). |
Items that physically cannot be placed are not an error: they come back in unpacked with a reason, so a partial order still ships.
Quotas and rate limits
| Plan | Packs / month | Requests / min | Overage |
|---|---|---|---|
| Free | 0 | 60 | hard cap |
| Trial | 250 | 60 | hard cap |
| Starter | 1,000 | 120 | billed |
| Growth | 50,000 | 600 | billed |
| Scale | 500,000 | 3,000 | billed |
Every response carries Packfit-Quota-Limit, Packfit-Quota-Used, Packfit-Quota-Remaining and RateLimit-* headers, so a client can self-throttle without a second request.
Determinism and privacy
The endpoint is stateless: your dimensions, SKUs and carton data are computed in memory and discarded with the response. We persist only a monthly request counter — see the privacy policy.