Book a shipment
Create a shipment and submit it to the carrier in a single API call.
Booking is asynchronous: the call returns immediately with status
pending, and the carrier's outcome, booked (tracking number and
label) or error (rejected), arrives moments later by webhook. Most
integrations start here.
Scenario
You have an order ready to fulfill, pickup location, delivery address, parcels, items, and you want a carrier label so the warehouse can pack and ship.
If you instead need to create the shipment first and book it later (to edit it in between), see Draft then confirm.
If you use Carriyo Orders
This recipe books a shipment directly, the path for clients not using the Carriyo Order object. If you do use Carriyo Orders, you don't book shipments yourself: fulfilling a fulfillment order creates and books the shipment for you.
Prerequisites
- API credentials: see Getting started.
- At least one active Carrier account.
- Configured shipping rules so Carriyo selects the carrier account for each shipment.
- To receive the booking outcome (Step 3), a webhook pointed at your endpoint. Set this up once in the Dashboard, see Configure a webhook.
Step 1, create the shipment
draft=false (or omitting it) creates the shipment and submits the
booking in one call.
curl -X POST 'https://api.carriyo.com/shipments' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'tenant-id: YOUR_TENANT_ID' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"merchant": "ACME",
"references": {
"partner_order_reference": "YOUR_ORDER_REF",
"partner_shipment_reference": "YOUR_ORDER_REF-1"
},
"pickup": {
"partner_location_code": "NJ-WH-01"
},
"dropoff": {
"address1": "350 5th Avenue",
"city": "New York",
"state": "NY",
"country": "US",
"postcode": "10118",
"contact_name": "Alex Chen",
"contact_phone": "+12125550100",
"contact_email": "alex@example.com"
},
"parcels": [
{
"weight": { "value": 1.5, "unit": "kg" },
"dimension": { "width": 20, "height": 15, "depth": 30, "unit": "cm" }
}
],
"items": [
{
"sku": "WIDGET-RED-M",
"quantity": 2,
"description": "Red widget, medium",
"price": { "amount": 100, "currency": "USD" }
}
],
"payment": {
"total_amount": 250,
"currency": "USD"
}
}'
A few things about this payload:
- No
carrier_accountis set, so Carriyo runs your shipping rules to pick the carrier account. That's the recommended default. partner_shipment_referenceis your shipment id, required and unique across your tenant. It's the key you reconcile webhooks against, and reusing one returns a400.partner_order_referenceis your order id, also required but not unique, several shipments can share one order. The example usesYOUR_ORDER_REFfor the order andYOUR_ORDER_REF-1for the shipment. Deriving the shipment reference from the order reference plus a per-shipment suffix keeps each one unique while showing which order it belongs to; a second shipment for the same order would beYOUR_ORDER_REF-2.
Step 2, read the response
The call returns 200 with the shipment in pending status.
Carriyo has validated the payload, assigned a carrier account, and
submitted the booking request, but it does not wait for the
carrier to accept. There's no tracking number or label yet. Both
arrive once the carrier accepts and the status moves to booked.
{
"shipment_id": "MPPF4Y76ACNAH9",
"references": {
"partner_order_reference": "YOUR_ORDER_REF",
"partner_shipment_reference": "YOUR_ORDER_REF-1"
},
"carrier_account": {
"carrier": "UPS",
"carrier_id": "c1053e4b-f2e7-4cee-917f-a390e73887a6",
"carrier_account_name": "UPS US"
},
"post_shipping_info": {
"status": "pending",
"key_milestones": {}
},
"...": "(other fields elided)"
}
Booking then resolves asynchronously, in one of two directions:
pending→booked: the carrier accepted. The tracking number is attached and a label is generated. As pickup approaches the status moves on toready_to_ship, then the carrier's own tracking events take over. You learn of this on a webhook, see Step 3.pending→error: booking failed. See Handling failures.
Step 3, receive the booking outcome by webhook
With the webhook configured (see Prerequisites), Carriyo POSTs the
full Shipment object to your endpoint as the booking resolves, no
polling required. For warehouse label printing, the trigger you want
is Default Label Generated (default_label_update): it fires once
the shipment is booked and the label is ready to pull.
The request your endpoint receives on success:
POST /your-webhook-endpoint
content-type: application/json
event-id: F985CFE1A00B6805929501E1395984BD
trigger: default_label_update
webhook-id: ACCOUNT_58137df6-d609-4922-8a05-d86ea8fdbb0e
{
"shipment_id": "MPPF4Y76ACNAH9",
"merchant": "ACME",
"references": {
"partner_order_reference": "YOUR_ORDER_REF",
"partner_shipment_reference": "YOUR_ORDER_REF-1"
},
"carrier_account": {
"carrier": "UPS",
"carrier_id": "c1053e4b-f2e7-4cee-917f-a390e73887a6",
"carrier_account_name": "UPS US"
},
"post_shipping_info": {
"status": "booked",
"tracking_no": "1Z999AA10123456784",
"default_label_url": "https://documents.carriyo.com/redirect/ACME/UPS/SHIPMENT/MPPF4Y76ACNAH9.pdf?expiry=...&token=...",
"carriyo_zpl_label_url": "https://documents.carriyo.com/label?format=zpl&shipmentId=MPPF4Y76ACNAH9&token=...",
"documents": [
{ "name": "Commercial Invoice", "type": "commercial_invoice", "format": "pdf", "url": "https://documents.carriyo.com/.../commercial_invoice/...?token=..." },
{ "name": "Packing List", "type": "packing_list", "format": "pdf", "url": "https://documents.carriyo.com/.../packing_list/...?token=..." }
],
"key_milestones": { "booked": "2026-05-27T12:38:00.250Z" },
"async_statuses": { "label": "complete" }
},
"...": "(full Shipment object, other fields elided)"
}
Read post_shipping_info.status (booked), pull the label from
default_label_url, match the shipment by
references.partner_shipment_reference, and respond 2xx.
default_label_url resolves to whichever label is nominated as the
default for that carrier account, carrier-provided or
Carriyo-generated, PDF or ZPL, so for most integrations it's the only
label URL you need. If you specifically want one format or source, use
the explicit variant alongside it (carrier_pdf_label_url,
carriyo_pdf_label_url, carriyo_zpl_label_url).
See Platform → Webhooks for the delivery,
authentication, and retry contract. The same mechanism delivers later
tracking statuses (shipped, out_for_delivery, delivered, …); see
Track shipment status for
the handler-side pattern.
Handling failures
A booking can fail in two places, and error_details[].source tells
you which: CARRIYO means Carriyo's own validation rejected the
shipment; CARRIER means a carrier was assigned but its API
rejected the booking. Either way the shipment lands in
status: "error" with no label, and every reason is listed in
post_shipping_info.error_details. The error webhook (trigger error)
lets you record the failure and alert your team.
Carriyo validation (source: CARRIYO). The create call returns
error immediately instead of pending. One common cause is that no
carrier account could be assigned, no carrier was supplied and no
shipping rule matched, which leaves carrier_account empty:
{
"shipment_id": "MPPF4Y76ACNAH9",
"merchant": "ACME",
"references": {
"partner_order_reference": "YOUR_ORDER_REF",
"partner_shipment_reference": "YOUR_ORDER_REF-1"
},
"carrier_account": {},
"post_shipping_info": {
"status": "error",
"key_milestones": {},
"error_details": [
{
"level": "ERROR",
"trigger": "BOOKING",
"source": "CARRIYO",
"type": "VALIDATION",
"code": "no_carrier_assigned",
"field": "carrier",
"message": "No carrier assigned. Please provide a valid carrier and reprocess booking again."
}
]
},
"...": "(other fields elided)"
}
Other Carriyo validations fail with a carrier already assigned, for
example a missing pickup or dropoff field, so carrier_account can be
populated and the shipment still errors. Read error_details for what
to fix rather than inferring the cause from carrier_account.
Carrier rejection (source: CARRIER). A carrier was assigned but
rejects the booking. The error arrives on the webhook with the
populated carrier_account and the carrier's own message passed
through largely as-is:
POST /your-webhook-endpoint
content-type: application/json
event-id: 9F1C0A77E2B4488D90AABE6633127C04
trigger: error
webhook-id: ACCOUNT_e213db9e-3070-434e-8419-0df1198597e9
{
"shipment_id": "MPPF4Y76ACNAH9",
"merchant": "ACME",
"references": {
"partner_order_reference": "YOUR_ORDER_REF",
"partner_shipment_reference": "YOUR_ORDER_REF-1"
},
"carrier_account": {
"carrier": "UPS",
"carrier_id": "c1053e4b-f2e7-4cee-917f-a390e73887a6",
"carrier_account_name": "UPS US"
},
"post_shipping_info": {
"status": "error",
"error_details": [
{
"level": "ERROR",
"trigger": "BOOKING",
"source": "CARRIER",
"type": "VALIDATION",
"message": "{\"detail\":\"420505: The destination location is invalid. Please check the data\",\"title\":\"Bad request\",\"status\":\"400\"}"
}
],
"key_milestones": {}
},
"...": "(full Shipment object, other fields elided)"
}
Resolving errors is usually easiest in the Dashboard. An operator
sees the error, corrects the data or switches the carrier, and
reprocesses in a few clicks, no parsing carrier messages or rebuilding
payloads. See
Resolve shipment errors.
To re-book programmatically instead, the
POST /shipments/{id}/reprocess endpoint switches the carrier account
and/or amends shipment details in one call, see
Fix or reassign a shipment.
Field reference
Because the shipment is submitted for booking immediately, all of these must be present at create time:
| Field | Required | Notes |
|---|---|---|
merchant | yes | Your Carriyo merchant ID (uppercase). Scopes carrier accounts. |
references.partner_order_reference | yes | Your order id; ties the shipment to your order. Need not be unique, several shipments can share one. |
references.partner_shipment_reference | yes | Your shipment id; the key for webhook reconciliation. Unique across your tenant, reuse returns 400. |
payment | yes | total_amount and currency. Defaults to PRE_PAID; cash-on-delivery is covered in a separate recipe. |
pickup | yes | partner_location_code (your friendly location code) for a pre-configured location, or full address fields. |
dropoff | yes | Full delivery address: contact_name, contact_phone, address1, city, country. For US, CA, PH, AU, state is required. |
parcels[] | yes | At least one parcel with weight ({ value, unit }). dimension ({ width, height, depth, unit }) strongly recommended, carriers use it for volumetric weight. |
items[] | yes (most carriers) | At least one item with description, sku, quantity, and price ({ amount, currency }). Feeds label descriptions and customs (cross-border). |
Pitfalls
partner_shipment_referenceis mandatory, tenant-unique, and not reusable. Omit it and the shipment isn't created at all; reuse one and the call returns400(it won't update or return the existing shipment). Derive it from something stable on your side. To edit before booking, use Draft then confirm.- A
200means submitted, not booked. The create call returnspending; the carrier can still reject it. Don't mark the shipment confirmed, or print from it, until thebookedwebhook arrives (Step 3). - Label URLs expire.
default_label_url(and the carrier- and Carriyo-specific URLs alongside it) are signed and expire after a configurable duration set on the account. Either save the PDF to your own storage, or, if you'd rather not, raise the label-expiry duration to match your workflow.