Buy a Label
/api/v1/shipments/{shipmentId}/labels
Open label purchase in the API Reference.
Purchase a Label for a Shipment using a Rate identifier returned by
Get Shipping Rates. Use
Select a Rate to make the
application-owned choice. The request body carries only the selected Rate's
nested rate.id:
{ "rate": { "id": "c4a1b2d3-..." } }
- cURL
- JavaScript
- C#
- Python
- Java
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}"'"}}'
const labelResponse = await fetch(`${process.env.MAILHUB_API_BASE_URL}/api/v1/shipments/${shipmentId}/labels`, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({rate: {id: selectedRateId}}),
});
if (labelResponse.status === 202) {
// Still running. Poll GET /api/v1/orders/{orderId} for the outcome.
return;
}
const {data: shipment} = await labelResponse.json();
const labelId = shipment.postageLabel.id;
var labelRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/shipments/{shipmentId}/labels")
{
Content = JsonContent.Create(new { rate = new { id = selectedRateId } }),
};
labelRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var labelResponse = await client.SendAsync(labelRequest);
using var labelPayload = JsonDocument.Parse(await labelResponse.Content.ReadAsStreamAsync());
// 202 means the purchase is still running; read the Order for the outcome.
var labelId = labelResponse.StatusCode == HttpStatusCode.Accepted
? null
: labelPayload.RootElement
.GetProperty("data")
.GetProperty("postageLabel")
.GetProperty("id")
.GetString();
import requests
label_response = requests.post(
f"{api_base_url}/api/v1/shipments/{shipment_id}/labels",
headers={"Authorization": f"Bearer {access_token}"},
json={"rate": {"id": selected_rate_id}},
)
if label_response.status_code == 202:
# Still running. Poll GET /api/v1/orders/{orderId} for the outcome.
label_id = None
else:
shipment = label_response.json()["data"]
label_id = shipment["postageLabel"]["id"]
// Java 11+ java.net.http — no MailHub package to install and no third-party client.
var request = HttpRequest.newBuilder()
.uri(URI.create(apiBaseUrl + "/api/v1/shipments/" + shipmentId + "/labels"))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{\"rate\":{\"id\":\"" + selectedRateId + "\"}}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 202) {
// Still running. Read GET /api/v1/orders/{orderId} for the outcome.
} else {
// Parse response.body() with your own JSON library. The Label identifier is
// data.postageLabel.id — data.id is the Shipment.
}
Prerequisites
The Order must not already be Cancelled, already LabelCreated (a Label
was already purchased for it), or Returned — any other status, including a
brand-new Created Order, is accepted. A successful purchase automatically
moves the Order to LabelCreated — you do not PATCH the status yourself for
this transition. See
Update Order Status for the full
transition table.
Response
A 200 means the purchase has completed. data is the Shipment: its root
data.id is the Shipment's own identifier, not a Label. Take the nested
data.postageLabel.id — that is the value
Download a Label and
Cancel a Label accept.
{
"success": true,
"data": {
"id": "b91a2c3d-...",
"status": "Unknown",
"trackingCode": "9400111899223197428490",
"orderId": "5f2c1a10-...",
"selectedRate": {
"id": "c4a1b2d3-...",
"carrier": "USPS",
"service": "Priority Mail",
"rate": "8.42",
"currency": "USD"
},
"postageLabel": {
"id": "f6c7d8e9-...",
"labelProcessingStatus": "processing",
"labelUrl": "",
"labelPdfUrl": null,
"labelZplUrl": null,
"labelFileType": "pdf"
},
"createdAt": "...",
"updatedAt": "..."
},
"errors": null
}
When the purchase is still running
The operation waits for the purchase for a bounded window. If the purchase is
still running when that window ends, it answers 202 instead of holding the
request open. The purchase continues server-side; 202 is not a failure and
not a signal to send the request again.
{
"success": true,
"data": {
"operationId": "9a8b7c6d-...",
"shipmentId": "b91a2c3d-...",
"status": "Processing",
"correlationId": "..."
},
"errors": null
}
The body also echoes the selected Rate's identifier. None of these fields is an input to any operation — they identify the running purchase for your own logging, not a resource you can fetch.
Observe the outcome by retrieving the Order —
Get Order returns
labelId once the purchase has completed, and its status moves to
LabelCreated. That labelId is the same identifier a 200 returns as
postageLabel.id. There is no operation-status endpoint to poll, and
operationId above is diagnostic only — you never send it back. Choose your own
polling cadence; the contract defines no interval.
Repeating the request
The request carries no idempotency key. Instead, Mailhub recognizes a repeat
server-side from the Shipment's purchase operation and the request that started
it: sending the same purchase request again does not buy a second Label. A
repeat of the same request replays the original outcome — 200 with the
original result once it has completed, 202 while it is still running.
A request that is not the same purchase returns 409 rather than starting a
competing one: a different rate for a Shipment whose purchase is in progress, a
Shipment that already has a Label, or an earlier purchase whose carrier outcome
is still being resolved. Correct the request or read the Order; do not loop on a
409.
Two bounded conditions remain, so this is safe repetition rather than a blanket guarantee. Retry within your own bounded budget:
- If the Label bought by your request was cancelled before the result reached you, the request is answered as a conflict and will not purchase a replacement. Submit a new purchase if you still need one.
- An unresolved carrier outcome is reconciled server-side. Until it settles, a
new purchase for that Shipment is refused with
409.
See Retries and Ambiguous Outcomes for the cross-operation policy.
Errors
400 for an invalid or no-longer-usable rate id. 401 for a missing or
expired bearer token. 409 when a different purchase is in progress or the
Shipment already has a Label. See Label Errors.
For the shortest working version of this purchase, see Buy a Label from a Selected Rate.
After purchase, continue to Download a Label. When your application needs the documented cancellation operation, see Cancel a Label. Working with USPS? See USPS with Mailhub.