Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CartonPilot Python SDK

Official Python SDK for the CartonPilot 3D bin-packing / cartonization REST API.

The SDK is a thin pass-through client: requests are plain dicts shaped like the documented API body, and responses are the parsed JSON as dicts. Nothing is renamed or remodeled, so every API feature works without waiting for an SDK update.

Install

pip install cartonpilot

Requires Python >= 3.9 and requests >= 2.28. You will need an API key; the free plan needs no credit card.

Quickstart (single order)

from cartonpilot import CartonPilot

client = CartonPilot(api_key="sk_live_...")  # base_url defaults to https://cartonpilot.com

result = client.optimize({
    "boxes": [
        {
            "id": "small-box",
            "name": "Small Shipping Box",
            "dimensions": {"length": 10, "width": 8, "height": 6},
            "weightCapacity": 20,
            "weight": 0.4,   # tare weight of the empty box
            "cost": 5.99,
        },
        {
            "id": "medium-box",
            "name": "Medium Shipping Box",
            "dimensions": {"length": 14, "width": 12, "height": 10},
            "weightCapacity": 35,
            "cost": 8.99,
        },
    ],
    "items": [
        {
            "id": "item-001",
            "name": "Product A",
            "dimensions": {"length": 4, "width": 3, "height": 2},
            "weight": 1.5,
            "quantity": 2,
        },
    ],
    "options": {"objective": "fewest-parcels", "allowRotation": True},
})

for shipment in result["data"]["shipments"]:
    print(shipment["box"]["name"], "->", len(shipment["packedItems"]), "items")
print(result["data"]["summary"])       # totalShipments, totalCost, averageUtilization, ...
print(result["data"]["unpackedItems"]) # items that could not be packed, with reasons

Instead of inline boxes you can pass a saved box set: "boxSetKey": "bs_...". Items can also reference a saved item catalog by SKU — pass "catalogKey": "ic_..." and use {"sku": "WIDGET-1", "quantity": 2} entries in items.

Supported options.objective values: "fewest-parcels" (default), "lowest-cost", "lowest-billable-weight" (requires options.dimDivisor), "lowest-invoice-cost" (requires options.rateCardKey and a zone), "fastest".

Batch orders

Send up to 50 orders synchronously with orders instead of items:

result = client.optimize({
    "boxSetKey": "bs_abc123xyz456",
    "orders": [
        {"orderId": "ORD-001", "items": [{"id": "item-001", "dimensions": {"length": 4, "width": 3, "height": 2}, "weight": 1.5}]},
        {"orderId": "ORD-002", "items": [{"id": "item-002", "dimensions": {"length": 6, "width": 5, "height": 3}, "weight": 2.8}]},
    ],
    "options": {"objective": "fewest-parcels"},
})

for order in result["results"]:  # batch responses: {success, batch, results, summary, metadata}
    print(order["orderId"], "ok" if order["success"] else order.get("error"))
print(result["summary"])  # totalOrders, successfulOrders, totalBoxesUsed, totalCost, ...

Async batches + webhooks

Larger batches (up to 500 orders / 5000 items) run in the background with "async": True. The API returns 202 Accepted immediately with a job id. Note that async is a Python keyword, so it appears as a string dict key — the SDK passes your request through untouched.

accepted = client.optimize({
    "boxSetKey": "bs_abc123xyz456",
    "catalogKey": "ic_def789uvw012",
    "orders": [
        {"orderId": "ORD-001", "items": [{"sku": "WIDGET-1", "quantity": 2}]},
        {"orderId": "ORD-002", "items": [{"sku": "GADGET-7"}]},
    ],
    "async": True,
    "webhook": {  # optional: get notified instead of polling
        "url": "https://example.com/webhooks/cartonpilot",
        "secret": "whsec_my_signing_secret",
    },
    "options": {"objective": "lowest-billable-weight", "dimDivisor": 139},
})
print(accepted["jobId"], accepted["status"], accepted["statusUrl"])

Polling

job = client.wait_for_batch_job(accepted["jobId"], poll_interval=2.0, timeout=600.0)
if job["status"] == "completed":
    batch_result = job["result"]  # same shape as a sync batch response
