Skip to content

Rates, Shipments, and Labels

Compare rates, create a shipment, buy, reprint, and void labels, and handle first-purchase owner actions and test labels.

Updated September 3, 20265 min readPostsale CLI

This article covers the CLI path from an order to a paid label: comparing rates, creating a shipment, purchasing or voiding a label, and reprinting without charging again.

In this article, we will rate shop without committing, create a shipment and purchase a label, reprint and void safely, and rehearse with dry-run first.

Real-World Example

Order ord_123 is ready. We want the cheapest service that meets the weight and address, then a PDF label, without guessing JSON field names by hand.

Before We Begin

This article assumes that:

  • Carriers and origin addresses are configured (postsale doctor, postsale carriers list)
  • You know the order id (from search or the work queue)
  • You have read Safety
  • You know how to rehearse without spending (Test Without Spending)

Web app shipping help (Dock, rate selector) lives on postsale.com/help. Here we stay on terminal commands and agent tools.

From order to label

text
Order
  → get rates          (read)
  → create shipment    (money-moving)
  → purchase label     (money-moving)
  → reprint            (read, existing PDF only)
  → void               (money-moving; refund not guaranteed)

Agent tools: get_rates, create_shipment, purchase_labels, void_shipment, get_shipment, list_shipments_for_orders, get_tracking.

Compare rates, which costs nothing

With direct commands

bash
postsale shipments get-rates --input rates-request.json
postsale schema shipments.get-rates

With the agent

bash
postsale run "get rates for shipment <shipment_id>"
postsale run "What are the cheapest rates for a 2lb package to New York?"

Weight and dimensions decide the price

  1. If you send weight, you must send weight_unit. There is no silent default.
  2. Provide all of length, width, and height, or none. Partial dimensions fail with package_dimensions_incomplete.
  3. Weight-only quotes may apply a documented small-parcel estimate (6×6×6 in, or 15×15×15 cm when dim_unit is cm). That estimate appears in applied_defaults.
  4. Do not reuse estimated dimensions for create or purchase. Measure the real package.

Use carrier accounts from postsale carriers list. Do not invent carrier ids.

Create a shipment, a money-moving step

Creating a shipment is classified money-moving in the CLI safety model (not only label purchase). Treat it like a spend-adjacent step: it needs --yes (or authorize / TTY confirm) and should be rehearsed with dry-run when you use the agent.

bash
postsale orders create-shipment <order_id> --yes
# or
postsale shipments create --input shipment.json --yes

Agent path uses create_shipment (intent-shaped builder). Parent postsale run needs --yes, --authorize, or an interactive confirmation. Purchasing the label is a separate money-moving step afterward.

Intent fields: order_id, optional carrier (fedex | ups | usps), service (a code from get_carriersenabled_services), packages, test_label, and advanced overrides. Pinning a carrier picks your default account of that type, or the built-in USPS account when you have none. When any field is given, the shipment ships from the carrier account's address unless advanced.from_address_type is origin_address with an advanced.origin_address_id.

Test labels cost nothing

Set test_label: true on the shipment to buy its labels in the carrier's test environment. The label is watermarked, never billed, metered, or insured, and the package is never shipped. USPS (including the built-in house account, with no terms or payment prompt) and UPS honour it; any other carrier refuses the purchase rather than issuing a real label.

Two things to know before using it on a real order:

  • Store automation still runs. The order is marked shipped in the store and the store's shipping email goes out.
  • The purchase step is still gated as money-moving in the CLI, because the flag lives on the shipment rather than on the purchase call. Each label returned carries test_label: true; total_cost is still populated, so read the flag, not the cost.

To make a whole session test-only, set POSTSALE_TEST_LABEL_MODE=1: every shipment the CLI creates is stamped test_label: true (disclosed in applied_defaults), and every label purchase reads its shipments first and refuses the batch unless each is a test shipment. The mode constrains what this process creates and buys and nothing else: it weakens no gate and does not touch updates or voids. Details and the refusal codes are in the safety model.

bash
postsale run "create a USPS test label for order 1001, 1 lb, 8x6x4 in" --yes

Buy a label, which charges your carrier account

bash
postsale labels purchase --shipment-id <shipment_id> --yes

