Create an Order and Get Rates in One Call
One request creates an Order and returns the Rates available for each of its Shipments, so you can go straight from an address pair to a Rate identifier.
What you'll build
- One Order containing a single Shipment, from an address pair and a parcel.
- The Rate options that Shipment can be shipped at, grouped by Shipment.
- The
rate.idyour Label purchase will need.
Prerequisites
- An access token — see Authentication.
- Your API base URL in
MAILHUB_API_BASE_URL. The portal publishes no fixed API host: use the value supplied for your account, or your configured Sandbox base URL. - Python examples use the
requestspackage.
Step 1 — Create the Order and request Rates
POST/api/v2/orders
The body carries exactly one order object — a single Order per request.
- cURL
- JavaScript
- Python
- Java
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}}
]
}
}'
const response = await fetch(`${process.env.MAILHUB_API_BASE_URL}/api/v2/orders`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MAILHUB_ACCESS_TOKEN}`,
'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 [shipment] = data.shipmentRates;
// Your application picks the Rate. Nothing in the response ranks them, so match
// on the values you care about — here, the service your integration ships with.
const chosen = shipment.rates.find(rate => rate.serviceName === wantedServiceName);
const selectedRateId = chosen?.id;
import os
import requests
response = requests.post(
f"{os.environ['MAILHUB_API_BASE_URL']}/api/v2/orders",
headers={"Authorization": f"Bearer {os.environ['MAILHUB_ACCESS_TOKEN']}"},
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"]
shipment = data["shipmentRates"][0]
# Your application picks the Rate. Nothing in the response ranks them, so match
# on the values you care about — here, the service your integration ships with.
selected_rate_id = next(
(rate["id"] for rate in shipment["rates"] if rate["serviceName"] == wanted_service_name),
None,
)
// Java 11+ java.net.http — no MailHub package to install and no third-party client.
String body = """
{
"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(System.getenv("MAILHUB_API_BASE_URL") + "/api/v2/orders"))
.header("Authorization", "Bearer " + System.getenv("MAILHUB_ACCESS_TOKEN"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// Parse response.body() with your own JSON library. Your application picks the Rate:
// nothing in the response ranks them, so match on the values you care about and keep
// data.shipmentRates[].rates[].id together with data.shipmentRates[].shipmentId.
See Create an Order for the full address and parcel rules, including which fields are optional.
Response
Trimmed to the fields this workflow uses. The complete schema is in the API Reference.
{
"success": true,
"data": {
"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
}
carrier, serviceName, rate, and currency are the fields to compare when
your application decides. serviceCode is also returned; it is
provider-dependent, so pass it through unchanged rather than parsing or
switching on it.
For the untrimmed body — every declared field on the Order, its Addresses, its Parcel, and a Rate — see the complete success response.
Use this next
data.shipmentRates[].rates[].id — keep the id of the Rate your
application chooses. That value is the entire body of the Label purchase
request:
{ "rate": { "id": "c4a1b2d3-4e5f-6a7b-8c9d-0e1f2a3b4c5d" } }
Keep data.shipmentRates[].shipmentId too: it is the path parameter for that
purchase. Continue with
Buy a Label from a Selected Rate, or see
Select a Rate for how to compare
the options first.
Expected result
A 200 with a persisted Order, one entry in shipmentRates per created
Shipment, and at least one Rate to choose from.
Common outcomes
| Status | What it means | What to do |
|---|---|---|
200 | The Order was created and Rates were returned. | Pick a Rate and keep its id. |
422 — invalid input | An address or parcel field failed validation. | Correct the body from the response errors, then send it again. See Validation Errors. |
422 — no Rate returned | No created Shipment returned a Rate. | Read the caveat below before doing anything else. |
400, 401, 403, 404, 409, and 500 follow the standard public error
envelope — see Error Handling.
Optional: tag the Order to a Sub-account
If supplied, X-SubAccount-Id tags the newly created Order and its Shipments
with that Sub-account. Its effect differs per operation, so consult
Work with Sub-accounts before sending it elsewhere.
-H "X-SubAccount-Id: <SUB_ACCOUNT_ID>"
Next steps
- Create an Order with Rates in the API Reference — the complete request and response schema.
- Create an Order with Rates — how this operation compares with standard Order creation.
- Rates — the public Rate model and what a returned Rate does and does not promise.
- Buy a Label from a Selected Rate — spend the Rate
idyou just kept.