# Buy a shipping label (charges your Shippo account)

```http
POST https://secure.yoprint.com/v2/api/store/{team_slug}/v2/shipment/{hash_id}/label/goshippo
```

- Spends money: see Side effects.
- API version: v2
- Operation ID: `v2_public_shipment_label_create_goshippo`
- Section: Shipping › [Shipping labels](https://developer.yoprint.com/reference/shipping-labels.md)
- Web page: https://developer.yoprint.com/reference/shipping-labels/v2-shipment-label-create-goshippo/
- Authentication: `Authorization: Bearer YOUR_API_KEY`

**This spends real money.** Each successful call buys a label from the carrier and Shippo charges your team's Shippo account. The API can't undo a purchase: deleting the package later only asks Shippo for a refund, which Shippo and the carrier may refuse, and a refused refund is not reported back. Anyone whose role can view the order can buy: edit rights are not needed.

Buys the label for a rate from Get shipping rates and records it on a YoPrint shipment. Send the rate's `object_id` as `rate_id`: the addresses, parcels, service and options all come from the rate. The label file type is your Shippo integration's label size setting (4x6 PDF unless changed).

To bill the label's cost to the customer, send `create_shipping_cost_line_item: true`: after the purchase YoPrint adds one "Shipping Cost" line to the shipment, linked to the master package, for the label's cost (on an Auto Invoice order it then reaches the managed invoice as a shipping fee). Without it nothing is added, as before, and you can add the line yourself with Add a line to a shipment.

Returns the new packages: one per parcel of the rate, the first one the master, each with its tracking number, cost (`charges`, in thousandths, repeated on every package of a multi-parcel label) and a `pdf` entry whose `url` is Shippo's signed link to the label file: don't publish it. Next to `data`, `shipping_cost_line_item` says what happened to the shipping cost line.

Before you call: the shipment must belong to a published sales order (not a quote), and you should have shown the rate and its price to whoever approves the spend.

**Retries:** Never retry a label purchase automatically, whatever the error. Follow these steps instead:

1. **Check if the label was already bought.** List the shipment's packages (`GET /v2/shipment/{shipment_id}/package`). If a package with that carrier and service is present, the label was bought. Do not retry.
2. **Check your Shippo account.** If no package appears in YoPrint, a purchase might still have succeeded: when YoPrint buys a label in Shippo but encounters an error reading back or saving it, it returns HTTP 500 and queues a background refund request to Shippo. Check your Shippo dashboard to see whether a label or refund was created.
3. **When to stop (do not reuse the rate).** Never repeat the purchase call with the same `rate_id`. YoPrint holds only a temporary 10-second lock on `rate_id`. Once cleared or expired, repeating the call sends a new purchase to Shippo, risking a duplicate charge (or 422 `label_error` for multi-parcel rates).
4. **When to retry safely.** If both YoPrint and Shippo confirm no label was purchased, request fresh shipping rates (`POST /v2/shipment/{shipment_id}/goshippo-rates`), choose a rate, and buy using the newly issued `rate_id`.

To avoid buying twice: send one purchase per shipment at a time and wait for its answer, however long it takes (give the call a client timeout of at least 120 seconds: a single-parcel purchase can take up to about 42 seconds and a multi-parcel one about 90); never resend while it is pending; after any error or timeout, list the shipment's packages and check your Shippo account before anything else; and if you do buy again, get new rates first.

Before you buy:

1. Build the rate request from the shipment, its Ship To and its brand profile or company location (see Get shipping rates), and check the rate's `address_from`, `address_to` and `parcels` match the shipment.
2. Show the rate's carrier, service and `amount` to a person and get their approval. AI agents must always ask.
3. Check the shipment has no package for this rate yet.
4. Buy soon after rating. YoPrint doesn't record how long Shippo keeps a rate buyable; if the purchase is refused, rate again.

Money here: a rate's `amount` is a decimal string in `currency` (`"12.40"`); parse it as a decimal. After purchase, each package's `charges` is that amount cut to 3 decimal places, in thousandths, an older encoding (`12400` is 12.40; usually a whole number of thousandths; a finer value comes back as a fractional number, or as an exact decimal string with `X-Yoprint-Decimal-Format: exact-v1`). On a multi-parcel label, every package repeats the rate's full amount: read the master package's value and don't add the packages up. See [Money and quantities](/concepts/money-and-quantities/).

The order of calls:

1. Optional: List Shippo carrier accounts, to limit rates to some of your accounts.
2. Get shipping rates with the addresses and parcels. Note the top-level `object_id` (the Shippo shipment) and the chosen rate's `object_id` (`rates[].object_id`).
3. Optional: Save a shipping estimate on the YoPrint shipment, to keep the quote.
4. Buy a shipping label with the rate's `object_id` as `rate_id`. This is the only step that costs money.
5. Print: Download labels as one PDF. Track: Get live tracking for a package.

## Permission

Needs permission to view the shipment's order: the team owner, a role whose Manage Sales Orders permission is "view all", or "view assigned" when you created the order or are assigned to it. Otherwise 403 `user_is_not_authorized`. Edit rights are not needed, so a role that can only view orders can do this too. Limit who holds API keys. Needs a connected, enabled Shippo integration (Settings > Integrations > Shippo Shipping). Without one the reply is 422 `goshippo_not_enabled` with no message.

## Business rules

- Only for sales orders: a quote's shipment gets 422 "Shipping labels can't be bought for a quote. Convert it to a sales order first."
- The sales order must be published: otherwise 422 "No Permission - you cannot purchase goShippo label in an unpublished order/quote".
- FedEx allows at most 12 parcels per shipment: 422 `goshippo_fedex_package_limit_exceeded`.
- `shipment_estimate_id`, when sent, must be an estimate of this shipment: otherwise 422 `validation_error` "The shipping estimate does not belong to this shipment.", checked before anything is bought.
- The shipping cost line (with `create_shipping_cost_line_item: true`) is added once per label: its cost is the master package's `charges` (quantity 1, pricing mode none), and a second request for the same package returns the line already there. It is left out when the rate bills a third party (Shippo `extra.billing.type` set to anything other than `SENDER`), so the customer isn't charged twice.
- A rate is not guaranteed to be bought only once. YoPrint holds a 10-second lock per `rate_id` from the start of a purchase (cleared at once on success; after a failure it stays until 10 seconds after the start); a purchase can run longer than 10 seconds, because each of its Shippo calls may take up to 14. A repeat outside the lock goes to Shippo, which refuses a used multi-parcel rate (`label_error`); YoPrint has no record of what Shippo does with a reused single-parcel rate, so treat any repeat as a possible second charge.
- On a shipment linked to Shopify: 422 `shopify_managed_shipment_locked`, `shopify_fulfilled_shipment_package_locked` ("This shipment has already been fulfilled to Shopify. Void fulfillment first to edit packages or create labels.") or `shopify_broken_link_shipment_locked`; 422 "Shipment label cannot be created because shipment contains items fulfilled by a third-party."; and 422 when the carrier differs from the carrier of the shipment's other packages (Shopify needs one carrier per fulfillment).

## Side effects

- **Spends money.** SPENDS REAL MONEY: buys the label from Shippo, charged to your team's Shippo account. It can't be undone through the API.
- If YoPrint buys the label in Shippo but fails while reading back or saving it, the request returns HTTP 500 and YoPrint queues a background refund request to Shippo for that label (the refund's outcome is not reported).
- Creates one package per parcel on the shipment, with tracking number, carrier, service and cost.
- With `create_shipping_cost_line_item: true`: adds the shipping cost line after packages are saved. Partial success: if saving the shipping cost line fails, the label and packages are still preserved and the call returns HTTP 200. Check `shipping_cost_line_item.status`: if `failed`, add the shipping cost line manually.
- For an order from Shopify with tracking sync on, sends the tracking to Shopify, which may email the customer.
- YoPrint itself sends no email. Open Admin Portal pages refresh, and so do Customer Portal pages the order's customer has open.

## Path parameters

- `team_slug` (string, required): Your team slug, shown as Team ID next to your API key in Profile. Example `your-team`.
- `hash_id` (string, required): The shipment's hash id (`id`). Hash ids only: a unique key or a document number gives 404. Example `MQyJUnd3oty75h`.

## Request body

Content type `application/json`, required.

Fields:

- `rate_id` (string, required): The `rates[].object_id` to buy, from Get shipping rates (or an estimate's `goshippo_selected_rate_id`).
- `create_shipping_cost_line_item` (boolean, optional): true to add a "Shipping Cost" line for the label to the shipment after the purchase. Default false: nothing is added. Default `false`.
- `shipment_estimate_id` (string | null, optional): Optional: the `id` of one of this shipment's shipping estimates, to link the shipping cost line to it. It must belong to this shipment, even when you don't ask for the line.

### Example request

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

## Response

`200` Successful response.

Fields:

- `data` (array of objects, required): One package per parcel of the rate: the master first, then the others (with `parent_id`).
  - Each item:
    - `charges` (integer | number | string | null, optional): The label cost. For a multi-parcel label every package repeats the rate's full amount: read the master's value and don't add packages up. Null for external labels. Thousandths, an older encoding: `12500` is 12.50. Usually a whole number; an amount finer than a thousandth comes back as a number with a fraction (`1234.5` is 1.2345), or as a string (`"1234.5"`) when you send `X-Yoprint-Decimal-Format: exact-v1`. See [Money and quantities](/concepts/money-and-quantities/).
    - `created_at` (string (date-time), optional)
    - `from_address` (any, optional): The address record behind `from_address_id`, or null when it is not set.
    - `from_address_id` (string | null, optional): Older packages only: the ship-from address record; null otherwise.
    - `goshippo` (object, optional): Shippo labels only: the Shippo details (shipment, rate, carrier, addresses, parcels).
      - `address_from` (any, optional): The ship-from address sent to Shippo.
      - `address_return` (any, optional): The return address sent to Shippo.
      - `address_to` (any, optional): The ship-to address sent to Shippo.
      - `carrier_id` (string | null, optional): The Shippo carrier account.
      - `parcel_id` (string | null, optional): The Shippo parcel.
      - `parcels` (any, optional): The parcels of the Shippo shipment.
      - `provider` (string | null, optional)
      - `provider_image_200` (string | null, optional)
      - `provider_image_75` (string | null, optional)
      - `provider_tracking_number` (string | null, optional)
      - `rate_id` (string | null, optional): The Shippo rate bought.
      - `shipment_data` (any, optional): The Shippo shipment the rate came from.
      - `shipment_id` (string | null, optional): The Shippo shipment.
    - `height` (number | string | null, optional): Parcel size and weight as stored, in `length_unit` and `weight_unit`. Null for external labels.
    - `id` (string, optional): The package.
    - `is_master` (boolean, optional): The first package of a label (the others have `parent_id`).
    - `length` (number | string | null, optional)
    - `length_unit` (string | null, optional): Shippo labels only, upper case.
    - `order_external_ids` (array of objects, optional): The order's links to external systems.
    - `order_external_type` (string | null, optional): `shopify` when the order came from Shopify.
    - `order_id` (string, optional): The order.
    - `order_object_type` (string | null, optional)
    - `order_quote` (boolean, optional)
    - `order_scoped_id` (string | null, optional)
    - `parent_id` (string | null, optional): The master package, for the other parcels of a multi-parcel label.
    - `pdf` (array of objects, optional): Not on external labels: the label file. `label` and `description` are fixed texts that always say 4 x 6, whatever your label size setting.
      - Each item:
        - `description` (string, optional)
        - `label` (string, optional)
        - `url` (string, optional): For a Shippo label, Shippo's signed link to the label file: don't publish it.
    - `provider_name` (string | null, optional): The carrier.
    - `provider_saturday_delivery` (boolean, optional): Shippo labels only (absent on external labels); always false today.
    - `provider_service_code` (string | null, optional): For external labels, `ups`, `fedex` or `other`. For Shippo labels it may be null: read the service from `provider_service_name`.
    - `provider_service_name` (string | null, optional)
    - `provider_shipment_id` (string | null, optional): The Shippo transaction id (Shippo labels).
    - `provider_tracking_number` (string | null, optional)
    - `shipment_external_type` (string | null, optional): `shopify` when the shipment is linked to Shopify.
    - `shipment_has_third_party_fulfilled_items` (boolean, optional)
    - `shipment_id` (string, optional): The shipment.
    - `shipment_scoped_id` (string, optional)
    - `shipment_shopify_sync` (any, optional): Shopify sync state of the shipment.
    - `status` (string, optional): One of: `"pending"`, `"ready-to-ship"`, `"shipped"`, `"delivered"`.
    - `to_address` (any, optional): The address record behind `to_address_id`, or null when it is not set.
    - `to_address_id` (string | null, optional): Older packages only: the ship-to address record; null otherwise.
    - `type` (string, optional): `goshippo` a label bought through Shippo; `external_label` a label recorded from elsewhere. One of: `"goshippo"`, `"external_label"`, `"single_label"`.
    - `weight` (number | string | null, optional)
    - `weight_unit` (string | null, optional): Shippo labels only, upper case.
    - `width` (number | string | null, optional)
- `shipping_cost_line_item` (object, required): What happened to the shipping cost line. Always present.
  - `shipment_item_id` (string | null, required): The shipment line's `id` for `created` and `existing`; null otherwise.
  - `status` (string, required): `not_requested` without `create_shipping_cost_line_item: true`; `created` or `existing` when the shipment has the line; `skipped_third_party_billing` when a third party pays for the label; `failed` when adding it failed (the label is still bought). One of: `"not_requested"`, `"created"`, `"existing"`, `"skipped_third_party_billing"`, `"failed"`.
- `status` (integer, required)
- `success` (boolean, required)

Example response:

```json
{
  "data": [
    {
      "charges": 12400,
      "created_at": "2026-11-03T15:00:00+00:00",
      "goshippo": {
        "parcels": [
          {
            "distance_unit": "in",
            "height": "4",
            "length": "12",
            "mass_unit": "lb",
            "weight": "2",
            "width": "10"
          }
        ],
        "provider": "USPS",
        "rate_id": "rate_0001"
      },
      "height": "4",
      "id": "o6foWpSAayc79D",
      "is_master": true,
      "length": "12",
      "length_unit": "IN",
      "order_id": "sFbALVSYD55eXe",
      "order_object_type": "sales_order",
      "order_quote": false,
      "order_scoped_id": "SO1001",
      "parent_id": null,
      "pdf": [
        {
          "description": "Postage label individually to be printed 4\" x 6\" label",
          "label": "Postage Label (4 in x 6 in label)",
          "url": "https://example.com/labels/o6foWpSAayc79D.pdf"
        }
      ],
      "provider_name": "USPS",
      "provider_saturday_delivery": false,
      "provider_service_code": null,
      "provider_service_name": "Ground Advantage",
      "provider_shipment_id": "transaction_0001",
      "provider_tracking_number": "9400100000000000000000",
      "shipment_has_third_party_fulfilled_items": false,
      "shipment_id": "MQyJUnd3oty75h",
      "shipment_scoped_id": "SHIP-12",
      "status": "pending",
      "type": "goshippo",
      "weight": "2",
      "weight_unit": "LB",
      "width": "10"
    }
  ],
  "shipping_cost_line_item": {
    "shipment_item_id": "Tt8N6jGCrroTHW",
    "status": "created"
  },
  "status": 200,
  "success": true
}
```

## Errors

- `401` The API key is missing or wrong (`unauthenticated`).
- `403` You are not allowed to do this. `api_access_not_in_plan`: your team's plan doesn't include API access (Pro and above, with an active subscription or trial), so every request answers this until the team upgrades. When your role lacks the permission the operation needs, the body is only `{"message": "Insufficient Permission"}`. Other refusals use the error envelope, with `unauthorized` or `user_is_not_authorized`.
  - When: Your role can't view this order (`user_is_not_authorized`). It is checked before the quote, Shippo and publish checks, and nothing was bought. Fix: Ask the team owner for permission to view this order, or to be assigned to it.
- `404` Not found. `page_not_found`: the team slug is unknown, you are not a member of the team, or an id in the path is not one of this team's. `code: null`, with a message that starts "No query results for": there is no record with that id.
  - When: The shipment isn't your team's. Fix: Check the shipment id.
- `422` The request was refused and changed nothing. `validation_failed` lists what to fix in `error.fields`; other codes name a business rule, explained in `error.message`. A few checks answer with only `{"message": "..."}`. An operation that calls another service (a payment provider, a carrier, an accounting app) can answer `connect_timeout` ("Please retry again") when that service does not answer in time: the change may or may not have happened there, so read the record back before you retry.
  - When: `rate_id` is missing, or `create_shipping_cost_line_item` isn't a boolean (`validation_failed`). Fix: Send the rate's `object_id` as `rate_id`.
  - When: `shipment_estimate_id` isn't an estimate of this shipment (`validation_error`, "The shipping estimate does not belong to this shipment."). Nothing was bought. Fix: Send an estimate `id` from List the shipping estimates of a shipment, or leave it out.
  - When: Shippo already used this rate (`label_error`: "Shippo has already used this rate for an earlier label attempt, so it can't be bought again. Get rates again and choose a rate..."). Fix: Check your Shippo account for a label from the earlier attempt before you buy another.
  - When: Shippo answered the purchase with a status other than `SUCCESS` (`label_error`, with Shippo's message; `error` also carries Shippo's `object_id`, `status` and `messages`), for example an invalid address. Fix: Fix the cause, get rates again and buy the new rate.
  - When: A quote, an unpublished order, a Shopify or carrier conflict, or the FedEx parcel limit (see rules). Fix: Resolve the named rule first.
  - When: Shippo isn't connected or is disabled (`goshippo_not_enabled`). Fix: Connect Shippo in the web app first.
  - When: Another purchase of the same `rate_id` started less than 10 seconds ago and hasn't succeeded: it is still running, or it failed (a failure keeps the lock until 10 seconds after that start). `error.code` is `validation_error` and the message is "Locking Token has Been Called: " followed by your request's `locking_token` field, if you sent one (nothing otherwise). Fix: Wait, then list the shipment's packages: the first attempt may have bought the label.
- `500` A business rule refused the request (see the operation's errors), or something failed inside YoPrint. Refusals use the error envelope with a code such as `action_not_allowed`; an unexpected failure answers `{"message": "Server Error"}`. Repeating a refusal gets the same answer; after an unexpected failure, read the record back before you retry a change.
  - When: Shippo failed or timed out mid-purchase, Shippo refused the purchase call itself (a 4xx other than an already used multi-parcel rate), or YoPrint couldn't read back or save a bought label (it then asks Shippo for a refund in the background). A label may or may not have been bought. Fix: Don't retry. List the shipment's packages and check your Shippo account first.

## Notes

- Never retry a label purchase automatically, whatever the error. Follow these steps instead: 1. **Check if the label was already bought.** List the shipment's packages (`GET /v2/shipment/{shipment_id}/package`). If a package with that carrier and service is present, the label was bought. Do not retry. 2. **Check your Shippo account.** If no package appears in YoPrint, a purchase might still have succeeded: when YoPrint buys a label in Shippo but encounters an error reading back or saving it, it returns HTTP 500 and queues a background refund request to Shippo. Check your Shippo dashboard to see whether a label or refund was created. 3. **When to stop (do not reuse the rate).** Never repeat the purchase call with the same `rate_id`. YoPrint holds only a temporary 10-second lock on `rate_id`. Once cleared or expired, repeating the call sends a new purchase to Shippo, risking a duplicate charge (or 422 `label_error` for multi-parcel rates). 4. **When to retry safely.** If both YoPrint and Shippo confirm no label was purchased, request fresh shipping rates (`POST /v2/shipment/{shipment_id}/goshippo-rates`), choose a rate, and buy using the newly issued `rate_id`.
- To avoid buying twice: send one purchase per shipment at a time and wait for its answer, however long it takes (give the call a client timeout of at least 120 seconds: a single-parcel purchase can take up to about 42 seconds and a multi-parcel one about 90); never resend while it is pending; after any error or timeout, list the shipment's packages and check your Shippo account before anything else; and if you do buy again, get new rates first.
- You can't buy only some parcels of a multi-parcel rate.
- Read `shipping_cost_line_item.status`: `created` (the line was added; `shipment_item_id` is its `id`), `existing` (the package already had one), `not_requested`, `skipped_third_party_billing`, or `failed`. After a lost answer, adding the line again with Add a line to a shipment and the same `shipment_package_id` returns the line if it already exists.
- To void a label, delete its package: YoPrint then asks Shippo for a refund in the background. Whether you get the money back is up to Shippo and the carrier, and a refused refund is not reported back.
- AI agents: always ask a person to confirm the rate and price before calling this.

## Related

- [Get shipping rates](https://developer.yoprint.com/reference/shipping-rates-and-estimates/v2-shipment-rate-goshippo.md): `POST /v2/api/store/{team_slug}/v2/shipment_rate/goshippo`
- [List the packages of a shipment](https://developer.yoprint.com/reference/shipment-packages/v2-shipment-package-index.md): `GET /v2/api/store/{team_slug}/v2/shipment/{hash_id}/packages`
- [Download labels as one PDF](https://developer.yoprint.com/reference/shipping-labels/v2-shipment-label-compile-goshippo.md): `POST /v2/api/store/{team_slug}/v2/shipment_package/labels/compile`
- [Delete a package (voids its label)](https://developer.yoprint.com/reference/shipment-packages/v2-shipment-package-destroy.md): `DELETE /v2/api/store/{team_slug}/v2/shipment_package/{hash_id}`
- [Add a line to a shipment](https://developer.yoprint.com/reference/shipment-line-items/v2-shipment-item-create.md): `POST /v2/api/store/{team_slug}/v2/shipment/{unique_key}/item`