Confirm label type and other flags with postsale labels purchase --help.

Agent tool: purchase_labels.

Labels are bought through POST /v2/shipment/labels, one shipment per request. The CLI keeps a batch interface (several ids on the command, shipment_ids on the agent tool, at most 10 per call), buys them in order, and merges the answers into one { labels, errors } result. Ids must be shipment UUIDs and are checked before anything is bought. Each purchase answers 200 with { labels, errors } and does not fail the request when a purchase does: a carrier rejection, a declined card, an unaccepted terms version, or a second purchase already running for the same shipment arrives as an entry in errors. A shipment with an entry in errors has no label. Shipments already bought stay bought whatever happens next. In an agent run, cancelling part-way refuses the next purchase before it is sent; the bought ones appear in the trace's confirmed_effects and the tool error lists the attempted and not-attempted shipment ids. On the direct command, a request that fails part-way prints the partial { labels, errors } result with _cli_meta.partial: true before the error (there is no cancellation handling on the direct command: Ctrl-C ends the process, and labels bought before it stay bought).

USPS sender and return-to addresses. The business/shipper block and the return-to block printed on a USPS label come from the USPS carrier account's settings.senderAddress and settings.returnAddress, not from the shipment. Set them with postsale carriers update, passing the account id and only the settings keys you are changing (see carriers); the values in force at purchase time are copied onto the processed shipment as sender_address and return_address.

First built-in (house/USPS) or Shipsurance label: one-time terms acceptance. The first time you buy one of these, the carrier's terms must be accepted by the account owner (once per version). The CLI never accepts on your behalf. For built-in USPS labels it opens a hosted page in your browser (at a TTY) or prints the link to open. For Shipsurance, accept the terms in the Postsale menubar app or Chrome extension (there is no hosted page yet), then re-run the purchase. The error code is terms_acceptance_required (exit 78), with data.accept_url or an in-app remediation for the owner. Under postsale run, the agent surfaces this as an owner_action gate and stops: it relays where to accept and does not retry until you confirm.

Inactive subscription. When the account's subscription is inactive, the backend answers 402 to every billable request. The CLI turns that into the second owner-action gate, subscription_reactivation_required (exit 101): the envelope carries data.reactivate_url for the owner to reactivate in the app, and at a terminal the CLI opens it for you. Both owner-action gates hand over a tokenless page when the CLI is running on an API key; the owner signs in there.

Reprint without purchasing again

bash
postsale labels reprint <shipment_id>
postsale labels reprint <shipment_id> --output ./label.pdf

Reprint re-fetches the existing PDF URL (optional download). It is read-only and is not listed in schema --list.

Void a label, which requests a carrier refund

bash
postsale labels void --shipment-id <shipment_id> --yes
postsale run "void the label on shipment <shipment_id>" --yes

We recommend loading the shipment first and checking tracking state. Carriers can deny voids. The CLI does not promise a refund.

Here's how:

  1. Find work:
bash
postsale run "what should I ship? show the work queue"
  1. Rehearse:
bash
postsale run --dry-run "Ship order <order_id> with the cheapest carrier"
  1. Review simulated_effects and any confirmed_effects.

  2. Ship for real:

bash
postsale run "ship order <order_id> with the cheapest carrier" --yes
  1. Confirm:
bash
postsale shipments get --reference <order_id>

Success looks like a shipment and label reflected in the envelope summary and in a follow-up get.

End-of-day manifests

bash
postsale manifest list ...
postsale manifest close ... --yes

Close is destructive (irreversible without carrier support). Agent tools: get_manifest_eligible_shipments, create_end_of_day_manifest.

Good to Know

SymptomLikely causeWhat to do
Exit 64Missing --yesAuthorize intentionally
Validation on weightMissing unitAlways send weight_unit
Cheap quote, expensive labelQuote-only dimension estimateMeasure for purchase
Void rejectedCarrier rulesPreflight shipment and tracking
Many ordersParallel batches path[Parallel batches](./parallel batch.md)
Wrong return address printedUSPS account settingscarriers update returnAddress
Purchase refused by carriertest_label on FedExUse USPS or UPS for test labels
Update refused (422)Shipment processed, or a stale insurance.amountVoid first, or resend the stored amount

Additional Reading