Run a Shipping Workflow for a Sub-account
Take the same Order → Rates → Label path you already know, and scope the steps where scope actually changes the answer.
What you'll build
- One Order created and tagged to a Sub-account.
- Rates evaluated against that Sub-account's carrier availability.
- A Label purchase evaluated the same way.
- A deliberate decision to stop sending the header once it no longer does anything.
Account and Sub-account scope
A Sub-account is an application scope beneath the authenticated Account. You
select it per request with the optional X-SubAccount-Id header. It is not a
second credential: the same access token authenticates every request below, and
record access is decided by Account ownership whether or not the header is
sent.
Prerequisites
- An access token — see Authentication.
- A Sub-account identifier authorized for your Account. It is prepared in the authenticated Mailhub application; the public API publishes no operation that creates, lists, or edits one.
- Your API base URL in
MAILHUB_API_BASE_URL. - Python examples use the
requestspackage.
Step 1 — Create the Order and request Rates, scoped
POST/api/v2/orders
Here the header does two things at once: it tags the created Order and its Shipments, and the Rates come back evaluated against that Sub-account's carrier availability.
- cURL
- JavaScript
- Python
- Java
curl -s -X POST "${MAILHUB_API_BASE_URL}/api/v2/orders" \
-H "Authorization: Bearer ${MAILHUB_ACCESS_TOKEN}" \
-H "X-SubAccount-Id: ${SUB_ACCOUNT_ID}" \
-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}}
]
}
}'
const scope = {'X-SubAccount-Id': process.env.SUB_ACCOUNT_ID};
const auth = {Authorization: `Bearer ${process.env.MAILHUB_ACCESS_TOKEN}`};
const base = process.env.MAILHUB_API_BASE_URL;
const response = await fetch(`${base}/api/v2/orders`, {
method: 'POST',
headers: {...auth, ...scope, 'Content-Type': 'application/json'},
body: JSON.stringify({
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}}],
},
}),
});
const {data} = await response.json();
const [rated] = data.shipmentRates;
const shipmentId = rated.shipmentId;
import os
import requests
base = os.environ["MAILHUB_API_BASE_URL"]
auth = {"Authorization": f"Bearer {os.environ['MAILHUB_ACCESS_TOKEN']}"}
scope = {"X-SubAccount-Id": os.environ["SUB_ACCOUNT_ID"]}
response = requests.post(
f"{base}/api/v2/orders",
headers={**auth, **scope},
json={
"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}}
],
}
},
)
data = response.json()["data"]
rated = data["shipmentRates"][0]
shipment_id = rated["shipmentId"]
// Java 11+ java.net.http — no MailHub package to install and no third-party client.
String base = System.getenv("MAILHUB_API_BASE_URL");
String auth = "Bearer " + System.getenv("MAILHUB_ACCESS_TOKEN");
String subAccountId = System.getenv("SUB_ACCOUNT_ID");
HttpClient client = HttpClient.newHttpClient();
String orderBody = """
{
"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}}
]
}
}
""";
var request = HttpRequest.newBuilder()
.uri(URI.create(base + "/api/v2/orders"))
.header("Authorization", auth)
.header("X-SubAccount-Id", subAccountId)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(orderBody))
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
// Parse response.body() with your own JSON library, then keep
// data.shipmentRates[0].shipmentId and data.shipmentRates[0].rates.
Response
Trimmed to what this workflow carries forward. The Order and its Shipments are tagged with the Sub-account you sent:
{
"success": true,
"data": {
"status": "created",
"order": {
"id": "5f2c1a10-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"status": "Created",
"...": "..."
},
"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",
"...": "..."
}
]
}
]
},
"errors": null
}
Step 2 — Buy the Label with the same scope
POST/api/v1/shipments/{shipmentId}/labels
Label purchase is the second operation whose contract gives the header an effect: the purchase is evaluated against the same Sub-account's carrier availability. Send the same value you used in Step 1.
- cURL
- JavaScript
- Python
- Java
curl -s -X POST \
"${MAILHUB_API_BASE_URL}/api/v1/shipments/${SHIPMENT_ID}/labels" \
-H "Authorization: Bearer ${MAILHUB_ACCESS_TOKEN}" \
-H "X-SubAccount-Id: ${SUB_ACCOUNT_ID}" \
-H "Content-Type: application/json" \
-d '{ "rate": { "id": "c4a1b2d3-4e5f-6a7b-8c9d-0e1f2a3b4c5d" } }'
const purchase = await fetch(`${base}/api/v1/shipments/${shipmentId}/labels`, {
method: 'POST',
headers: {...auth, ...scope, 'Content-Type': 'application/json'},
body: JSON.stringify({rate: {id: selectedRateId}}),
});
if (purchase.status === 202) {
// Still running. Recover through the Order — see the recovery Recipe.
} else {
const {data} = await purchase.json();
// data.id is the Shipment. The Label is the nested one.
var labelId = data.postageLabel.id;
}
purchase = requests.post(
f"{base}/api/v1/shipments/{shipment_id}/labels",
headers={**auth, **scope},
json={"rate": {"id": selected_rate_id}},
)
if purchase.status_code == 202:
# Still running. Recover through the Order — see the recovery Recipe.
label_id = None
else:
data = purchase.json()["data"]
# data["id"] is the Shipment. The Label is the nested one.
label_id = data["postageLabel"]["id"]
var purchase = HttpRequest.newBuilder()
.uri(URI.create(base + "/api/v1/shipments/" + shipmentId + "/labels"))
.header("Authorization", auth)
.header("X-SubAccount-Id", subAccountId)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{\"rate\":{\"id\":\"" + selectedRateId + "\"}}"))
.build();
HttpResponse<String> purchaseResponse = client.send(
purchase, HttpResponse.BodyHandlers.ofString());
if (purchaseResponse.statusCode() == 202) {
// Still running. Recover through the Order — see the recovery Recipe.
} else {
// The root data.id is the Shipment. The Label is data.postageLabel.id.
}
Step 3 — Stop sending the header
Download and Cancel record NoEffect in the public contract. Sending
X-SubAccount-Id on them changes nothing, so the scoped part of the workflow
ends after the purchase:
curl -s -o label.pdf \
"${MAILHUB_API_BASE_URL}/api/v1/labels/${LABEL_ID}/download?format=1" \
-H "Authorization: Bearer ${MAILHUB_ACCESS_TOKEN}"
Continue with Download a Label File or Cancel a Label exactly as the unscoped Recipes show them.
Use this next
data.shipmentRates[].shipmentId and the chosen
data.shipmentRates[].rates[].id — the same two values every Order-with-Rates
workflow carries into the purchase. The Sub-account changes which Rates you had
to choose from, not what you do with the one you picked.
Expected result
An Order tagged to 3a7d9e21-6b48-4c15-9f2e-7d0c5b83a614, a Label purchased
from a Rate that Sub-account could see, and a download call that carries no
scope header.
Common outcomes
| Status | What it means | What to do |
|---|---|---|
400 | The header value is not a well-formed identifier. | Send a UUID string, or omit the header. |
404 | The Sub-account does not exist or does not belong to your Account. | Confirm the value against the authenticated Mailhub application. |
409 | The Sub-account is not active. | Correct the selected scope rather than resending unchanged. |
| Missing Rate | Carrier availability for that Sub-account did not produce the option. | Compare against the same request with the header omitted. |
401, 403, 422, and 500 behave exactly as they do without the header —
see Error Handling.
Next steps
- Work with Sub-accounts — the authoritative per-operation effect matrix.
- Account and Sub-account Scope — the model behind the header.
- Get shipment rates in the API Reference — the header's current description on a scoped operation.
- Recipes — back to the section.