Referencia API CartonPilot
Guía completa para integrar la API de optimización de embalaje CartonPilot en tus aplicaciones.
API Interactiva (Swagger)Descripción general
La API CartonPilot proporciona optimización de embalaje 3D para envíos de comercio electrónico. Dado un conjunto de tamaños de cajas disponibles y artículos a enviar, la API calcula la configuración de embalaje óptima para minimizar los costos de envío mientras maximiza la utilización de las cajas.
Optimización rápida
Resultados en milisegundos con algoritmos avanzados
Embalaje 3D
Embalaje 3D real con soporte de rotación
Restricciones de peso
Respeta los límites de peso para cada tipo de caja
Visualización
Datos de colocación 3D estructurados con salida ASCII y HTML opcional (Starter en adelante)
Tools & SDKs
Explore the API interactively in the live API explorer, or download the Postman collection with ready-to-run requests for every endpoint (set the apiKey collection variable and go). Official TypeScript and Python SDKs - with typed requests, async job polling, and webhook signature verification - are headed to npm and PyPI; contact us for early access.
Autenticación
Todas las solicitudes API requieren autenticación usando una clave API. Incluye tu clave API en los encabezados de la solicitud:
Authorization: Bearer YOUR_API_KEYImportant: Mantén tus claves API seguras. Nunca las expongas en código del lado del cliente o en repositorios públicos.
Puedes generar claves API desde tu panel de control.
Endpoints
/api/v1/shipping-optimizeOptimizar el embalaje de un conjunto de artículos en las cajas disponibles.
Cuerpo de la solicitud
Objeto JSON que contiene boxes, items y options (ver Formato de solicitud)
/api/v1/shipping-optimizeRecuperar capacidades de la API, estadísticas de uso y ejemplos de solicitudes para tu nivel.
Respuesta
Objeto JSON con capacidades del nivel, información de cuota y estadísticas de uso
/api/v1/batch-jobsList your 20 most recent async batch jobs (statuses only). See Batch Orders for details.
Respuesta
JSON object with a jobs array of job status objects
/api/v1/batch-jobs/{id}Poll the status of an async batch job submitted with "async": true. When the job is completed, the response includes the full batch result.
Respuesta
Job status object: jobId, status (pending | processing | completed | failed), totalItems, timestamps, plus result when completed or error when failed
Conjuntos de cajas
Los conjuntos de cajas te permiten guardar y reutilizar configuraciones de cajas en las solicitudes API. En lugar de enviar el array completo de cajas con cada solicitud, puedes referenciar un conjunto guardado por su clave única.
Tip: Crea conjuntos de cajas separados para diferentes escenarios de envío, como cajas de cartón estándar vs cajas aisladas para artículos sensibles a la temperatura.
Gestión de conjuntos de cajas
Crea y gestiona tus conjuntos de cajas desde el panel de conjuntos de cajas. Cada conjunto obtiene una clave única como bs_abc123xyz456.
Uso de conjuntos de cajas en solicitudes
Usa boxSetKey en lugar del array boxes:
{
"boxSetKey": "bs_abc123xyz456",
"items": [
{
"id": "item-001",
"name": "Product A",
"dimensions": { "length": 4, "width": 3, "height": 2 },
"weight": 1.5,
"quantity": 2
}
],
"options": {
"objective": "fewest-parcels"
}
}Límites de conjuntos de cajas por nivel
| Nivel | Conjuntos máx |
|---|---|
| Free | 1 (max 10 boxes) |
| Starter | 2 (max 20 boxes each) |
| Growth | 5 (max 50 boxes each) |
| Scale | 50 (max 100 boxes each) |
Formato de solicitud
El cuerpo de la solicitud POST debe incluir boxes o boxSetKey (no ambos), más items (pedido único) u orders (lote), y opcionalmente options. Las dimensiones y pesos son independientes de las unidades — usa cualquier unidad (pulgadas/lbs, cm/kg) siempre que sea consistente dentro de una solicitud.
Top-Level Fields
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| boxes | array | Yes* | Available boxes for packing (see Box Object). *Either boxes or boxSetKey is required, not both |
| boxSetKey | string | Yes* | Saved box set key (e.g. bs_abc123xyz456) used instead of inline boxes (see Box Sets) |
| catalogKey | string | No | Saved item catalog key (e.g. ic_def789uvw012). Required when any items entry is a { sku, quantity } catalog reference (see SKU Catalog References below) |
| items | array | Yes* | Items to pack for a single order (see Item Object). Entries may alternatively be { sku, quantity } catalog references. *Either items or orders is required, not both |
| orders | array | Yes* | Array of { orderId, items } objects for batch processing (see Batch Orders). Each order may also carry a zone for rate-card lookups, overriding options.zone (see Rate Cards) |
| async | boolean | No | Process a batch in the background (requires orders; default false). Returns 202 with a job to poll at /api/v1/batch-jobs/{jobId} and allows larger batches (see Async Batch Processing) |
| webhook | object | No | { url, secret? } - async batches only. CartonPilot POSTs the job outcome to url when it completes or fails; secret enables HMAC signatures (see Completion Webhooks) |
| options | object | No | Optimization options (see Options Object) |
Objeto Box
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| id | string | Yes | Identificador único para el tipo de caja |
| name | string | No | Nombre legible |
| dimensions | object | Yes | { length, width, height } en tu unidad preferida |
| weightCapacity | number | No | Límite de peso estructural de la caja - peso máximo que el material de la caja puede soportar físicamente |
| weight | number | No | Tare weight of the empty box. Added to the contents weight to compute each shipment's totalWeight |
| cost | number | No | Costo por caja para la optimización |
Objeto Item
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Unique identifier for the item |
| name | string | No | Human-readable name |
| dimensions | object | Yes | { length, width, height } in your preferred unit |
| weight | number | No | Item weight for constraint checking |
| quantity | number | No | Number of this item to pack (default: 1) |
| fragile | boolean | No | Mark as fragile for special handling |
| keepUpright | boolean | No | Prevent vertical rotation |
| fragilityLevel | number | No | Nivel de fragilidad de 0 (robusto) a 5 (extremadamente frágil). Usado por cartonpilot-physics para decisiones de apilamiento. (Solo Pro+) |
| maxTopLoad | number | No | Peso máximo que este artículo puede soportar encima. Usado por cartonpilot-physics para aplicar límites de capacidad de carga. (Solo Pro+) |
SKU Catalog References
Instead of repeating dimensions on every request, save your product dimensions once as an item catalog (managed via /api/item-catalogs; keys look like ic_def789uvw012). Then send catalogKey on the request and reference items by SKU: any entry in items (or an order's items in a batch) may be { "sku": "WIDGET-1", "quantity": 2 } instead of a full item object (quantity defaults to 1). Inline items and SKU references can be mixed freely in the same request.
{
"boxSetKey": "bs_abc123xyz456",
"catalogKey": "ic_def789uvw012",
"items": [
{ "sku": "WIDGET-1", "quantity": 2 },
{ "sku": "GADGET-7" },
{ "id": "custom-1", "dimensions": { "length": 4, "width": 3, "height": 2 }, "weight": 1.5 }
],
"options": { "objective": "fewest-parcels" }
}Referencing a SKU that doesn't exist in the catalog returns 400 invalid_request with an unknownSkus list; using SKU references without a catalogKey also returns 400. SKUs per catalog are tier-limited: Free 100, Starter 1,000, Growth 10,000, Scale 50,000 (with 1 / 2 / 5 / 50 catalogs per plan respectively).
Objeto Options
| Campo | Tipo | Predeterminado | Descripción |
|---|---|---|---|
| objective | string | "fewest-parcels" | Recommended. Lets the engine pick the algorithm: "fewest-parcels" (minimize parcels, uses cartonpilot-ultra), "lowest-cost" (cartonpilot-ultra with cost prioritization), "lowest-billable-weight" (minimize carrier billable weight, uses cartonpilot-ultra; requires dimDivisor), "lowest-invoice-cost" (minimize estimated carrier invoice cost against your saved rate card; requires rateCardKey and a zone — see Rate Cards), "fastest" (first-fit) |
| algorithm | string | - | Advanced. Explicit packing algorithm override (see Algorithms section). Takes precedence over objective. If neither is set, your account's default algorithm (dashboard setting) or cartonpilot-ultra is used |
| allowRotation | boolean | true | Allow items to be rotated for better fit |
| prioritize | string | "space" | "space" | "cost" | "speed" | "consolidation" | "billable-weight" | "invoice-cost" ("billable-weight" requires dimDivisor; "invoice-cost" requires rateCardKey and a zone) |
| dimDivisor | number | - | Your carrier's dimensional-weight divisor (e.g. 139 for inches/lbs, 5000 for cm/kg, or your negotiated value). Must match the units used in the request. When provided, every shipment includes a billing object and the summary includes totalBillableWeight. Overrides a rate card's divisor when both are present |
| rateCardKey | string | - | Key of a saved rate card (e.g. rc_abc123xyz456) for invoice-cost estimation and optimization. Requires a zone (options.zone or per-order zones). See Rate Cards below |
| zone | string | number | - | Shipping zone for rate-card lookups; must exist in the referenced rate card. For batch requests this is the default — a zone on an individual order overrides it |
| enableVisualization | boolean | false | Include structured placement data per shipment in a visualization field for client-side rendering (not available on Free tier) |
| visualizationFormat | string | "data" | "data" | "ascii" | "html". "ascii" adds an asciiVisualization string per shipment; "html" adds visualizationHtml and shipmentVisualizations HTML strings at the top level. Requires enableVisualization |
| maxShipmentWeight | number | - | Carrier weight limit - shipments above this threshold incur overweight fees |
| oversizedItemHandling | string | "unpacked" | "custom-box" | "unpacked" - How to handle items larger than any available box |
| overweightItemHandling | string | "allow" | "allow" | "unpacked" - How to handle items heavier than maxShipmentWeight (defaults to "allow" when maxShipmentWeight is set) |
Most carriers bill against the greater of a parcel's actual weight and its dimensional weight (box volume divided by your carrier's divisor). Supply your contract's divisor in options.dimDivisor and every shipment includes a billing breakdown of actual, dimensional, and billable weight. Set options.objective to "lowest-billable-weight" and the engine also picks boxes to minimize what carriers bill. Billable weight is a major driver of freight cost, so this usually reduces it — but it is not the only driver, and it does not account for zone pricing, weight breaks or surcharges in your specific agreement. For a rate-aware answer, use "lowest-invoice-cost" with a configured rate card, which prices each candidate against your actual contract.
Rate Cards (Invoice-Cost Optimization)
For true invoice-cost optimization, upload your negotiated carrier rates as a rate card (managed via /api/rate-cards; keys look like rc_abc123xyz456). A card contains a zone × weight rate table (rates: strictly increasing weight breaks, each with the same zone keys), an optional negotiated DIM divisor, an optional maxPackageWeight (a hard cap — the optimizer will never build a parcel heavier than this; it is applied as maxShipmentWeight, so all items need weights), and an optional overweight surcharge (overweightThreshold + overweightFee — e.g. UPS's heavy-package fee for parcels over 50 lb actual weight — priced into the cost function rather than enforced as a limit). Everything is unit-agnostic: weights and the divisor just need to use the same units as your request.
{
"name": "UPS Ground 2026",
"carrier": "UPS",
"service": "Ground",
"currency": "USD",
"dimDivisor": 139,
"maxPackageWeight": 150,
"overweightThreshold": 50,
"overweightFee": 24.00,
"rates": [
{ "weight": 1, "zoneRates": { "2": 9.51, "4": 10.02, "7": 11.33 } },
{ "weight": 5, "zoneRates": { "2": 11.87, "4": 13.44, "7": 16.09 } },
{ "weight": 10, "zoneRates": { "2": 14.52, "4": 17.91, "7": 23.86 } },
{ "weight": 50, "zoneRates": { "2": 32.10, "4": 44.75, "7": 68.20 } }
]
}Reference the card on optimize requests with options.rateCardKey plus a zone: options.zone for single orders (or as a batch-wide default), or a zone on each entry in orders (per-order zones override the request-level one). With a rate card present, every shipment's billing block gains an invoice estimate ({ zone, baseRate, overweightFee, total, currency }), dimWeight/billableWeight use the card's DIM divisor (unless options.dimDivisor overrides it), and the order summary — and batch summary — gain totalInvoiceCost and currency. Billable weights beyond the card's last weight break are billed at the last break's rate; the overweight surcharge keys off actual weight (contents plus box tare), matching how carriers assess heavy-package fees.
Set options.objective to "lowest-invoice-cost" and the optimizer downsizes each shipment to the box that minimizes your estimated freight (rate table + DIM weight + overweight surcharge) plus box material cost. It also composes with an explicit options.algorithm, since it runs as a post-pass.
{
"boxSetKey": "bs_abc123xyz456",
"catalogKey": "ic_def789uvw012",
"orders": [
{ "orderId": "ORD-001", "zone": "4", "items": [{ "sku": "WIDGET-1", "quantity": 2 }] },
{ "orderId": "ORD-002", "zone": "7", "items": [{ "sku": "GADGET-7" }] }
],
"options": { "objective": "lowest-invoice-cost", "rateCardKey": "rc_abc123xyz456" }
}Using an unknown rateCardKey returns 404 not_found; a zone that isn't in the card returns 400 invalid_request with an availableZones list; "lowest-invoice-cost" without rateCardKey, or rateCardKey without any zone, also returns 400. Rate cards are available on every plan; saved cards are tier-limited: Free 1, Starter 3, Growth 10, Scale 50.
Understanding Weight Constraints
The API supports two distinct types of weight constraints:
Box Structural Limit (weightCapacity)
Set on each box type. This represents the maximum weight the box material can physically hold without breaking. The optimizer will not place items that would exceed this structural limit.
"boxes": [{ "id": "medium", "weightCapacity": 25, ... }]Carrier Shipment Limit (maxShipmentWeight)
Set in options. This represents the carrier threshold above which overweight fees apply. All shipments will be optimized to stay under this limit regardless of box capacity. When this option is set, all items must have a weight property defined.
"options": { "maxShipmentWeight": 50 }Overweight Item Handling (overweightItemHandling)
When a single item exceeds the maxShipmentWeight, this option controls how it's handled:
"allow"(default): Item is allowed to ship alone in a box, weight limit waived for this shipment"unpacked": Item is placed in the unpackedItems array with reason
"options": { "maxShipmentWeight": 50, "overweightItemHandling": "unpacked" }Both constraints are enforced simultaneously. A shipment must satisfy both the box structural capacity and the carrier weight limit.
Pedidos por lotes
Process multiple orders in a single API request. Instead of sending individual items, you can send an array of orders, each with its own items. The API will optimize packing for each order independently and return consolidated results.
Availability: All tiers
Batch order processing is available for all subscription tiers with the same limits.
Batch Limits
| Limit Type | Sync (default) | Async ("async": true) |
|---|---|---|
| Max Orders per Batch | 50 | 500 |
| Max Items per Order | 100 | 100 |
| Max Total Items per Batch | 500 | 5,000 |
Batch Request Format
Use the orders array instead of items. Each order must have an orderId and its own items array.
{
"boxes": [
{ "id": "small", "name": "Small Box", "dimensions": { "length": 10, "width": 8, "height": 6 }, "cost": 2.50 },
{ "id": "medium", "name": "Medium Box", "dimensions": { "length": 14, "width": 12, "height": 10 }, "cost": 4.00 }
],
"orders": [
{
"orderId": "ORD-001",
"items": [
{ "id": "item-1", "name": "Book", "dimensions": { "length": 9, "width": 6, "height": 1.5 }, "weight": 1.2, "quantity": 2 }
]
},
{
"orderId": "ORD-002",
"items": [
{ "id": "item-2", "name": "Laptop", "dimensions": { "length": 14, "width": 10, "height": 2 }, "weight": 4.5, "quantity": 1 },
{ "id": "item-3", "name": "Charger", "dimensions": { "length": 4, "width": 3, "height": 2 }, "weight": 0.3, "quantity": 3 }
]
}
],
"options": {
"objective": "fewest-parcels"
}
}Batch Response Format
The response includes results for each order plus an overall summary.
{
"success": true,
"batch": true,
"results": [
{
"orderId": "ORD-001",
"success": true,
"data": {
"shipments": [...],
"unpackedItems": [],
"summary": { "totalShipments": 1, "totalCost": 2.50, ... }
}
},
{
"orderId": "ORD-002",
"success": true,
"data": {
"shipments": [...],
"unpackedItems": [],
"summary": { "totalShipments": 1, "totalCost": 4.00, ... }
}
}
],
"summary": {
"totalOrders": 2,
"successfulOrders": 2,
"failedOrders": 0,
"totalBoxesUsed": 2,
"totalCost": 6.50,
"averageUtilization": 42.5,
"totalItemsPacked": 3,
"totalItemsUnpacked": 0
},
"metadata": {
"algorithm": "cartonpilot-ultra",
"executionTimeMs": 45,
"timestamp": "2025-12-06T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"tier": "BASIC"
}
}Async Batch Processing
For larger batches, set "async": true on a batch request (requires orders). This raises the limits to 500 orders, 100 items per order, and 5,000 total items. Instead of waiting for results, the API responds immediately with 202 Accepted, a jobId, and a Location header pointing at the job status URL. Your monthly item quota is consumed when the job is submitted.
{
"boxes": [
{ "id": "standard", "dimensions": { "length": 12, "width": 10, "height": 8 }, "cost": 3.00 }
],
"orders": [
{
"orderId": "ORD-001",
"items": [
{ "id": "item-1", "dimensions": { "length": 4, "width": 3, "height": 2 }, "quantity": 2 }
]
}
],
"async": true,
"options": {
"objective": "fewest-parcels"
}
}{
"success": true,
"jobId": "cm0abc123",
"status": "pending",
"statusUrl": "/api/v1/batch-jobs/cm0abc123",
"totalOrders": 1,
"totalItems": 2,
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}Poll GET /api/v1/batch-jobs/{id} (same API-key authentication) to track the job. The response contains jobId, status (pending | processing | completed | failed), totalItems, and createdAt / startedAt / completedAt timestamps. When the status is completed, the full batch response (same format as the sync batch response above) is included under result; when failed, the failure reason is included under error. You can also list your 20 most recent jobs with GET /api/v1/batch-jobs. Jobs (including their request payloads and results) are automatically deleted 30 days after submission.
Completion Webhooks
Instead of polling, add a webhook to an async batch request and CartonPilot will POST the outcome to your URL when the job completes or fails. URLs must use https (plain http is allowed for localhost during development). Deliveries are retried twice - after 5 and 30 seconds - and the outcome is reported as webhook.deliveryStatus (pending | delivered | failed: <reason>) when you poll the job.
{
"boxes": [ ... ],
"orders": [ ... ],
"async": true,
"webhook": {
"url": "https://example.com/hooks/cartonpilot",
"secret": "whsec_your_signing_secret"
},
"options": { "objective": "fewest-parcels" }
}{
"event": "batch_job.completed",
"jobId": "cm0abc123",
"status": "completed",
"statusUrl": "/api/v1/batch-jobs/cm0abc123",
"totalOrders": 120,
"totalItems": 480,
"summary": { "totalOrders": 120, "successfulOrders": 120, "failedOrders": 0, "totalBoxesUsed": 143, "totalCost": 214.5, "averageUtilization": 74.2, "totalItemsPacked": 480, "totalItemsUnpacked": 0 },
"timestamp": "2026-07-27T04:29:12.982Z"
}Failed jobs send "event": "batch_job.failed" with an error field instead of summary. Every delivery carries X-CartonPilot-Event and X-CartonPilot-Job-Id headers. When you supply a secret (8+ characters), deliveries are signed so you can verify they came from CartonPilot: the X-CartonPilot-Signature header contains sha256= followed by the hex HMAC-SHA256 of the raw request body using your secret. Compute the same HMAC on your side and compare with a timing-safe comparison.
import { createHmac, timingSafeEqual } from "crypto";
function verifyCartonPilotWebhook(rawBody, signatureHeader, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}Backward Compatibility
The original single-order format using items array continues to work. Batch processing is only triggered when the orders array is present. Provide either items or orders, not both.
Formato de respuesta
Successful responses include shipment details, packing statistics, and metadata.
Response Structure
| Field | Description |
|---|---|
| success | Boolean indicating request success |
| data.shipments | Array of shipment objects with packed items (see shipment fields below) |
| data.shipments[].utilization | { volume, weight, efficiency } utilization percentages for the shipment |
| data.shipments[].contentsWeight | Total weight of the packed items only (excludes the box itself) |
| data.shipments[].totalWeight | contentsWeight plus the box tare weight, when box.weight is provided |
| data.shipments[].billing | { actualWeight, dimWeight, billableWeight } carrier billing figures, when options.dimDivisor or a rate card is provided. actualWeight is contents plus box tare (unrounded), dimWeight is box volume / dimDivisor rounded up, billableWeight is the greater of the two — what a carrier bills against. With a rate card, also includes an invoice object: { zone, baseRate, overweightFee, total, currency } (see Rate Cards) |
| data.shipments[].visualization | Structured placement data (boxDimensions + item placements) for client-side rendering, when enableVisualization is true |
| data.shipments[].asciiVisualization | ASCII rendering of the shipment, only when visualizationFormat is "ascii" |
| data.unpackedItems | Items that couldn't fit in any box |
| data.summary | Aggregated statistics: totalShipments, totalCost, averageUtilization, itemsSuccessfullyPacked, itemsUnpacked, plus totalBillableWeight (sum of per-shipment billable weights) when options.dimDivisor is provided, and totalInvoiceCost + currency (sum of per-shipment invoice estimates) when a rate card is used — the batch summary gains the same two fields |
| data.suggestions | Optimization suggestions |
| metadata | algorithm, executionTimeMs (number, milliseconds), timestamp, requestId (uuid, also sent as the X-Request-Id header), tier, and an optional note (e.g. when physics parameters were ignored) |
| visualizationHtml | Combined 3D HTML visualization, included with shipmentVisualizations (per-shipment HTML strings) only when visualizationFormat is "html" (not available on Free tier) |
Box Structure in Shipments
Each shipment contains a box object with the following fields:
| Field | Type | Description |
|---|---|---|
| id | string | Box ID (from your submitted boxes, or "custom-*" for oversized items) |
| name | string | Box name (or "Custom Box" for oversized items) |
| dimensions | object | { length, width, height } of the box |
| cost | number | Box cost (optional, 0 for custom boxes) |
| weight | number | Tare weight of the empty box (only present when supplied in the request) |
| type | string | "custom" when oversizedItemHandling is "custom-box" (only present for custom boxes) |
Packed Items Structure
Each packed item in a shipment includes the following fields:
| Field | Type | Description |
|---|---|---|
| itemId | string | Original item ID as submitted in the request |
| itemIndex | number | Index within quantity expansion (0, 1, 2, ...). For items with quantity > 1, each instance gets a unique index. |
| position | object | { x, y, z } coordinates of item placement in the box |
| rotation | object | Axis mapping showing how item was rotated to fit |
| rotatedDimensions | object | { length, width, height } after rotation applied |
Rate Limit Headers
Every response includes rate limit information and a unique request identifier (also available as metadata.requestId in the body):
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 985
X-RateLimit-Reset: 2025-11-30T00:00:00.000Z
X-Request-Id: 550e8400-e29b-41d4-a716-446655440000Algoritmos
All 8 algorithms are available on every subscription tier. For most integrations we recommend setting options.objective ("fewest-parcels", "lowest-cost", "lowest-billable-weight", "lowest-invoice-cost", or "fastest") and letting the engine pick the algorithm; options.algorithm remains available as a power-user override and takes precedence over objective.
first-fitAll TiersFast, simple algorithm that places items in the first available space. Best for speed-critical applications. Used by the "fastest" objective.
best-fitAll TiersFinds the best fitting space for each item, improving utilization over first-fit.
guillotineAll TiersDivides space into rectangles for efficient 2D packing. Good balance of speed and utilization.
max-rectsAll TiersAdvanced algorithm maintaining multiple free rectangles for optimal packing.
cartonpilot-ultraAll Tiers (Default)Our smartest algorithm, optimized for both binning (grouping items into separate shipments) and packing (placing items into the smallest possible box). Uses look-ahead optimization, post-pack consolidation, and multi-pass refinement to minimize total boxes and maximize space utilization. Best for most use cases. Used by the "fewest-parcels", "lowest-cost", "lowest-billable-weight", and "lowest-invoice-cost" objectives.
cartonpilot-fitAll TiersCombines global box selection strategy with fast 3D placement. Uses O(items × boxes × positions) complexity for efficient packing with excellent results.
cartonpilot-maxAll TiersWeight-based grouping algorithm that groups items by order, splits bundles exceeding weight limits, consolidates small bundles, and validates each bundle with 3D packing. Ideal for multi-order shipments where weight-based grouping is preferred.
cartonpilot-physicsAll TiersPhysics-aware packing algorithm that prioritizes heavier items at the bottom for stability, respects fragility levels (0-5), enforces load capacity limits, and validates center of gravity balance. Includes support area validation (70% minimum) and rotation constraints for fragile items. Advanced features (fragilityLevel, maxTopLoad) require the Growth plan or above.
Niveles de suscripción
Choose the plan that fits your needs. All 8 algorithms are available on every tier — tiers differ only by request limits, monthly item quota, visualization, and physics parameters.
| Feature | Free ($0) | Starter ($49/mo) | Growth ($149/mo) | Scale ($399/mo) |
|---|---|---|---|---|
| Items/Month | 1,000 | 10,000 | 50,000 | 250,000 |
| Max Items per Request | 10 | 25 | 100 | 1,000 |
| Max Box Types per Request | 10 | 20 | 50 | 100 |
| API Keys | 1 | 1 | 5 | 10 |
| Rate Cards | 1 | 3 | 10 | 50 |
| Batch Orders (max/items/total) | 50/100/500 | 50/100/500 | 50/100/500 | 50/100/500 |
| 3D Visualization | - | ✓ | ✓ | ✓ |
| Physics Parameters (fragilityLevel, maxTopLoad) | - | - | ✓ | ✓ |
| Algorithms | All 8 | All 8 | All 8 | All 8 |
| Overage Rate (per 1000 items) | $8.00 | $6.00 | $4.00 | $3.00 |
All Algorithms on All Tiers
- Algorithms: All 8 packing algorithms are available on every tier, including FREE.
- What differs: items per request, box types per request, and monthly item quota.
- Visualization: not available on the FREE tier.
- Physics parameters:
fragilityLevelandmaxTopLoadrequire Growth or Scale.
Límites de tasa
Why We Count Items, Not API Requests
Batching multiple orders into a single API call is more efficient for everyone. Charging per item optimized aligns with our CPU costs and encourages you to batch orders for maximum efficiency. This means you can send 100 orders in one batch request and pay the same as 100 individual requests with one order each.
Usage limits are based on items processed per month (counting item quantities). Limits reset on the 1st of each month. When you exceed your limit, the API returns a 429 status code with details about your usage.
| Tier | Monthly Item Limit | Overage Rate |
|---|---|---|
| Free | 1,000 items | $8.00 / 1,000 items |
| Starter | 10,000 items | $6.00 / 1,000 items |
| Growth | 50,000 items | $4.00 / 1,000 items |
| Scale | 250,000 items | $3.00 / 1,000 items |
Rate Limit Response
{
"error": "quota_exceeded",
"message": "This request would use 50 items, but you only have 25 items remaining in your monthly quota of 1000 items.",
"itemsRequested": 50,
"itemsRemaining": 25,
"monthlyLimit": 1000,
"resetAt": "2026-02-01T00:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}Response Headers
Every API response includes usage headers:
X-RateLimit-Limit: Your monthly item limitX-RateLimit-Remaining: Items remaining after this requestX-RateLimit-Reset: When your quota resets (1st of next month)X-Items-Used: Total items used this monthX-Request-Id: Unique identifier for this request (matches metadata.requestId)
Códigos de error
All error responses use a consistent envelope with a machine-readable error code, a human-readable message, optional detail fields, and a requestId you can include in support requests (also sent as the X-Request-Id header). Validation failures (400 invalid_request) include an issues array pointing at the offending fields. Requests are validated strictly: dimensions must be positive numbers, unknown algorithms are rejected, either boxes or boxSetKey is required (not both), and either items or orders is required (not both).
{
"error": "invalid_request",
"message": "Request validation failed",
"issues": [
{ "path": "items.0.dimensions.length", "message": "Number must be greater than 0" },
{ "path": "boxes", "message": "Either 'boxes' or 'boxSetKey' is required" }
],
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}| Status | Error | Description |
|---|---|---|
| 400 | invalid_request | Request body failed validation (includes an issues array) |
| 401 | authentication_failed | Invalid or missing API key |
| 403 | endpoint_not_allowed | This API key is restricted from the requested endpoint |
| 403 | algorithm_not_allowed | This API key is restricted from the requested algorithm |
| 400 / 403 | limit_exceeded | Too many items or box types for your tier (403), or batch limits exceeded (400) |
| 404 | not_found | Referenced resource (e.g. boxSetKey, catalogKey, or rateCardKey) not found |
| 409 | idempotency_conflict | Idempotency-Key reused with a different request payload |
| 429 | quota_exceeded | Monthly item quota exhausted |
| 500 | internal_error | Server-side error, please retry |
Idempotency
To retry a POST /api/v1/shipping-optimize request safely, send an optional Idempotency-Key header (any unique string up to 200 characters). If a request with the same key and the same payload is received again within 24 hours, the stored response is replayed — marked with an Idempotency-Replayed: true response header — without consuming your quota a second time. Reusing a key with a different payload returns 409 idempotency_conflict. This works for sync single and batch requests (200) as well as async submits (202), where the replay returns the same jobId.
Idempotency-Key: order-ORD-001-attempt-1Ejemplos
Ejemplo de solicitud
{
"boxes": [
{
"id": "b1-box",
"name": "Box B1",
"dimensions": {
"length": 7,
"width": 7,
"height": 12
},
"weightCapacity": 25,
"weight": 0.4,
"cost": 1.18
},
{
"id": "b3-box",
"name": "Box B3",
"dimensions": {
"length": 11,
"width": 10,
"height": 14
},
"weightCapacity": 35,
"weight": 0.6,
"cost": 2.11
},
{
"id": "b7-box",
"name": "Box B7",
"dimensions": {
"length": 20,
"width": 16,
"height": 18
},
"weightCapacity": 55,
"weight": 0.9,
"cost": 3.98
}
],
"items": [
{
"id": "BOOK-001",
"name": "Hardcover Book",
"dimensions": {
"length": 9.5,
"width": 7.5,
"height": 1.5
},
"weight": 1.8,
"quantity": 2
},
{
"id": "LAPTOP-COMP",
"name": "Laptop Computer",
"dimensions": {
"length": 18,
"width": 11,
"height": 4.5
},
"weight": 6.8,
"quantity": 1
}
],
"options": {
"objective": "fewest-parcels",
"allowRotation": true,
"dimDivisor": 139,
"enableVisualization": true
}
}Ejemplo de respuesta
{
"success": true,
"data": {
"shipments": [
{
"box": {
"id": "b7-box",
"name": "Box B7",
"dimensions": {
"length": 20,
"width": 16,
"height": 18
},
"cost": 3.98,
"weight": 0.9
},
"packedItems": [
{
"itemId": "BOOK-001",
"itemIndex": 0,
"position": {
"x": 0,
"y": 0,
"z": 0
},
"rotation": {
"lengthAxis": "x",
"widthAxis": "y",
"heightAxis": "z"
},
"rotatedDimensions": {
"length": 9.5,
"width": 7.5,
"height": 1.5
}
},
{
"itemId": "BOOK-001",
"itemIndex": 1,
"position": {
"x": 9.5,
"y": 0,
"z": 0
},
"rotation": {
"lengthAxis": "x",
"widthAxis": "y",
"heightAxis": "z"
},
"rotatedDimensions": {
"length": 9.5,
"width": 7.5,
"height": 1.5
}
},
{
"itemId": "LAPTOP-COMP",
"itemIndex": 0,
"position": {
"x": 0,
"y": 0,
"z": 1.5
},
"rotation": {
"lengthAxis": "x",
"widthAxis": "y",
"heightAxis": "z"
},
"rotatedDimensions": {
"length": 18,
"width": 11,
"height": 4.5
}
}
],
"utilization": {
"volume": 18.2,
"weight": 18.9,
"efficiency": 18.2
},
"contentsWeight": 10.4,
"totalWeight": 11.3,
"billing": {
"actualWeight": 11.3,
"dimWeight": 42,
"billableWeight": 42
},
"visualization": {
"boxDimensions": {
"length": 20,
"width": 16,
"height": 18
},
"items": [
{
"id": "BOOK-001_0",
"name": "Hardcover Book",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"dimensions": {
"length": 9.5,
"width": 7.5,
"height": 1.5
},
"color": "#FF6B6B"
},
{
"id": "BOOK-001_1",
"name": "Hardcover Book",
"position": {
"x": 9.5,
"y": 0,
"z": 0
},
"dimensions": {
"length": 9.5,
"width": 7.5,
"height": 1.5
},
"color": "#4ECDC4"
},
{
"id": "LAPTOP-COMP_0",
"name": "Laptop Computer",
"position": {
"x": 0,
"y": 0,
"z": 1.5
},
"dimensions": {
"length": 18,
"width": 11,
"height": 4.5
},
"color": "#45B7D1"
}
]
}
}
],
"unpackedItems": [],
"summary": {
"totalShipments": 1,
"totalCost": 3.98,
"averageUtilization": 18.2,
"itemsSuccessfullyPacked": 3,
"itemsUnpacked": 0,
"totalBillableWeight": 42
},
"suggestions": [
"CartonPilot-Ultra algorithm: Enhanced with look-ahead and consolidation"
]
},
"metadata": {
"algorithm": "cartonpilot-ultra",
"executionTimeMs": 32,
"timestamp": "2025-12-06T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"tier": "BASIC"
}
}Ejemplo cURL
curl -X POST https://cartonpilot.com/api/v1/shipping-optimize \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"boxes": [
{
"id": "small-box",
"dimensions": { "length": 10, "width": 8, "height": 6 }
}
],
"items": [
{
"id": "item-1",
"dimensions": { "length": 4, "width": 3, "height": 2 },
"quantity": 1
}
],
"options": {
"objective": "fewest-parcels"
}
}'¡Pruébalo ahora!
Prueba la API de forma interactiva en nuestro explorador Swagger: cada endpoint, esquema de solicitud y ejemplo está disponible.
Abrir explorador de API