Book a shipment

Updated May 31, 20266 min read

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

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_account is set, so Carriyo runs your shipping rules to pick the carrier account. That's the recommended default.
  • partner_shipment_reference is your shipment id, required and unique across your tenant. It's the key you reconcile webhooks against, and reusing one returns a 400.
  • partner_order_reference is your order id, also required but not unique, several shipments can share one order. The example uses YOUR_ORDER_REF for the order and YOUR_ORDER_REF-1 for 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 be YOUR_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:

  • pendingbooked: the carrier accepted. The tracking number is attached and a label is generated. As pickup approaches the status moves on to ready_to_ship, then the carrier's own tracking events take over. You learn of this on a webhook, see Step 3.
  • pendingerror: 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:

FieldRequiredNotes
merchantyesYour Carriyo merchant ID (uppercase). Scopes carrier accounts.
references.partner_order_referenceyesYour order id; ties the shipment to your order. Need not be unique, several shipments can share one.
references.partner_shipment_referenceyesYour shipment id; the key for webhook reconciliation. Unique across your tenant, reuse returns 400.
paymentyestotal_amount and currency. Defaults to PRE_PAID; cash-on-delivery is covered in a separate recipe.
pickupyespartner_location_code (your friendly location code) for a pre-configured location, or full address fields.
dropoffyesFull delivery address: contact_name, contact_phone, address1, city, country. For US, CA, PH, AU, state is required.
parcels[]yesAt 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_reference is mandatory, tenant-unique, and not reusable. Omit it and the shipment isn't created at all; reuse one and the call returns 400 (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 200 means submitted, not booked. The create call returns pending; the carrier can still reject it. Don't mark the shipment confirmed, or print from it, until the booked webhook 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.