Guide
Shipping and labels
Carriers, label templates including the virtual Bol.com and letter ones, the shipping rule suggestion, and the two step async label flow that must never be blindly retried.
Buying a label is the most expensive thing this API does. It reaches a carrier, it costs money, and it cannot be undone by a retry. Everything below is arranged around that.
Carriers
GET /shipping/integrations lists what the organization has connected. You need the carrier id from here to request anything.
Templates
GET /shipping/label-templates
returns the templates available, each with an id, a name and a carrier_identifier.
Some templates are virtual: they are generated per connected channel rather than configured by hand.
Bol.com VVB labels. If the organization has BolAPIConnect channels connected, each one produces two:
vvb_mailbox_<channel_id> Bol.com Mailbox Label
vvb_parcel_<channel_id> Bol.com Parcel Label
Take the Bol channel id from /shipping/integrations to pick the right one.
Letter labels. Always present, no channel needed:
letter_unstamped Unstamped Letter
letter_stamped Stamped Letter
You can filter rather than fetching everything:
curl -s "https://api.stockpilot.dev/shipping/label-templates?carrier=vvb_mailbox&id=42" \
-H "X-CLIENT-ID: $SP_CLIENT_ID" -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"
[
{ "id": "vvb_parcel_42", "name": "Bol.com Parcel Label", "carrier_identifier": "VVB_PARCEL" },
{ "id": "letter_unstamped", "name": "Unstamped Letter", "carrier_identifier": "LETTER_UNSTAMPED" }
]
Let the shipping rules choose
Most organizations already encode their carrier logic in Stockpilot’s shipping rules: weight bands, destination country, channel. Rather than reimplementing that in your integration, ask:
GET /shipping/label-suggestionIt returns the template and carrier the rules would pick for that order. Your integration then stays consistent with what warehouse staff see in the UI, and a rule change does not require a deploy on your side.
Requesting a label
POST /shipping/request-labelThis 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 second call buys a second label, and the carrier charges for it. Persist entity_id the moment you receive it, before doing anything else, so a crashed run can recover instead of re-requesting.
If a call times out and you never got an entity_id, check the order’s shipping state before sending anything again.
Retrieving the label
POST /shipping/retrieve-label
takes entity_id, order_pk and service, and returns the PDF with tracking metadata once the carrier has answered.
Retrieval is safe to retry: it reads the result of an earlier request rather than creating anything. Poll it with a backoff.
def retrieve(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):
r = client.post("/shipping/retrieve-label", json=body)
if r.is_success and r.json().get("label"):
return r.json()
time.sleep(delay)
delay = min(delay * 1.7, 15.0)
raise TimeoutError(entity_id)
Budget
All four shipping endpoints sit in the heavy rate limit bucket at 20 requests per minute, because each one reaches a carrier with its own limits. A tight polling loop on retrieve-label will exhaust that budget for the whole key, including the parts of your integration doing something else.
Labels are not fulfilment
Buying a label does not mark the order fulfilled, and fulfilling does not buy a label. Record the shipment separately with POST /orders/fulfil , passing the tracking code the label gave you. See orders and fulfilment.
