Tutorial 03

Order to label: fulfil an order end to end

Pick an open order, choose a carrier, request a label, poll for the PDF and mark the order fulfilled, including the retry trap that can buy you two labels.

intermediate ·15 min read ·PythonCLI ·Updated 3 Sep 2026

This is the path most integrations end up walking: find work, ship it, record it. Six calls, one of which is asynchronous and one of which you must never blindly retry.

1. Find open orders

GET /orders
curl -s "https://api.stockpilot.dev/orders?status=open&page_size=20" \
  -H "X-CLIENT-ID: $SP_CLIENT_ID" \
  -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"

Order statuses are open, pending, on-hold, completed and cancelled.

2. Read the whole order

The list gives you a summary. Before shipping, pull the full record with line items, addresses and channel:

GET /orders/get-single
curl -s "https://api.stockpilot.dev/orders/get-single?order_id=$ORDER_ID" \
  -H "X-CLIENT-ID: $SP_CLIENT_ID" \
  -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"

3. Pick a carrier

GET /shipping/integrations GET /shipping/label-templates

curl -s https://api.stockpilot.dev/shipping/integrations \
  -H "X-CLIENT-ID: $SP_CLIENT_ID" \
  -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"

If the organization has shipping rules configured, let Stockpilot choose instead of hardcoding a carrier:

GET /shipping/label-suggestion
curl -s "https://api.stockpilot.dev/shipping/label-suggestion?order_id=$ORDER_ID" \
  -H "X-CLIENT-ID: $SP_CLIENT_ID" \
  -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"

This respects the same rules the Stockpilot UI uses for weight bands, destination country and channel, so your integration stays consistent with what warehouse staff see.

4. Request the label

POST /shipping/request-label
curl -s -X POST https://api.stockpilot.dev/shipping/request-label \
  -H "X-CLIENT-ID: $SP_CLIENT_ID" \
  -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"order_pk": 55120, "service": "sendcloud"}'

This is asynchronous. It returns a handle, not a PDF:

{
  "status": "queued",
  "entity_id": "lbl_88fa2e",
  "order_pk": 55120,
  "service": "sendcloud"
}

request-label is not safe to retry. A retry can buy you a second label, and a second charge from the carrier. If the call times out or fails ambiguously, poll retrieve-label with the entity_id you have before sending it again. If you never got an entity_id, check the order’s shipping state in the app rather than firing blind.

It also lands in the heavy rate-limit bucket (20 requests per minute), because it reaches a carrier that has its own limits.

5. Poll for the PDF

POST /shipping/retrieve-label
import time

def retrieve_label(client, entity_id, order_pk, service, attempts=10):
    body = {"entity_id": entity_id, "order_pk": order_pk, "service": service}
    delay = 1.0
    for _ in range(attempts):
        resp = client.post("/shipping/retrieve-label", json=body)
        if resp.is_success:
            data = resp.json()
            if data.get("label"):           # PDF + tracking metadata
                return data
        time.sleep(delay)
        delay = min(delay * 1.7, 15.0)      # back off; this is a heavy route
    raise TimeoutError(f"label {entity_id} not ready")

The response carries the label PDF along with tracking metadata. Retrieval is safe to retry, because it reads state rather than creating it.

6. Mark the order fulfilled

POST /orders/fulfil
curl -s -X POST https://api.stockpilot.dev/orders/fulfil \
  -H "X-CLIENT-ID: $SP_CLIENT_ID" \
  -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"order_id": 55120}'

Confirm what was recorded with GET /orders/fulfillment .

The same flow from the terminal

Every step above has a CLI equivalent, which makes it a good way to explore before you write code:

stockpilot orders list --status open
stockpilot orders get 55120
stockpilot shipping integrations
stockpilot shipping label 55120 --integration sendcloud
stockpilot orders fulfil 55120

Add --json to any of them to pipe into jq. See the CLI reference.

Putting it together

def ship_open_orders(client, service="sendcloud", limit=20):
    orders = client.get("/orders", params={"status": "open", "page_size": limit}).json()

    for order in orders["results"]:
        pk = order["id"]
        queued = client.post("/shipping/request-label",
                             json={"order_pk": pk, "service": service}).json()

        label = retrieve_label(client, queued["entity_id"], pk, service)
        save_pdf(pk, label["label"])

        client.post("/orders/fulfil", json={"order_id": pk})
        print(f"shipped {pk}, tracking {label.get('tracking_number')}")

In production, wrap the per-order body in a try/except and record the entity_id before you do anything else with it. That single value is what makes a crashed run recoverable without double-buying labels.

A Stockpilot engineer talking through an integration with a developer

Stuck on something

Ask a person, not a search box

The API surface is wide, and some of it only makes sense once someone explains why it works that way. If a payload is not doing what you expect, or you are weighing two approaches, say so and we will look at it with you.