else:
    print("Job failed:", job.get("error"))

wait_for_batch_job raises CartonPilotTimeoutError if the job hasn't reached completed or failed within timeout seconds. You can also call client.get_batch_job(job_id) for a single poll, or client.list_batch_jobs() for your 20 most recent jobs.

Webhook deliveries

When you include a webhook field, CartonPilot POSTs JSON to your URL on completion or failure:

{
  "event": "batch_job.completed",
  "jobId": "",
  "status": "completed",
  "statusUrl": "/api/v1/batch-jobs/…",
  "totalOrders": 2,
  "totalItems": 3,
  "summary": { "…": "" },
  "timestamp": "2026-07-26T12:00:00.000Z"
}

(event is "batch_job.failed" with an "error" field on failure.) Deliveries include X-CartonPilot-Event and X-CartonPilot-Job-Id headers, and — when you provided a secret — an X-CartonPilot-Signature: sha256=<hex> header (HMAC-SHA256 of the raw body). Always verify it:

# Flask
from flask import Flask, request, abort
from cartonpilot import verify_webhook_signature

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_my_signing_secret"

@app.post("/webhooks/cartonpilot")
def cartonpilot_webhook():
    signature = request.headers.get("X-CartonPilot-Signature", "")
    if not verify_webhook_signature(request.get_data(), signature, WEBHOOK_SECRET):
        abort(401)
    event = request.get_json()
    if event["event"] == "batch_job.completed":
        ...  # fetch full results via client.get_batch_job(event["jobId"])
    return "", 204
# FastAPI
from fastapi import FastAPI, Header, HTTPException, Request
from cartonpilot import verify_webhook_signature

app = FastAPI()
WEBHOOK_SECRET = "whsec_my_signing_secret"

@app.post("/webhooks/cartonpilot")
async def cartonpilot_webhook(request: Request, x_cartonpilot_signature: str = Header("")):
    raw_body = await request.body()
    if not verify_webhook_signature(raw_body, x_cartonpilot_signature, WEBHOOK_SECRET):
        raise HTTPException(status_code=401)
    event = await request.json()
    ...
    return {"ok": True}

Idempotency

Pass idempotency_key to safely retry optimize calls (e.g. after a network error). Replays of the same key + payload return the stored response without consuming quota; reusing a key with a different payload fails with code idempotency_conflict (HTTP 409). Keys expire after 24 hours.

result = client.optimize(request_body, idempotency_key="order-batch-2026-07-26-001")

Rate cards and invoice-cost optimization

Upload your negotiated carrier rates (zone × weight table, DIM divisor, overweight surcharge) via the /api/rate-cards endpoints, then optimize against your actual freight costs:

result = client.optimize({
    "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_ups2026ground",
        # For single orders (or as a batch-wide default) use "zone" here instead
        # of per-order zones.
    },
})

Each shipment then carries a billing block with actualWeight, dimWeight, billableWeight, and an invoice estimate (zone, baseRate, overweightFee, total, currency); the summary includes totalInvoiceCost and currency. With just options.dimDivisor (no rate card), you get billable weights and a summary totalBillableWeight.

Error handling

Non-2xx responses raise CartonPilotError carrying the structured error envelope:

from cartonpilot import CartonPilot, CartonPilotError, CartonPilotTimeoutError

try:
    result = client.optimize(request_body)
except CartonPilotError as err:
    print(err.status)      # HTTP status, e.g. 400
    print(err.code)        # machine code, e.g. "invalid_request", "quota_exceeded"
    print(err.message)     # human-readable message
    print(err.request_id)  # server request id for support
    if err.code == "invalid_request" and err.issues:
        for issue in err.issues:
            print(f"  {issue['path']}: {issue['message']}")
    print(err.body)        # full parsed error body (extra fields vary by code)

Error codes: invalid_request, authentication_failed, endpoint_not_allowed, algorithm_not_allowed, feature_not_available, limit_exceeded, quota_exceeded, idempotency_conflict, not_found, internal_error. CartonPilotTimeoutError (a CartonPilotError subclass) is raised only by wait_for_batch_job.

About

Official Python SDK for the CartonPilot 3D bin-packing and cartonization API.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages