API Conventions
Conventions that apply across the public Mailhub API.
Base URL and environments
Mailhub publishes two customer environments, and this page is the canonical source for both base URLs:
| Environment | API base URL |
|---|---|
| Sandbox | https://api-sandbox.mailhub.com |
| Production | https://api.mailhub.com |
Each value is an origin: no trailing slash, and no /api suffix. Every public
route already begins with /api/v1 or /api/v2, so a request URL is the base
plus the route — https://api.mailhub.com/api/v1/orders — and a base ending in
/api would send /api/api/v1/orders.
Every example in this portal keeps the base URL in an environment variable rather than inlining a host, so the same snippet runs against either environment:
# Sandbox
export MAILHUB_API_BASE_URL=https://api-sandbox.mailhub.com
# Production
export MAILHUB_API_BASE_URL=https://api.mailhub.com
Credentials are per environment and are not interchangeable — see Prepare Your Mailhub Account. Sandbox is for non-production testing and Production for live usage; see Sandbox for what that environment does and does not establish, and Production Readiness.
Authentication
Every operation requires:
Authorization: Bearer <access-token>
except the three token operations, which are anonymous and carry their credential in the request body instead:
| Operation | Bearer required? | Body credential |
|---|---|---|
POST /api/v1/auth/token | No | apiKey |
POST /api/v1/auth/token/refresh | No | refreshToken |
POST /api/v1/auth/token/revoke | No | refreshToken |
Sending a bearer token to any of the three is neither required nor a substitute for the body credential.
See Authentication for the full token exchange/refresh/revoke flow — this page does not repeat it. There is no public API-key lifecycle endpoint (creation, rotation, or listing) in the current boundary; key management happens through the Mailhub application (see Get an API Key).
Content types
- Every request/response body is
application/jsonexcept Download a Label, which returnsapplication/pdforapplication/zpldepending on the requestedformat. All three ZPL resolutions share the one ZPL media type. - Always inspect the HTTP status and
Content-Typeheader of the download response before treating the body as either JSON or a label file — a202/error response does not carry a label file at all.
Response envelope
Every JSON endpoint (auth, Orders, Rates, label purchase, and cancellation) returns the same envelope shape:
{
"success": true,
"data": { "...": "operation-specific" },
"errors": null
}
On failure, success is false, data is typically null, and errors is
an array of:
{ "code": "...", "message": "...", "metaData": null }
POST /api/v2/orders keeps the outer response envelope. Its data
contains order, shipmentRates, status, and operation-specific errors.
See Create an Order with Rates.
See Error Handling for status-code-by-status-code guidance.
IDs and paths
Resource IDs are passed as path parameters
(/api/v1/orders/{orderId}, /api/v1/labels/{labelId}/download, …) and
returned as opaque strings in response bodies — see
Orders and Shipments for
the identifier flow and why you should not parse or reuse them across
environments.
Timestamps
Every timestamp field observed in the contract
(createdAt, updatedAt, accessTokenExpiresAtUtc,
refreshTokenExpiresAtUtc, …) is an ISO 8601 string in UTC — the Utc
suffix on the token-expiry fields is explicit about this, and none of the
other timestamp fields declare a different offset. Do not assume a local
timezone.
Pagination and filtering
Only GET /api/v1/orders currently supports list filtering:
| Parameter | Required | Notes |
|---|---|---|
search | No | Free-text search |
sortByNewest | No | Boolean |
dateFrom / dateTo | No | ISO 8601 date-time |
statuses | No | Array of the numeric OrderStatus enum (see Object Model) |
pageNumber / pageSize | No | Defaults to page 1, size 20 |
The response envelope carries items, totalItemsCount, pageNumber,
pageSize, totalPages, hasNext, and hasPrevious — see
Make Your First Request for a worked
example. No other public list endpoint currently exists.
Status fields and enums
Order status is always a number. Every place the public API reports or
accepts an Order's fulfilment status, it does so as the numeric OrderStatus
value 0–11: the Create Order responses (POST /api/v1/orders's
data.status and POST /api/v2/orders's data.order.status), the list and
retrieval responses, the statuses filter, and the PATCH /orders/{orderId}/status request together with its previousStatus and
currentStatus. See
Update Order Status
for the full mapping.
The names tooling shows you are explanatory, not an alternate wire value.
The contract publishes a name alongside each OrderStatus code, so the API
Reference and a generated client can show Created rather than a bare 0.
That name is a label for the number. It is never what the API sends, and it is
never something you may send.
Do not conflate the status domains. Several unrelated concepts are spelled
status on this API, and they do not share a vocabulary or a representation:
| Field | Concept | Published values |
|---|---|---|
Order status | Order fulfilment lifecycle | The numeric OrderStatus, 0–11 |
Shipment status | Carrier tracking state, as a string | Unknown, PreTransit, InTransit, OutForDelivery, Delivered, AvailableForPickup, ReturnToSender, Failure, Cancelled, Error |
data.status on POST /api/v2/orders | A fixed acknowledgement for the combined create-and-rate call. Not an Order status | created |
Label-purchase operation status (the 202 response) | The asynchronous operation's own state | Claimed, Processing, OutcomeUnknown |
labelProcessingStatus, labelAssets[].status | Label file rendering progress | completed, processing |
labelAssets[].format | Which printable rendering the entry is | PDF, ZPL300DPI, ZPL203DPI, ZPL600DPI |
Label status | Label lifecycle | The numeric LabelStatus, 0–4 |
category on a label-file error | Broad failure class the HTTP status derives from | General, Validation, Authorization, NotFound, Conflict, Infrastructure, Upstream |
Each of these vocabularies is published in the contract, so the API Reference, the Postman collection and a generated client all carry the permitted values. Two caveats on reading them:
- A published vocabulary is what the field can carry, not what you will see
today. Carrier tracking is not wired in, so a Shipment
statusreadsUnknownin practice even though the full tracking vocabulary is declared. - The
202vocabulary is a strict subset of the internal operation states. A completed purchase answers200and a confirmed failure answers an error, so neither of those states can appear on the202body.
Read the exact type the contract declares for the specific field you are reading, and do not translate one domain's values into another's.
Fields that carry a carrier's own vocabulary — serviceCode, serviceName,
carrier, currency, billingType, mailClass, predefinedPackage — are
deliberately not constrained. They have no closed value set Mailhub owns, and a
carrier adding a service must not become a contract violation. Do not build against a
fixed list of their values.
Versioning
/api/v1is the current stable surface./api/v2currently has one Order operation:POST /api/v2/orders, the combined create-and-rate flow. It creates one Order per request — see Create an Order with Rates.
Change policy
The running Mailhub API is the source of truth for the public contract. What is published here — this portal, the API Reference, the OpenAPI document and the Postman collection — is generated from it, so a change to the API and a change to its documentation are the same change.
- Customer-visible changes are described in Release Notes. When a release contains a breaking change, that release's notes identify it as breaking and name the operations affected.
- A breaking correction does not automatically create a new major version. Where the published contract described something the API does not do, Mailhub may correct it in place under review rather than versioning the inaccuracy. Read the release notes for the release you are updating across.
- Regenerated clients may need work after some changes. When a change to the OpenAPI document can alter generated types, method signatures, or required fields, the release notes say so.
- Additive change is not breaking. A new response field, or a published set of permitted values for a field that previously declared none, does not require a new version. Parse responses tolerantly.
Deprecation and retirement
Mailhub communicates planned API changes through this Developer Portal and Release Notes.
Mailhub does not currently guarantee a fixed minimum deprecation notice period, and does not currently guarantee a fixed overlap window during which a superseded major version keeps running alongside its replacement — a new major version may replace the previous one. Do not build a migration schedule on an assumed notice period.
If a version or an operation is deprecated, its concrete retirement timing is documented with that deprecation.
Idempotency and retries
There is no global idempotency guarantee across the public API. Behavior is endpoint-specific:
| Operation | Safe to retry? |
|---|---|
GET/list/retrieve operations, and Label download | Generally safe to retry after a transient failure |
POST /api/v1/auth/token/refresh | Not safely repeatable with the same refresh token — refresh rotates it; retrying with the now-spent token fails |
POST /api/v1/auth/token/revoke | Repeating a revoke for a token Mailhub has already revoked answers 200 again. Not blanket idempotency: an unrecognized token answers 401, and a missing, empty, or whitespace-only refreshToken answers 422 |
DELETE /api/v1/labels/{labelId} (cancel) | Do not assume repeat safety beyond data.isSuccess |
POST /api/v1/shipments/{shipmentId}/labels (purchase) | No idempotency key, but a repeat of the same purchase request is recognized server-side and replays its outcome instead of buying a second Label |
POST /api/v1/orders, POST /api/v2/orders, POST .../rates, PATCH .../status | No documented idempotency key — do not blindly repeat an ambiguous result |
See Retries and Ambiguous Outcomes for operation-specific ambiguity guidance.
Rate limiting
The public API does not currently declare a 429 response or a
requests-per-minute quota. Do not depend on a specific limit or recovery
behavior that is not documented.
Correlation and support evidence
The public API does not currently declare a request/correlation ID response
header. Individual response bodies may still carry diagnostic identifiers — a
Label-purchase 202 returns operationId and correlationId — but no public
operation accepts either value back. When retaining evidence for
Developer Support, record your own client-side timestamp,
the operation and route template, the HTTP status, the sanitized errors
body, and any Order/Shipment/Label identifiers involved — never a bearer
token, refresh token, or full label file content.