Skip to main content

Create and Download a USPS Label

Three requests take you from a sender and a recipient to a USPS Label PDF on disk; a purchase that answers 202 adds one or more Order reads before the download. USPS adds no endpoint of its own — this is the standard Mailhub workflow, with the carrier chosen from what the Rates response returns.

What you'll build

  • An Order with one Shipment, and the Rates for it.
  • A USPS Rate, picked from the returned options by your own rule.
  • A purchased Label, including the branch where the purchase outruns the response.
  • A PDF file on disk.

Prerequisites

  • An access token — see Authentication. Every request below sends it as a bearer token.
  • Your API base URL in MAILHUB_API_BASE_URL. The portal publishes no fixed API host; use the value supplied for your account.
  • US domestic addresses with two-letter state codes and ZIP or ZIP+4, and a parcel with weight and custom dimensions. See Requirements & Restrictions for the input rules Mailhub applies before a Shipment can be rated.
  • Python examples use the requests package.

Step 1 — Create the Order and request Rates

POST/api/v2/orders

One order object per request.

curl -s -X POST "${MAILHUB_API_BASE_URL}/api/v2/orders" \
-H "Authorization: Bearer ${MAILHUB_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"order": {
"fromAddress": {
"name": "Jane Sender", "street1": "123 Main St", "city": "Austin",
"state": "TX", "zip": "78701", "country": "US", "phone": "5125550100"
},
"toAddress": {
"name": "John Recipient", "street1": "456 Oak Ave", "city": "Denver",
"state": "CO", "zip": "80202", "country": "US", "phone": "3035550100"
},
"shipments": [
{"parcel": {"weight": 16, "length": 10, "width": 8, "height": 4}}
]
}
}'

Response

{
"success": true,
"data": {
"order": {"id": "5f2c1a10-2b3d-4e5f-8a9b-0c1d2e3f4a5b", "...": "..."},
"shipmentRates": [
{
"shipmentId": "b91a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"rates": [
{
"id": "c4a1b2d3-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"carrier": "USPS",
"serviceName": "USPS Ground Advantage",
"rate": 8.42,
"currency": "USD",
"...": "..."
},
{"id": "...", "carrier": "UPS", "serviceName": "UPS Ground", "...": "..."}
]
}
]
},
"errors": null
}

Step 2 — Pick a returned USPS Rate

Read carrier to recognise USPS, and serviceName to tell the services apart. Mailhub normalises three known names — USPS Ground Advantage, USPS Priority Mail, and USPS Priority Mail Express — and other provider-supplied USPS service names may still be returned. Treat an unrecognised name as a usable option and use its Rate id.

# Selection is your application's job. With jq, take the USPS options and
# pick the service you ship with, falling back to any USPS option.
SELECTED_RATE_ID=$(printf '%s' "${RATES_JSON}" | jq -r '
[.data.shipmentRates[0].rates[] | select(.carrier == "USPS")]
| (map(select(.serviceName == $want)) + .) | .[0].id
' --arg want "USPS Ground Advantage")

Continue only when the response contains a Rate whose carrier is USPS. A returned Rate is the only evidence the service can be used for this Shipment; if none comes back, see Carrier Availability rather than changing the request. serviceCode also appears on each Rate — it is provider-dependent, so pass it through unchanged if your application stores it, and select on serviceName instead.

Step 3 — Buy the Label

POST/api/v1/shipments/{shipmentId}/labels

The body is the Rate you chose, nested:

{ "rate": { "id": "c4a1b2d3-4e5f-6a7b-8c9d-0e1f2a3b4c5d" } }
curl -s -X POST "${MAILHUB_API_BASE_URL}/api/v1/shipments/${SHIPMENT_ID}/labels" \
-H "Authorization: Bearer ${MAILHUB_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"rate": {"id": "'"${SELECTED_RATE_ID}"'"}}'

On a 200

{
"success": true,
"data": {
"id": "b91a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"postageLabel": {"id": "f6c7d8e9-0a1b-4c2d-8e3f-4a5b6c7d8e9f", "...": "..."},
"...": "..."
},
"errors": null
}

data.id is the Shipment — the same value you sent in the path. The Label identifier is the nested data.postageLabel.id.

On a 202

The purchase is still running. The body's operationId is diagnostic only: do not send it to another Mailhub operation, and do not look for an operation-status endpoint, because there is none. Read the Order instead:

GET/api/v1/orders/{orderId}
{
"success": true,
"data": {
"id": "5f2c1a10-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"labelId": "f6c7d8e9-0a1b-4c2d-8e3f-4a5b6c7d8e9f",
"status": 8,
"...": "..."
},
"errors": null
}

labelId is the same identifier a 200 returns as postageLabel.id. status is the numeric Order status enum, and 8 is LabelCreated. The purchase sets that status itself — there is no status update for you to send.

If the Order does not carry a Label ID yet, stop here. Read the Order again later, at a cadence appropriate for your application; the contract defines none. Continue to Step 4 only once data.labelId is present — a download built from an absent identifier is a request for a Label that does not exist. Full treatment: Recover a Label Purchase After 202.

Step 4 — Download the PDF

GET/api/v1/labels/{labelId}/download

format=1 asks for PDF. The successful body is the file, not JSON.

curl -s -o label.pdf -w '%{http_code} %{content_type}\n' \
"${MAILHUB_API_BASE_URL}/api/v1/labels/${LABEL_ID}/download?format=1" \
-H "Authorization: Bearer ${MAILHUB_ACCESS_TOKEN}"

Expected result

label.pdf on disk — a PDF Label your application can store or hand to its own printing workflow.

Common outcomes

StepOutcomeWhat to do
Order + Rates422, no Rate returnedThe Order may already exist. Do not resend unchanged — see Recipe 2.
Pick a RateNo carrier of USPS in the responseStop. A returned Rate is the only evidence the service can be used here.
Buy the Label202Read the Order for labelId — see Recipe 4.
Buy the Label409A different purchase is already active, or the Shipment already has a Label.
Download202The file is pending or still being generated. Ask again later.

Every other status follows the standard public error envelope — see Error Handling.

Next steps