Skip to content

Buy a shipping label (charges your Shippo account)

Open .md

POST/v2/api/store/{team_slug}/v2/shipment/{hash_id}/label/goshippo

v2Operation ID v2_public_shipment_label_create_goshippo

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.

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_slugstringrequired

    Your team slug, shown as Team ID next to your API key in Profile.

    • Example your-team
  • hash_idstringrequired

    The shipment's hash id (id). Hash ids only: a unique key or a document number gives 404.

    • Example MQyJUnd3oty75h

Request body

application/json, required.

  • rate_idstringrequired

    The rates[].object_id to buy, from Get shipping rates (or an estimate's goshippo_selected_rate_id).

  • true to add a "Shipping Cost" line for the label to the shipment after the purchase. Default false: nothing is added.

    • Default false
  • 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.

Response

200Successful response.

  • dataarray of objectsrequired

    One package per parcel of the rate: the master first, then the others (with parent_id).

    Fields of each item (36)
    • chargesinteger | number | string | null

      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.

    • created_atstring (date-time)
    • The address record behind from_address_id, or null when it is not set.

    • from_address_idstring | null

      Older packages only: the ship-from address record; null otherwise.

    • goshippoobject

      Shippo labels only: the Shippo details (shipment, rate, carrier, addresses, parcels).

      Child fields (13)
    • heightnumber | string | null

      Parcel size and weight as stored, in length_unit and weight_unit. Null for external labels.

    • idstring

      The package.

    • is_masterboolean

      The first package of a label (the others have parent_id).

    • lengthnumber | string | null
    • length_unitstring | null

      Shippo labels only, upper case.

    • order_external_idsarray of objects

      The order's links to external systems.

    • order_external_typestring | null

      shopify when the order came from Shopify.

    • order_idstring

      The order.

    • order_object_typestring | null
    • order_scoped_idstring | null
    • parent_idstring | null

      The master package, for the other parcels of a multi-parcel label.

    • pdfarray of objects

      Not on external labels: the label file. label and description are fixed texts that always say 4 x 6, whatever your label size setting.

      Fields of each item (3)
      • labelstring
      • urlstring

        For a Shippo label, Shippo's signed link to the label file: don't publish it.

    • provider_namestring | null

      The carrier.

    • Shippo labels only (absent on external labels); always false today.

    • For external labels, ups, fedex or other. For Shippo labels it may be null: read the service from provider_service_name.

    • The Shippo transaction id (Shippo labels).

    • shopify when the shipment is linked to Shopify.

    • The shipment.

    • Shopify sync state of the shipment.

    • statusstring
      • One of: "pending", "ready-to-ship", "shipped", "delivered"
    • The address record behind to_address_id, or null when it is not set.

    • to_address_idstring | null

      Older packages only: the ship-to address record; null otherwise.

    • typestring

      goshippo a label bought through Shippo; external_label a label recorded from elsewhere.

      • One of: "goshippo", "external_label", "single_label"
    • weightnumber | string | null
    • weight_unitstring | null

      Shippo labels only, upper case.

    • widthnumber | string | null
  • What happened to the shipping cost line. Always present.

    Child fields (2)
    • shipment_item_idstring | nullrequired

      The shipment line's id for created and existing; null otherwise.

    • statusstringrequired

      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"
  • statusintegerrequired
  • successbooleanrequired

Errors

  • 401The API key is missing or wrong (unauthenticated).

  • 403You 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.
  • 404Not 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.
  • 422The 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.
  • 500A 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.