# Ship an order

> Create a shipment for a sales order, add its lines, get carrier rates, and buy a label through GoShippo (which spends money) or record a label bought elsewhere.

Web page: https://developer.yoprint.com/guides/shipping/

**Read first:** [Shipping](https://developer.yoprint.com/how-yoprint-works/shipping.md), for shipments, labels and the packages they make.

> **Danger: Buying a label spends money**
>
> Buying a GoShippo label buys postage with the GoShippo account your team connected, the moment the request succeeds. The API has no confirmation step and no test mode, and permission to view the order is enough to buy: roles that can't edit orders can still buy labels. Have a person approve the carrier, service and price before your code buys a label.

A **shipment** sends some or all of a sales order's lines to one address. Buying or recording a **label** makes its **packages**, one per parcel. You can buy labels through GoShippo, if your team has connected it, or record labels you bought elsewhere.

**Diagram:** Step 4 spends money: have a person approve the carrier, service and price first.

- **1. Create a shipment**: For one address on a sales order
- **2. Add the lines to ship**: Some or all of the order's lines
- **3. Get rates**: Live GoShippo rates for each carrier and service
- **4. Buy or record the label**: Buying spends your team's GoShippo money. Either way, the label makes the packages
- **5. Print the labels**: Download them as PDFs
- **6. Track and finish**: Follow the tracking and finish the shipment

## 1. Create a shipment

`POST /v2/api/store/{team_slug}/v2/sales_order/{unique_key}/shipment` [Create a shipment for a sales order](https://developer.yoprint.com/reference/shipments/v2-sales-order-shipment-create.md)

```bash
curl https://secure.yoprint.com/v2/api/store/your-team/v2/sales_order/V4keXwwxMQyXw7/shipment \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "X-Yoprint-Decimal-Format: exact-v1" \
  -H "Content-Type: application/json" \
  -d '{
    "operation_key": "order-1001-shipment-1",
    "shipping_address_id": "cwwkmbgJK3eE5A",
    "delivery_notes": "Leave at the loading dock"
  }'
```

- `shipping_address_id` is one of the customer's shipping addresses.
- `operation_key` is optional and makes this create safe to retry: sending the same key with the same body returns the shipment it already made, instead of a second one. The same key with a different body is refused.

To list an order's shipments, use the order's `id` (not its unique key):

`GET /v2/api/store/{team_slug}/v2/sales_order/{unique_key}/shipment` [List the shipments of a sales order](https://developer.yoprint.com/reference/shipments/v2-sales-order-shipment-index-for-order.md)

## 2. Add the lines to ship

A shipment line points at an order line, by the order line's `id`, with how many go in this shipment. Here, 24 of an order line:

`POST /v2/api/store/{team_slug}/v2/shipment/{unique_key}/item` [Add a line to a shipment](https://developer.yoprint.com/reference/shipment-line-items/v2-shipment-item-create.md)

```json
{
  "product": { "type": "order_item", "properties": { "id": "ZJTJ2uF5dMiArL" } },
  "quantity": { "type": "unit", "total": { "value": 24, "dp": 0 } },
  "position": 100
}
```

- The path's `{unique_key}` is the shipment's `id`, from step 1's answer.
- The rest of the line ships later, in another shipment.

To add, change or remove several lines at once:

`POST /v2/api/store/{team_slug}/v2/shipment/{unique_key}/item/bulk` [Add, change and remove shipment lines at once](https://developer.yoprint.com/reference/shipment-line-items/v2-shipment-item-bulk.md)

## 3. Get rates

Ask GoShippo for rates. This is free: nothing is bought, and nothing is saved in YoPrint.

`POST /v2/api/store/{team_slug}/v2/shipment_rate/goshippo` [Get shipping rates](https://developer.yoprint.com/reference/shipping-rates-and-estimates/v2-shipment-rate-goshippo.md)

The rate request isn't tied to the YoPrint shipment. You send both addresses and the parcels yourself, YoPrint doesn't compare them with the shipment, and the label is bought for exactly what you send here.

**Diagram:** You build each part from the shipment's records, the way the web app does.

1. **address_from**: The brand profile's Ship From, or your company location, with its own name and the team's email and phone +
2. **address_to**: The shipment's Ship To address, with the receiver contact's name, email and phone +
3. **parcels**: One per box: dimensions, weight and units =
4. **Rates**: Each rate's `object_id` is what you buy in step 4

Where each part comes from:

| Part           | Read                                                                                                                            | Take                                                                                                                                                                                    |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address_to`   | The shipment's `shipping_address_id` (a customer address) and `receiver_contact_id`                                             | The street, city, state, postcode and country from the address. `email` and `phone` from the contact. `name` from the contact, or else the address's label                              |
| `address_from` | The shipment's `customer_profile_id`, the brand profile, when it's set. Otherwise your company location and the team's settings | The profile's Ship From address, company name, email and phone. Or the location's own address and name, with the team's email and phone (locations have no email or phone of their own) |
| `parcels`      | Nothing: they're your boxes                                                                                                     | One entry per box: its dimensions, weight and units                                                                                                                                     |

The shipment names these records by id but doesn't include them, so read it first, then each record:

**The reads, and YoPrint's address fields in Shippo's names**

`GET /v2/api/store/{team_slug}/v2/shipment/{unique_key}` [Get a shipment](https://developer.yoprint.com/reference/shipments/v2-shipment-show.md)

`GET /v1/api/store/{team_slug}/customer/{hash_id}/address/{id}` [Get a customer address](https://developer.yoprint.com/reference/customer-addresses/v1-customer-address-show.md)

`GET /v1/api/store/{team_slug}/contact/{id}` [Get a contact by id](https://developer.yoprint.com/reference/contacts/v1-contact-show.md)

`GET /v1/api/store/{team_slug}/customer/{hash_id}/profile/{id}` [Get a brand profile](https://developer.yoprint.com/reference/customer-brand-profiles/v1-customer-profile-show.md)

`GET /v1/api/store/{team_slug}/setting/location` [List company locations](https://developer.yoprint.com/reference/company-locations/v1-location-index.md)

`GET /v1/api/store/{team_slug}/setting/detail` [Get your company details](https://developer.yoprint.com/reference/company-settings/v1-setting-detail-show.md)

| YoPrint address                         | Shippo address                    |
| --------------------------------------- | --------------------------------- |
| `address1`                              | `street1`                         |
| `address2`                              | `street2`                         |
| `city`                                  | `city`                            |
| `state_code`                            | `state`, as a code such as `TX`   |
| `postcode`                              | `zip`                             |
| `country_code`                          | `country`, as a code such as `US` |
| From the contact, brand profile or team | `name`, `phone`, `email`          |

Then ask for rates:

```bash
curl https://secure.yoprint.com/v2/api/store/your-team/v2/shipment_rate/goshippo \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "X-Yoprint-Decimal-Format: exact-v1" \
  -d '{
    "address_from": { "name": "Northwind Apparel", "street1": "100 Example Street", "city": "Austin", "state": "TX", "zip": "78701", "country": "US", "phone": "+1 201 555 0123" },
    "address_to": { "name": "Alex Rivera", "street1": "200 Example Street", "city": "Denver", "state": "CO", "zip": "80202", "country": "US", "email": "alex@example.com" },
    "parcels": [{ "length": 12, "width": 10, "height": 4, "distance_unit": "in", "weight": 2, "mass_unit": "lb" }]
  }'
```

- Both addresses need `name`, `street1`, `city`, `state`, `zip` and `country`.
- Options you want on the label, such as insurance or a signature, go in `extra` here. The purchase can't add them later.
- The answer is Shippo's own shipment, with status `201` and without YoPrint's usual `status`, `success` and `data` wrapper. Each entry in `rates` has an `object_id`: that's the rate id you buy in step 4.
- A rate's `amount` is a decimal string, such as `"12.40"`, in the rate's `currency` (Shippo's own format: parse it as a decimal). See [Money and quantities](https://developer.yoprint.com/concepts/money-and-quantities.md).
- A `201` can still have no rates. Check `status` and `messages`.

Rates are quotes. Buy soon after you get them. If a purchase fails because a rate is no longer valid, get rates again.

To save the rates on the YoPrint shipment, for a person to choose from later:

`POST /v2/api/store/{team_slug}/v2/shipment/{hash_id}/shipment_estimate/goshippo` [Save a shipping estimate](https://developer.yoprint.com/reference/shipping-rates-and-estimates/v2-shipment-estimate-create-goshippo.md)

`GET /v2/api/store/{team_slug}/v2/shipment/{hash_id}/estimates` [List the shipping estimates of a shipment](https://developer.yoprint.com/reference/shipping-rates-and-estimates/v2-shipment-estimate-index.md)

## 4. Buy the label

The API has no approval step: the approval happens in your app. Show a person the rates (carrier, service and price), and buy only the one they choose. Before you buy, check:

1. The rate's `address_from`, `address_to` and `parcels` match the YoPrint shipment, its Ship To and its brand profile or location.
2. A person approved the carrier, the service and the price.
3. The order is a published sales order, your team has connected GoShippo, and your role can view the order (all orders, or your own and assigned ones).

`POST /v2/api/store/{team_slug}/v2/shipment/{hash_id}/label/goshippo` [Buy a shipping label (charges your Shippo account)](https://developer.yoprint.com/reference/shipping-labels/v2-shipment-label-create-goshippo.md)

```bash
curl https://secure.yoprint.com/v2/api/store/your-team/v2/shipment/2Q4hJk2S8JjRQ6/label/goshippo \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "X-Yoprint-Decimal-Format: exact-v1" \
  -d '{ "rate_id": "RATE_OBJECT_ID_FROM_STEP_3", "create_shipping_cost_line_item": true }'
```

`create_shipping_cost_line_item: true` adds a "Shipping Cost" line to the shipment, priced at the label's cost, so you can bill it. On an order with Auto Invoice, the line also reaches the invoice. Its price is a decimal pair, like every order line. Leave the field out and no line is added. The answer's `shipping_cost_line_item.status` says what happened: `created`, `existing`, `not_requested`, `skipped_third_party_billing` (a third party pays for the label), or `failed`. A `failed` line doesn't undo the purchase: the label is bought, so add the line yourself instead of buying again.

YoPrint refuses the purchase when:

- The shipment belongs to a **quote** ("Shipping labels can't be bought for a quote. Convert it to a sales order first.").
- The order isn't **published**.
- The team hasn't **connected GoShippo** (`422` with `goshippo_not_enabled`).
- You sent a `shipment_estimate_id` that isn't one of this shipment's estimates.
- Your role can't view the order (`403`, `user_is_not_authorized`).

The answer lists the new packages, one per parcel. The first is the master package. Each package has the label, and its cost in `charges`, in thousandths: `12400` is 12.40 (see [Money and quantities](https://developer.yoprint.com/concepts/money-and-quantities.md)). Shippo quotes one price for all the parcels together, so every package of a multi-parcel shipment carries that same total. Read the master package's `charges`. Don't add the packages up.

> **Caution: Don't retry blindly**
>
> A retry can buy a second label.
>
> - YoPrint's only guard is a **10-second** lock per rate: a second purchase of the same rate within 10 seconds of the first one **starting** is refused with `422`. The lock is cleared at once when a purchase succeeds, and kept for the rest of its 10 seconds after a failure. It also expires after 10 seconds while the first purchase is still running, and one purchase can take longer than that.
> - After that, the retry goes to Shippo. Shippo refuses a used rate for several parcels ("Shippo has already used this rate…"), but it may sell a single-parcel rate again.
> - A **new** rate always buys a new label.
>
> If a purchase times out or answers `500`, a label may have been bought. Read the shipment's packages, and check your Shippo account, before you do anything else:
>
> `GET /v2/api/store/{team_slug}/v2/shipment/{hash_id}/packages` [List the packages of a shipment](https://developer.yoprint.com/reference/shipment-packages/v2-shipment-package-index.md)

## 5. Print the labels

Merge the labels of up to 40 packages into one PDF to print. Take the packages' ids from the shipment's package list:

`GET /v2/api/store/{team_slug}/v2/shipment/{hash_id}/packages` [List the packages of a shipment](https://developer.yoprint.com/reference/shipment-packages/v2-shipment-package-index.md)

`POST /v2/api/store/{team_slug}/v2/shipment_package/labels/compile` [Download labels as one PDF](https://developer.yoprint.com/reference/shipping-labels/v2-shipment-label-compile-goshippo.md)

```json
{ "shipment_package_ids": ["o6foWpSAayc79D", "UDsG6yjZAVrRNv"] }
```

The answer is the PDF itself. Packages without a GoShippo label are skipped.

## Or record a label bought elsewhere

If you bought postage outside YoPrint, record the label on the shipment. This doesn't spend anything through YoPrint:

`POST /v2/api/store/{team_slug}/v2/shipment/{hash_id}/label/external` [Record a label bought elsewhere](https://developer.yoprint.com/reference/shipping-labels/v2-shipment-label-create-external.md)

## 6. Track and finish

`PATCH /v1/api/store/{team_slug}/shipment_package/{id}/track` [Get live tracking for a package](https://developer.yoprint.com/reference/shipment-packages/v1-shipment-package-track.md)

Move the shipment through its statuses as it ships:

`PUT /v2/api/store/{team_slug}/v2/sales_order/{hash_id}/shipment/{id}/status` [Change the status of a shipment](https://developer.yoprint.com/reference/shipments/v2-sales-order-shipment-change-status.md)

`GET /v1/api/store/{team_slug}/setting/status/shipment` [List shipment statuses](https://developer.yoprint.com/reference/statuses/v1-status-list-shipment.md)

A status change fires `shipment.status_changed`. On an order synced from Shopify, moving to the final status can fulfil the order in Shopify.

## Cancel a label

Deleting a package asks Shippo to refund its label, in the background. Like buying, it needs permission to view the order, and so does the bulk package operation:

`DELETE /v2/api/store/{team_slug}/v2/shipment_package/{hash_id}` [Delete a package (voids its label)](https://developer.yoprint.com/reference/shipment-packages/v2-shipment-package-destroy.md)

`POST /v2/api/store/{team_slug}/v2/shipment/{unique_key}/package/bulk` [Change and delete packages at once](https://developer.yoprint.com/reference/shipment-packages/v2-shipment-package-bulk.md)

Deleting the whole shipment needs permission to edit the order:

`DELETE /v2/api/store/{team_slug}/v2/shipment/{unique_key}` [Delete a shipment](https://developer.yoprint.com/reference/shipments/v2-shipment-destroy.md)

Shippo and the carrier can refuse the refund, and YoPrint doesn't report a refused refund back. Check your Shippo account.

## Pitfalls

- **Labels cost money, and there's no undo in the API.** Confirm with a person before you buy.
- **Package `charges` repeat the multi-parcel total.** On a multi-parcel label, each package's `charges` shows the full rate cost in thousandths (`12400` is 12.40), not a fraction of it. Read the master package. Don't sum the packages. See [Money and quantities](https://developer.yoprint.com/concepts/money-and-quantities.md).
- **No GoShippo labels on quotes or drafts.** Convert the quote and publish the order first. A label bought elsewhere can be recorded on a draft sales order, but not on a quote.
- **Retries can buy twice.** YoPrint's guard lasts only 10 seconds from the first attempt's start. Wait for each purchase's answer, however long it takes, and read the packages before you retry, and never buy a fresh rate to "retry".
- **Rates use the addresses you type.** Build them from the shipment and your location, and compare before you buy.
- **Seeing the order is enough to buy.** Any role that can view the order can buy its labels, even without edit rights. Limit who holds keys.
- **Deleting a package only asks for a refund.** It may be refused.
- **List shipments by the order's `id`.** With the order's unique key, the list comes back empty.
- **Use `operation_key` on shipment creates** so a retry doesn't make a second shipment.
