API Conventions
Conventions that apply across the public Mailhub API.
Base URL and environments
No public hostname is approved yet. Use the environment variable convention already established in Getting Started and Make Your First Request:
${MAILHUB_API_BASE_URL}
Sandbox is for non-production testing and Production for live usage — obtain both base URLs and credentials from Mailhub separately for your Account. A customer-facing Sandbox's isolation, quotas, and non-billable behavior remain unconfirmed — do not assume Sandbox labels are free or automatically cleaned up. See Sandbox for what the environment does and does not establish, and Production Readiness.
Authentication
Every operation except token exchange (POST /api/v1/auth/token) requires:
Authorization: Bearer <access-token>
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.
Enum casing
Enum values are not uniformly represented. Some appear as PascalCase
strings (e.g. Order status on POST /orders's response: "Created"); the
PATCH /orders/{orderId}/status endpoint instead represents the same concept
as an integer (0–11). Do not normalize or translate either
representation in your own code — read the exact type the contract declares
for that field. See
Update Order Status
for the full mapping.
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.- No deprecation window or backward-compatibility guarantee beyond "breaking changes require a new version" is currently an accepted, published decision — do not assume a specific notice period.
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 | Do not assume repeat safety beyond the response you receive |
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. 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.