Shipping Label Gateway API
The Gateway API lets you validate addresses, create USPS shipping labels, and manage orders programmatically. It uses a two-phase ordering model: first validate your batch of orders, then confirm to create labels. This ensures you catch address errors and know the exact cost before committing funds.
All requests must include your API key in the Authorization header. All request and response bodies are JSON.
Quick Start
GET /account/ to verify your credits./orders/validate for pre-flight checks.# Verify your API key works curl https://app.example.com/gateway/v1/account/ \ -H "Authorization: Bearer sk_live_your_api_key_here"
import requests BASE_URL = "https://app.example.com/gateway/v1" API_KEY = "sk_live_your_api_key_here" headers = {"Authorization": f"Bearer {API_KEY}"} response = requests.get( f"{BASE_URL}/account/", headers=headers, ) print(response.json())
const BASE_URL = "https://app.example.com/gateway/v1"; const API_KEY = "sk_live_your_api_key_here"; const response = await fetch(`${BASE_URL}/account/`, { headers: { "Authorization": `Bearer ${API_KEY}` }, }); const data = await response.json(); console.log(data);
<?php $baseUrl = "https://app.example.com/gateway/v1"; $apiKey = "sk_live_your_api_key_here"; $ch = curl_init("$baseUrl/account/"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer $apiKey", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); echo $response;
require "net/http" require "json" BASE_URL = "https://app.example.com/gateway/v1" API_KEY = "sk_live_your_api_key_here" uri = URI("#{BASE_URL}/account/") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{API_KEY}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts JSON.parse(response.body)
using System.Net.Http; var baseUrl = "https://app.example.com/gateway/v1"; var apiKey = "sk_live_your_api_key_here"; var client = new HttpClient(); client.DefaultRequestHeaders.Add( "Authorization", $"Bearer {apiKey}" ); var response = await client.GetAsync($"{baseUrl}/account/"); var json = await response.Content.ReadAsStringAsync(); Console.WriteLine(json);
Authentication
The Gateway API uses Bearer token authentication. Include your API key in the Authorization header of every request.
Header Format
API keys are prefixed with sk_live_ followed by 48 hexadecimal characters
(56 characters total). Send the key as a Bearer token:
Security Best Practices
- Never expose your API key in client-side code, public repos, or logs
- Use environment variables to store your key
- Rotate your key from the dashboard if you suspect it has leaked — regenerating immediately invalidates the old secret
- The full key is shown only once at generation — store it securely, it cannot be retrieved later
401 Unauthorized.
curl https://app.example.com/gateway/v1/account/ \ -H "Authorization: Bearer sk_live_your_api_key_here" \ -H "Content-Type: application/json"
import requests headers = { "Authorization": "Bearer sk_live_your_api_key_here", "Content-Type": "application/json", } response = requests.get( "https://app.example.com/gateway/v1/account/", headers=headers, ) print(response.json())
const response = await fetch( "https://app.example.com/gateway/v1/account/", { headers: { "Authorization": "Bearer sk_live_your_api_key_here", "Content-Type": "application/json", }, } ); const data = await response.json(); console.log(data);
<?php $ch = curl_init("https://app.example.com/gateway/v1/account/"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer sk_live_your_api_key_here", "Content-Type: application/json", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); print_r($response);
require "net/http" require "json" uri = URI("https://app.example.com/gateway/v1/account/") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer sk_live_your_api_key_here" request["Content-Type"] = "application/json" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts JSON.parse(response.body)
using System.Net.Http; var client = new HttpClient(); client.DefaultRequestHeaders.Add( "Authorization", "Bearer sk_live_your_api_key_here" ); var response = await client.GetAsync( "https://app.example.com/gateway/v1/account/" ); var json = await response.Content.ReadAsStringAsync(); Console.WriteLine(json);
{
"success": true,
"data": {
"balance": 142.50,
"daily_orders_used": 12,
"max_bulk_labels": 100
},
"error": null,
"message": "OK"
}{
"success": false,
"data": null,
"error": {
"code": "AUTH_MISSING",
"details": {}
},
"message": "Authentication required"
}Errors
The Gateway API uses standard HTTP status codes and returns a consistent JSON error format. Every response follows the same envelope structure, making error handling predictable.
Response Envelope
All responses include success, data, error, and message fields.
On success, error is null. On failure, data is null
and error contains a machine-readable code and optional details.
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success — every Gateway endpoint (including validate and confirm) returns 200 on success |
| 400 | Validation error — check error.details |
| 401 | Missing or invalid API key |
| 403 | Account suspended or insufficient permissions |
| 404 | Resource not found |
| 409 | Conflict — duplicate order, batch already confirmed |
| 422 | Insufficient balance or unprocessable |
| 429 | Rate limit exceeded — check Retry-After header |
| 500 | Internal server error |
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"details": {
"errors": [
{
"field": "ship_to.zip",
"code": "INVALID_ZIP",
"message": "ZIP code must be 5 or 9 digits"
}
]
}
},
"message": "Validation failed"
}{
"success": false,
"data": null,
"error": {
"code": "INSUFFICIENT_BALANCE",
"details": {
"required_amount": 45.50,
"current_balance": 12.00,
"shortfall": 33.50
}
},
"message": "Insufficient balance"
}{
"success": false,
"data": null,
"error": {
"code": "RATE_LIMITED",
"details": {
"retry_after": 45
}
},
"message": "Too many requests"
}Rate Limits
The Gateway API enforces rate limits to ensure fair usage. Limits are tracked per API key.
When you exceed a limit, the API returns 429 Too Many Requests with a Retry-After header.
| Scope | Limit |
|---|---|
| All endpoints | 100 requests / minute per account |
Rate Limit Headers
Every response includes rate limit headers so you can track your usage:
| Header | Description |
|---|---|
| X-RateLimit-Limit | Maximum requests allowed in the window |
| X-RateLimit-Remaining | Requests remaining in current window |
| X-RateLimit-Reset | Unix timestamp when the window resets |
| Retry-After | Seconds to wait (only on 429 responses) |
HTTP/1.1 200 OK X-RateLimit-Limit: 100 X-RateLimit-Remaining: 87 X-RateLimit-Reset: 1712150400 Content-Type: application/json
import time import requests def api_request(url, headers, max_retries=3): for attempt in range(max_retries): response = requests.get(url, headers=headers) if response.status_code != 429: return response # Wait for the rate limit to reset retry_after = int( response.headers.get("Retry-After", 60) ) time.sleep(retry_after) raise Exception("Rate limit exceeded")
Get Wallet
Retrieve the deposit wallet address for your account. Deposit USDC or USDT (ERC-20) to this address to add credits to your balance.
Response Fields
| Field | Type | Description |
|---|---|---|
| address | string | Ethereum wallet address (0x...) |
| network | string | Network identifier (always Ethereum (ERC-20)) |
404.
curl https://app.example.com/gateway/v1/wallet/ \ -H "Authorization: Bearer sk_live_your_api_key_here"
response = requests.get( f"{BASE_URL}/wallet/", headers=headers, ) print(response.json())
const response = await fetch(`${BASE_URL}/wallet/`, { headers: { "Authorization": `Bearer ${API_KEY}` }, }); const data = await response.json(); console.log(data);
$ch = curl_init("$baseUrl/wallet/"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer $apiKey", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); print_r($response);
uri = URI("#{BASE_URL}/wallet/") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{API_KEY}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts JSON.parse(response.body)
var response = await client.GetAsync($"{baseUrl}/wallet/"); var json = await response.Content.ReadAsStringAsync(); Console.WriteLine(json);
{
"success": true,
"data": {
"address": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12",
"network": "Ethereum (ERC-20)"
},
"error": null,
"message": "OK"
}Account Info
Retrieve your current account balance, daily usage, and limits. Use this before placing orders to verify you have sufficient credits.
Response Fields
| Field | Type | Description |
|---|---|---|
| balance | number | Available balance in USD credits |
| daily_orders_used | integer | Orders created today (UTC) |
| max_bulk_labels | integer | Maximum labels per batch (default: 100) |
curl https://app.example.com/gateway/v1/account/ \ -H "Authorization: Bearer sk_live_your_api_key_here"
response = requests.get( f"{BASE_URL}/account/", headers=headers, ) account = response.json()["data"] print(f"Balance: ${account['balance']}") print(f"Orders today: {account['daily_orders_used']}")
const response = await fetch(`${BASE_URL}/account/`, { headers: { "Authorization": `Bearer ${API_KEY}` }, }); const { data } = await response.json(); console.log(`Balance: $${data.balance}`); console.log(`Orders today: ${data.daily_orders_used}`);
$ch = curl_init("$baseUrl/account/"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer $apiKey", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); $account = $response["data"]; echo "Balance: ${$account['balance']}\n"; echo "Orders today: {$account['daily_orders_used']}\n";
uri = URI("#{BASE_URL}/account/") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{API_KEY}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end account = JSON.parse(response.body)["data"] puts "Balance: $#{account['balance']}" puts "Orders today: #{account['daily_orders_used']}"
var response = await client.GetAsync($"{baseUrl}/account/"); var json = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(json); var data = doc.RootElement.GetProperty("data"); Console.WriteLine($"Balance: ${data.GetProperty("balance")}"); Console.WriteLine($"Orders today: {data.GetProperty("daily_orders_used")}");
{
"success": true,
"data": {
"balance": 142.50,
"daily_orders_used": 12,
"max_bulk_labels": 100
},
"error": null,
"message": "OK"
}Validate Orders
Submit a batch of orders for validation. The API checks addresses, verifies pricing,
detects duplicates, and confirms your balance covers the total cost.
Returns a batch_id that you use to confirm the orders.
Request Body
| Field | Type | Description |
|---|---|---|
| ordersrequired | array | Array of order objects (max 100 per batch) |
Order Object
| Field | Type | Description |
|---|---|---|
| ship_fromrequired | object | Sender address. See Address Object |
| ship_torequired | object | Recipient address. See Address Object |
| packagerequired | object | Package dimensions and weight. See Package Object |
| service_typerequired | string | priority_mail or ground_advantage |
| label_formatoptional | string | Label size. Default: 4x6 |
| referenceoptional | string | Your internal reference ID. Returned in all responses and stored with the order. |
| item_nameoptional | string | Item description stored on the label (e.g. for your own manifest). Passed through as-is. |
| confirm_duplicateoptional | boolean | Set to true to bypass the 1-hour duplicate detection window. Default: false |
Address Object
| Field | Type | Description |
|---|---|---|
| namerequired | string | Full name (max 32 chars) |
| companyoptional | string | Company name (max 40 chars) |
| address1required | string | Street address line 1 (max 32 chars) |
| address2optional | string | Apt, suite, unit (max 32 chars) |
| cityrequired | string | City (max 24 chars) |
| staterequired | string | 2-letter state code (e.g., CA) |
| ziprequired | string | ZIP code (5 or 9 digits) |
Package Object
| Field | Type | Description |
|---|---|---|
| weight_ozrequired | number | Weight in ounces (1.6 – 1120 oz) |
| length_inrequired | number | Length in inches (0.1 – 108) |
| width_inrequired | number | Width in inches (0.1 – 108) |
| height_inrequired | number | Height in inches (0.1 – 108) |
Response
The response includes a batch_id, an expiry time (15 minutes), a summary, and per-order results.
Each order's result is one of: passed, corrected, or failed.
curl -X POST https://app.example.com/gateway/v1/orders/validate \ -H "Authorization: Bearer sk_live_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{ "orders": [ { "reference": "ORDER-001", "ship_from": { "name": "John Smith", "address1": "123 Sender St", "city": "Los Angeles", "state": "CA", "zip": "90001" }, "ship_to": { "name": "Jane Doe", "address1": "456 Receiver Ave", "address2": "Apt 2B", "city": "New York", "state": "NY", "zip": "10001" }, "package": { "weight_oz": 12, "length_in": 10, "width_in": 8, "height_in": 4 }, "service_type": "priority_mail" }, { "reference": "ORDER-002", "ship_from": { "name": "John Smith", "address1": "123 Sender St", "city": "Los Angeles", "state": "CA", "zip": "90001" }, "ship_to": { "name": "Sam Lee", "address1": "789 Delivery Rd", "city": "Chicago", "state": "IL", "zip": "60601" }, "package": { "weight_oz": 10, "length_in": 9, "width_in": 6, "height_in": 3 }, "service_type": "ground_advantage" } ] }'
import requests response = requests.post( f"{BASE_URL}/orders/validate", headers=headers, json={ "orders": [ { "reference": "ORDER-001", "ship_from": { "name": "John Smith", "address1": "123 Sender St", "city": "Los Angeles", "state": "CA", "zip": "90001", }, "ship_to": { "name": "Jane Doe", "address1": "456 Receiver Ave", "address2": "Apt 2B", "city": "New York", "state": "NY", "zip": "10001", }, "package": { "weight_oz": 12, "length_in": 10, "width_in": 8, "height_in": 4, }, "service_type": "priority_mail", }, { "reference": "ORDER-002", "ship_from": { "name": "John Smith", "address1": "123 Sender St", "city": "Los Angeles", "state": "CA", "zip": "90001", }, "ship_to": { "name": "Sam Lee", "address1": "789 Delivery Rd", "city": "Chicago", "state": "IL", "zip": "60601", }, "package": { "weight_oz": 10, "length_in": 9, "width_in": 6, "height_in": 3, }, "service_type": "ground_advantage", } ] }, ) result = response.json() batch_id = result["data"]["batch_id"] print(f"Batch: {batch_id}") print(f"Summary: {result['data']['summary']}")
const response = await fetch( `${BASE_URL}/orders/validate`, { method: "POST", headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ orders: [ { reference: "ORDER-001", ship_from: { name: "John Smith", address1: "123 Sender St", city: "Los Angeles", state: "CA", zip: "90001", }, ship_to: { name: "Jane Doe", address1: "456 Receiver Ave", address2: "Apt 2B", city: "New York", state: "NY", zip: "10001", }, package: { weight_oz: 12, length_in: 10, width_in: 8, height_in: 4, }, service_type: "priority_mail", }, { reference: "ORDER-002", ship_from: { name: "John Smith", address1: "123 Sender St", city: "Los Angeles", state: "CA", zip: "90001", }, ship_to: { name: "Sam Lee", address1: "789 Delivery Rd", city: "Chicago", state: "IL", zip: "60601", }, package: { weight_oz: 10, length_in: 9, width_in: 6, height_in: 3, }, service_type: "ground_advantage", }, ], }), } ); const result = await response.json(); console.log("Batch:", result.data.batch_id); console.log("Summary:", result.data.summary);
$payload = json_encode([ "orders" => [ [ "reference" => "ORDER-001", "ship_from" => [ "name" => "John Smith", "address1" => "123 Sender St", "city" => "Los Angeles", "state" => "CA", "zip" => "90001", ], "ship_to" => [ "name" => "Jane Doe", "address1" => "456 Receiver Ave", "address2" => "Apt 2B", "city" => "New York", "state" => "NY", "zip" => "10001", ], "package" => [ "weight_oz" => 12, "length_in" => 10, "width_in" => 8, "height_in" => 4, ], "service_type" => "priority_mail", ], [ "reference" => "ORDER-002", "ship_from" => [ "name" => "John Smith", "address1" => "123 Sender St", "city" => "Los Angeles", "state" => "CA", "zip" => "90001", ], "ship_to" => [ "name" => "Sam Lee", "address1" => "789 Delivery Rd", "city" => "Chicago", "state" => "IL", "zip" => "60601", ], "package" => [ "weight_oz" => 10, "length_in" => 9, "width_in" => 6, "height_in" => 3, ], "service_type" => "ground_advantage", ], ], ]); $ch = curl_init("$baseUrl/orders/validate"); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer $apiKey", "Content-Type: application/json", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = json_decode(curl_exec($ch), true); echo "Batch: {$result['data']['batch_id']}\n";
uri = URI("#{BASE_URL}/orders/validate") request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{API_KEY}" request["Content-Type"] = "application/json" request.body = { orders: [ { reference: "ORDER-001", ship_from: { name: "John Smith", address1: "123 Sender St", city: "Los Angeles", state: "CA", zip: "90001" }, ship_to: { name: "Jane Doe", address1: "456 Receiver Ave", address2: "Apt 2B", city: "New York", state: "NY", zip: "10001" }, package: { weight_oz: 12, length_in: 10, width_in: 8, height_in: 4 }, service_type: "priority_mail" }, { reference: "ORDER-002", ship_from: { name: "John Smith", address1: "123 Sender St", city: "Los Angeles", state: "CA", zip: "90001" }, ship_to: { name: "Sam Lee", address1: "789 Delivery Rd", city: "Chicago", state: "IL", zip: "60601" }, package: { weight_oz: 10, length_in: 9, width_in: 6, height_in: 3 }, service_type: "ground_advantage" } ] }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end result = JSON.parse(response.body) puts "Batch: #{result['data']['batch_id']}"
var payload = new { orders = new[] { new { reference = "ORDER-001", ship_from = new { name = "John Smith", address1 = "123 Sender St", city = "Los Angeles", state = "CA", zip = "90001" }, ship_to = new { name = "Jane Doe", address1 = "456 Receiver Ave", address2 = "Apt 2B", city = "New York", state = "NY", zip = "10001" }, package = new { weight_oz = 12, length_in = 10, width_in = 8, height_in = 4 }, service_type = "priority_mail" }, new { reference = "ORDER-002", ship_from = new { name = "John Smith", address1 = "123 Sender St", city = "Los Angeles", state = "CA", zip = "90001" }, ship_to = new { name = "Sam Lee", address1 = "789 Delivery Rd", city = "Chicago", state = "IL", zip = "60601" }, package = new { weight_oz = 10, length_in = 9, width_in = 6, height_in = 3 }, service_type = "ground_advantage" } } }; var content = new StringContent( JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json" ); var response = await client.PostAsync( $"{baseUrl}/orders/validate", content ); var json = await response.Content.ReadAsStringAsync(); Console.WriteLine(json);
{
"success": true,
"data": {
"batch_id": "batch_6615a3f2e4b0c1d2e3f4a5b6",
"expires_at": "2025-04-10T15:30:00+00:00",
"summary": {
"total": 2,
"passed": 1,
"corrected": 1,
"failed": 0,
"estimated_total_price": 16.50
},
"orders": [
{
"index": 0,
"reference": "ORDER-001",
"result": "corrected",
"price": 8.25,
"verification": {
"ship_from": { "status": "verified" },
"ship_to": {
"status": "corrected",
"original": {
"address1": "456 Receiver Ave",
"city": "New York",
"state": "NY",
"zip": "10001"
},
"corrected": {
"address1": "456 Receiver Avenue",
"city": "New York",
"state": "NY",
"zip": "10001-6732"
}
}
},
"order_data": { /* ... corrected order data ... */ }
},
{
"index": 1,
"reference": "ORDER-002",
"result": "passed",
"price": 8.25,
"verification": {
"ship_from": { "status": "verified" },
"ship_to": { "status": "verified" }
},
"order_data": { /* ... order data ... */ }
}
]
},
"error": null,
"message": "Validation complete"
}// An order within the batch that failed validation { "index": 2, "reference": "ORDER-003", "result": "failed", "error": "VALIDATION_ERROR", "details": { "errors": [ { "field": "ship_to.state", "code": "INVALID_STATE", "message": "Not a valid 2-char state code" } ] } }
Confirm Orders
Confirm a previously validated batch to create shipping labels. This deducts credits from your balance
once and queues the labels for generation. The gateway is a
bulk-ordering API: confirming a batch creates a single bulk order that
holds every label with a passed or corrected validation result. Rows that failed
validation are returned in rejected and are not part of the order.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| batch_idrequired | string | Batch ID from the validate response (e.g., batch_6615a3f2...) |
409 BATCH_ALREADY_CONFIRMED.
Response
Returns the single bulk order_id, a summary (labels_accepted,
labels_rejected, total_price), and any rejected rows. The order starts
as pending and moves to completed, failed, or partial
(some labels succeeded, some failed) once processing finishes. Poll
GET /gateway/v1/orders/{order_id}/ or GET /gateway/v1/batches/{batch_id}/ for status
and per-label tracking numbers. If every row in the batch failed validation, the confirm still succeeds
with order_id: null and labels_accepted: 0 — no order is created and nothing is charged.
curl -X POST https://app.example.com/gateway/v1/orders/confirm/batch_6615a3f2e4b0c1d2e3f4a5b6 \ -H "Authorization: Bearer sk_live_your_api_key_here" \ -H "Content-Type: application/json"
batch_id = "batch_6615a3f2e4b0c1d2e3f4a5b6" response = requests.post( f"{BASE_URL}/orders/confirm/{batch_id}", headers=headers, ) data = response.json()["data"] print(f"Bulk order {data['order_id']} created " f"({data['summary']['labels_accepted']} labels, ${data['summary']['total_price']})")
const batchId = "batch_6615a3f2e4b0c1d2e3f4a5b6"; const response = await fetch( `${BASE_URL}/orders/confirm/${batchId}`, { method: "POST", headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json", }, } ); const { data } = await response.json(); console.log(`Bulk order ${data.order_id} created ` + `(${data.summary.labels_accepted} labels, $${data.summary.total_price})`);
$batchId = "batch_6615a3f2e4b0c1d2e3f4a5b6"; $ch = curl_init("$baseUrl/orders/confirm/$batchId"); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer $apiKey", "Content-Type: application/json", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $data = json_decode(curl_exec($ch), true)["data"]; echo "Bulk order {$data['order_id']} created " . "({$data['summary']['labels_accepted']} labels)\n";
batch_id = "batch_6615a3f2e4b0c1d2e3f4a5b6" uri = URI("#{BASE_URL}/orders/confirm/#{batch_id}") request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{API_KEY}" request["Content-Type"] = "application/json" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body)["data"] puts "Bulk order #{data['order_id']} created " \ "(#{data['summary']['labels_accepted']} labels)"
var batchId = "batch_6615a3f2e4b0c1d2e3f4a5b6"; var response = await client.PostAsync( $"{baseUrl}/orders/confirm/{batchId}", new StringContent("", Encoding.UTF8, "application/json") ); var json = await response.Content.ReadAsStringAsync(); Console.WriteLine(json);
{
"success": true,
"data": {
"batch_id": "batch_6615a3f2e4b0c1d2e3f4a5b6",
"order_id": "6615b1c2d3e4f5a6b7c8d9e0",
"summary": {
"labels_accepted": 2,
"labels_rejected": 0,
"total_price": 16.50
},
"rejected": []
},
"error": null,
"message": "Bulk order created"
}{
"success": false,
"data": null,
"error": {
"code": "BATCH_EXPIRED",
"details": {}
},
"message": "Batch has expired. Please re-validate your orders."
}Get Order Status
Poll the status of a single order. Use this after confirming a batch to check whether labels have been generated. Completed orders include a tracking number and proxy-wrapped download URLs for the label PDF.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| order_idrequired | string | Order ID from the confirm response |
Order Statuses
| Status | Description |
|---|---|
| pending | Order created, waiting for processor pickup |
| processing | Processor is generating the label |
| completed | All labels generated — each entry in labels[] carries a tracking number, plus merged download URLs |
| partial | Some labels succeeded, some failed — check each entry's status in labels[]. Only the failed labels are refunded. |
| failed | All labels failed — see each entry's error in labels[] and error_data. Create a new order. |
| cancelled | Order was cancelled by admin |
| refunded | Credits refunded to your balance |
curl https://app.example.com/gateway/v1/orders/6615b1c2d3e4f5a6b7c8d9e0/ \ -H "Authorization: Bearer sk_live_your_api_key_here"
import time order_id = "6615b1c2d3e4f5a6b7c8d9e0" interval = 2 # Start at 2 seconds timeout = 120 # 2-minute timeout elapsed = 0 while elapsed < timeout: response = requests.get( f"{BASE_URL}/orders/{order_id}/", headers=headers, ) data = response.json()["data"] if data["status"] in ("completed", "partial"): for label in data["labels"]: print(f" {label['reference']}: {label.get('status')} {label.get('tracking_number', '')}") print(f"Labels PDF: {data.get('labels_pdf_url')}") break elif data["status"] == "failed": print(f"Failed: {data['error_data']}") break time.sleep(interval) elapsed += interval interval = min(interval * 1.5, 30) else: print("Timed out — check back later")
const orderId = "6615b1c2d3e4f5a6b7c8d9e0"; let interval = 2000; const timeout = 120000; let elapsed = 0; while (elapsed < timeout) { const res = await fetch( `${BASE_URL}/orders/${orderId}/`, { headers: { "Authorization": `Bearer ${API_KEY}` } } ); const { data } = await res.json(); if (data.status === "completed" || data.status === "partial") { data.labels.forEach((l) => console.log(` ${l.reference}: ${l.status} ${l.tracking_number || ""}`)); console.log("Labels PDF:", data.labels_pdf_url); break; } else if (data.status === "failed") { console.log("Failed:", data.error_data); break; } await new Promise((r) => setTimeout(r, interval)); elapsed += interval; interval = Math.min(interval * 1.5, 30000); }
$orderId = "6615b1c2d3e4f5a6b7c8d9e0"; $interval = 2; $timeout = 120; $elapsed = 0; while ($elapsed < $timeout) { $ch = curl_init("$baseUrl/orders/$orderId/"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer $apiKey", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $data = json_decode(curl_exec($ch), true)["data"]; if (in_array($data["status"], ["completed", "partial"])) { foreach ($data["labels"] as $l) { echo " {$l['reference']}: {$l['status']} {$l['tracking_number']}\n"; } break; } elseif ($data["status"] === "failed") { echo "Failed\n"; break; } sleep($interval); $elapsed += $interval; $interval = min($interval * 1.5, 30); }
order_id = "6615b1c2d3e4f5a6b7c8d9e0" interval = 2 timeout = 120 elapsed = 0 while elapsed < timeout uri = URI("#{BASE_URL}/orders/#{order_id}/") req = Net::HTTP::Get.new(uri) req["Authorization"] = "Bearer #{API_KEY}" res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } data = JSON.parse(res.body)["data"] if ["completed", "partial"].include?(data["status"]) data["labels"].each { |l| puts " #{l['reference']}: #{l['status']} #{l['tracking_number']}" } break elsif data["status"] == "failed" puts "Failed: #{data['error_data']}" break end sleep(interval) elapsed += interval interval = [interval * 1.5, 30].min end
var orderId = "6615b1c2d3e4f5a6b7c8d9e0"; var interval = 2000; var timeout = 120000; var elapsed = 0; while (elapsed < timeout) { var response = await client.GetAsync( $"{baseUrl}/orders/{orderId}/" ); var json = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(json); var status = doc.RootElement .GetProperty("data") .GetProperty("status") .GetString(); if (status == "completed" || status == "failed") { Console.WriteLine(json); break; } await Task.Delay(interval); elapsed += interval; interval = Math.Min((int)(interval * 1.5), 30000); }
{
"success": true,
"data": {
"order_id": "6615b1c2d3e4f5a6b7c8d9e0",
"order_type": "bulk",
"status": "pending",
"label_count": 2,
"created_at": "2025-04-10T15:16:18+00:00",
"price": 16.50,
"labels": [
{ "index": 0, "reference": "ORDER-001", "service_type": "priority_mail" },
{ "index": 1, "reference": "ORDER-002", "service_type": "ground_advantage" }
]
},
"error": null,
"message": "OK"
}{
"success": true,
"data": {
"order_id": "6615b1c2d3e4f5a6b7c8d9e0",
"order_type": "bulk",
"status": "completed",
"label_count": 2,
"created_at": "2025-04-10T15:16:18+00:00",
"completed_at": "2025-04-10T15:16:42+00:00",
"price": 16.50,
"labels": [
{
"index": 0, "reference": "ORDER-001",
"service_type": "priority_mail", "status": "completed",
"tracking_number": "9400111899223100012345"
},
{
"index": 1, "reference": "ORDER-002",
"service_type": "ground_advantage", "status": "completed",
"tracking_number": "9400111899223100067890"
}
],
"labels_pdf_url": "https://app.example.com/storage/<token>/",
"csv_url": "https://app.example.com/storage/<token>/"
},
"error": null,
"message": "OK"
}{
"success": true,
"data": {
"order_id": "6615b1c2d3e4f5a6b7c8d9e0",
"order_type": "bulk",
"status": "partial",
"label_count": 2,
"created_at": "2025-04-10T15:16:18+00:00",
"completed_at": "2025-04-10T15:16:42+00:00",
"price": 16.50,
"labels": [
{
"index": 0, "reference": "ORDER-001",
"service_type": "priority_mail", "status": "completed",
"tracking_number": "9400111899223100012345"
},
{
"index": 1, "reference": "ORDER-002",
"service_type": "ground_advantage", "status": "failed",
"error": "Carrier rejected address"
}
],
"labels_pdf_url": "https://app.example.com/storage/<token>/"
},
"error": null,
"message": "OK"
}Get Batch Status
Poll the status of a confirmed batch's bulk order. A confirmed batch maps to exactly one bulk order, so
this returns that order's status and its full per-label serialization under order.
Equivalent to GET /gateway/v1/orders/{order_id}/ for the batch's order.
Order Statuses
| Status | Meaning |
|---|---|
| pending | The bulk order is still pending or processing |
| completed | All labels completed successfully |
| partial | Some labels completed, some failed |
| failed | All labels failed |
curl https://app.example.com/gateway/v1/batches/batch_6615a3f2e4b0c1d2e3f4a5b6/ \ -H "Authorization: Bearer sk_live_your_api_key_here"
import time batch_id = "batch_6615a3f2e4b0c1d2e3f4a5b6" interval = 5 # 5-second fixed interval for batches timeout = 300 # 5-minute timeout elapsed = 0 while elapsed < timeout: response = requests.get( f"{BASE_URL}/batches/{batch_id}/", headers=headers, ) data = response.json()["data"] if data["status"] in ("completed", "partial", "failed"): print(f"Batch {data['status']}:") for label in data["order"]["labels"]: print(f" {label['reference']}: {label.get('status')} " f"{label.get('tracking_number', '')}") break time.sleep(interval) elapsed += interval else: print("Timed out — check back later")
const batchId = "batch_6615a3f2e4b0c1d2e3f4a5b6"; const interval = 5000; const timeout = 300000; let elapsed = 0; while (elapsed < timeout) { const res = await fetch( `${BASE_URL}/batches/${batchId}/`, { headers: { "Authorization": `Bearer ${API_KEY}` } } ); const { data } = await res.json(); if (["completed", "partial", "failed"].includes(data.status)) { console.log(`Batch ${data.status}:`, data.order.labels); break; } await new Promise((r) => setTimeout(r, interval)); elapsed += interval; }
$batchId = "batch_6615a3f2e4b0c1d2e3f4a5b6"; $interval = 5; $timeout = 300; $elapsed = 0; while ($elapsed < $timeout) { $ch = curl_init("$baseUrl/batches/$batchId/"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer $apiKey", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $data = json_decode(curl_exec($ch), true)["data"]; if (in_array($data["status"], ["completed", "partial", "failed"])) { echo "Batch {$data['status']}\n"; print_r($data["order"]["labels"]); break; } sleep($interval); $elapsed += $interval; }
batch_id = "batch_6615a3f2e4b0c1d2e3f4a5b6" interval = 5 timeout = 300 elapsed = 0 while elapsed < timeout uri = URI("#{BASE_URL}/batches/#{batch_id}/") req = Net::HTTP::Get.new(uri) req["Authorization"] = "Bearer #{API_KEY}" res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } data = JSON.parse(res.body)["data"] if ["completed", "partial", "failed"].include?(data["status"]) puts "Batch #{data['status']}: #{data['order']['labels']}" break end sleep(interval) elapsed += interval end
var batchId = "batch_6615a3f2e4b0c1d2e3f4a5b6"; var interval = 5000; var timeout = 300000; var elapsed = 0; while (elapsed < timeout) { var response = await client.GetAsync( $"{baseUrl}/batches/{batchId}/" ); var json = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(json); var status = doc.RootElement .GetProperty("data") .GetProperty("status") .GetString(); if (status == "completed" || status == "partial" || status == "failed") { Console.WriteLine(json); break; } await Task.Delay(interval); elapsed += interval; }
{
"success": true,
"data": {
"batch_id": "batch_6615a3f2e4b0c1d2e3f4a5b6",
"order_id": "6615b1c2d3e4f5a6b7c8d9e0",
"status": "completed",
"order": {
"order_id": "6615b1c2d3e4f5a6b7c8d9e0",
"order_type": "bulk",
"status": "completed",
"label_count": 2,
"labels": [
{ "index": 0, "reference": "ORDER-001", "status": "completed", "tracking_number": "9400111899223100012345" },
{ "index": 1, "reference": "ORDER-002", "status": "completed", "tracking_number": "9400111899223100067890" }
],
"labels_pdf_url": "https://app.example.com/storage/<token>/",
"csv_url": "https://app.example.com/storage/<token>/"
}
},
"error": null,
"message": "OK"
}List Orders
Retrieve a paginated list of your orders with optional filters. Useful for building order history views or syncing data to your system.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| pageoptional | integer | Page number (default: 1) |
| per_pageoptional | integer | Results per page (default: 20, max: 100) |
| statusoptional | string | Filter by status: pending, completed, failed, etc. |
| sort_byoptional | string | created_at, status, or updated_at (default: created_at) |
| sort_orderoptional | string | asc or desc (default: desc) |
| date_fromoptional | string | ISO 8601 date filter (e.g., 2025-04-01) |
| date_tooptional | string | ISO 8601 end date (inclusive) |
| referenceoptional | string | Search by your reference ID (partial match) |
# List completed orders from the last 7 days curl "https://app.example.com/gateway/v1/orders/?status=completed&date_from=2025-04-03&per_page=50" \ -H "Authorization: Bearer sk_live_your_api_key_here"
response = requests.get( f"{BASE_URL}/orders/", headers=headers, params={ "status": "completed", "date_from": "2025-04-03", "per_page": 50, }, ) data = response.json() print(f"Total: {data['total']}, Page: {data['page']}") for order in data["items"]: print(f" {order['order_id']} — {order['status']}")
const params = new URLSearchParams({ status: "completed", date_from: "2025-04-03", per_page: "50", }); const response = await fetch( `${BASE_URL}/orders/?${params}`, { headers: { "Authorization": `Bearer ${API_KEY}` } } ); const data = await response.json(); console.log(`Total: ${data.total}, Page: ${data.page}`); data.items.forEach((o) => console.log(` ${o.order_id} — ${o.status}`) );
$query = http_build_query([ "status" => "completed", "date_from" => "2025-04-03", "per_page" => 50, ]); $ch = curl_init("$baseUrl/orders/?$query"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer $apiKey", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $data = json_decode(curl_exec($ch), true); echo "Total: {$data['total']}, Page: {$data['page']}\n"; foreach ($data["items"] as $order) { echo " {$order['order_id']} — {$order['status']}\n"; }
uri = URI("#{BASE_URL}/orders/") uri.query = URI.encode_www_form( status: "completed", date_from: "2025-04-03", per_page: 50 ) req = Net::HTTP::Get.new(uri) req["Authorization"] = "Bearer #{API_KEY}" res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } data = JSON.parse(res.body) puts "Total: #{data['total']}, Page: #{data['page']}" data["items"].each { |o| puts " #{o['order_id']} — #{o['status']}" }
var response = await client.GetAsync( $"{baseUrl}/orders/?status=completed&date_from=2025-04-03&per_page=50" ); var json = await response.Content.ReadAsStringAsync(); Console.WriteLine(json);
// Gateway-created orders are bulk (order_type: "bulk"). List items carry the // per-label breakdown; file URLs are omitted from list responses. { "items": [ { "order_id": "6615b1c2d3e4f5a6b7c8d9e0", "order_type": "bulk", "status": "completed", "label_count": 2, "created_at": "2025-04-10T15:16:18+00:00", "completed_at": "2025-04-10T15:16:42+00:00", "price": 16.50, "labels": [ { "index": 0, "reference": "ORDER-001", "service_type": "priority_mail", "status": "completed", "tracking_number": "9400111899223100012345" }, { "index": 1, "reference": "ORDER-002", "service_type": "ground_advantage", "status": "completed", "tracking_number": "9400111899223100067890" } ] } ], "total": 47, "page": 1, "per_page": 50 }
Retrieve Past Order
Retrieve full details for any order, including the per-label breakdown, tracking numbers, file download URLs, and error data. Unlike the status endpoint, this always includes full details regardless of order status.
Response Includes
labels— Per-label breakdown:reference,service_type, and (once processed)tracking_number,status, anderrorlabels_pdf_url,csv_url,invoice_url— Top-level proxy-wrapped download URLs (24-hour expiry by default, configurable per deployment) for the merged label PDF, manifest CSV, and invoiceerror_data— If the order failed, contains the processor error details
order_type: "bulk"), so tracking numbers live per-label
inside labels[] and file URLs are top-level (there is no order_details or files wrapper object).
curl https://app.example.com/gateway/v1/orders/6615b1c2d3e4f5a6b7c8d9e0/details \ -H "Authorization: Bearer sk_live_your_api_key_here"
order_id = "6615b1c2d3e4f5a6b7c8d9e0" response = requests.get( f"{BASE_URL}/orders/{order_id}/details", headers=headers, ) data = response.json()["data"] print(f"Status: {data['status']}") for label in data["labels"]: print(f" {label['reference']}: {label.get('tracking_number', '')}") if data.get("labels_pdf_url"): print(f"Labels PDF: {data['labels_pdf_url']}")
const orderId = "6615b1c2d3e4f5a6b7c8d9e0"; const response = await fetch( `${BASE_URL}/orders/${orderId}/details`, { headers: { "Authorization": `Bearer ${API_KEY}` } } ); const { data } = await response.json(); console.log("Status:", data.status); data.labels.forEach((l) => console.log(` ${l.reference}: ${l.tracking_number || ""}`)); if (data.labels_pdf_url) console.log("Labels PDF:", data.labels_pdf_url);
$orderId = "6615b1c2d3e4f5a6b7c8d9e0"; $ch = curl_init("$baseUrl/orders/$orderId/details"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer $apiKey", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $data = json_decode(curl_exec($ch), true)["data"]; echo "Status: {$data['status']}\n"; foreach ($data["labels"] as $l) { echo " {$l['reference']}: {$l['tracking_number']}\n"; } if (isset($data["labels_pdf_url"])) { echo "Labels PDF: {$data['labels_pdf_url']}\n"; }
order_id = "6615b1c2d3e4f5a6b7c8d9e0" uri = URI("#{BASE_URL}/orders/#{order_id}/details") req = Net::HTTP::Get.new(uri) req["Authorization"] = "Bearer #{API_KEY}" res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } data = JSON.parse(res.body)["data"] puts "Status: #{data['status']}" data["labels"].each { |l| puts " #{l['reference']}: #{l['tracking_number']}" } puts "Labels PDF: #{data['labels_pdf_url']}" if data["labels_pdf_url"]
var orderId = "6615b1c2d3e4f5a6b7c8d9e0"; var response = await client.GetAsync( $"{baseUrl}/orders/{orderId}/details" ); var json = await response.Content.ReadAsStringAsync(); Console.WriteLine(json);
{
"success": true,
"data": {
"order_id": "6615b1c2d3e4f5a6b7c8d9e0",
"order_type": "bulk",
"status": "completed",
"label_count": 2,
"created_at": "2025-04-10T15:16:18+00:00",
"completed_at": "2025-04-10T15:16:42+00:00",
"price": 16.50,
"labels": [
{ "index": 0, "reference": "ORDER-001", "service_type": "priority_mail", "status": "completed", "tracking_number": "9400111899223100012345" },
{ "index": 1, "reference": "ORDER-002", "service_type": "ground_advantage", "status": "completed", "tracking_number": "9400111899223100067890" }
],
"labels_pdf_url": "https://app.example.com/storage/<token>/",
"csv_url": "https://app.example.com/storage/<token>/",
"error_data": null
},
"error": null,
"message": "OK"
}Two-Phase Ordering
The Gateway API uses a validate-then-confirm workflow. This gives you a chance to review corrected addresses, check pricing, and handle errors before any credits are deducted.
Workflow
- Validate —
POST /orders/validatewith your orders. Each order is individually validated (address, dimensions, pricing, dedup). You get back abatch_idand per-order results. - Review — Check the response. Fix any
failedorders. Reviewcorrectedaddresses. Verify theestimated_total_price. - Confirm —
POST /orders/confirm/{batch_id}. Allpassedandcorrectedrows are created as a single bulk order, charged once. You get back oneorder_id; rows that failed validation are returned inrejected. - Poll — Use
GET /orders/{order_id}/orGET /batches/{batch_id}/to check when labels are ready and read per-label tracking numbers. - Download — When an order is
completed, the response includes proxy-wrapped download URLs for the label PDF (24-hour expiry by default, configurable per deployment).
corrected, the corrected addresses are automatically applied
when you confirm. You do not need to resubmit with corrected data.
Batch Expiry
Validated batches expire after 15 minutes. If the batch expires before you confirm,
you will receive a BATCH_EXPIRED error. Simply re-validate your orders.
Pricing is locked at validation time, so confirming within the window guarantees the quoted price.
Failed Orders Are Terminal
If an order moves to failed status after confirmation (the label processor could not generate a label),
it cannot be retried or reprocessed. You must create a new order. The credits for failed orders are automatically
refunded to your balance.
Validation Results
Each order in the validation response has a result field with one of three values:
| Result | Meaning | Action |
|---|---|---|
| passed | All validation passed, address verified as-is | Ready to confirm |
| corrected | Address was auto-corrected by the verification service | Review the verification field, then confirm |
| failed | Validation failed — see error and details |
Fix the issue and re-validate the order |
When you confirm a batch, only passed and corrected orders are created.
Failed orders are skipped. You can include fixed versions of failed orders in a new validation batch.
Package Requirements
All package dimensions and weight are validated against USPS limits. These constraints are enforced at validation time to prevent errors at label generation.
| Constraint | Limit | Error Code |
|---|---|---|
| Minimum weight | 0.1 lbs (1.6 oz) | WEIGHT_TOO_LIGHT |
| Maximum weight | 70 lbs (1120 oz) | WEIGHT_EXCEEDED |
| Maximum volume | 13,000 cubic inches | VOLUME_EXCEEDED |
| Maximum single dimension | 108 inches | DIMENSION_EXCEEDED |
| Minimum dimension | 0.1 inches | DIMENSION_TOO_SMALL |
| Length + girth | 165 inches max | LENGTH_GIRTH_EXCEEDED |
| Ground Advantage weight | 15.99 oz max | GROUND_ADVANTAGE_WEIGHT_EXCEEDED |
Service Types
| Value | Description | Weight Range |
|---|---|---|
| priority_mail | USPS Priority Mail (1–3 business days) | 1.6 – 1120 oz |
| ground_advantage | USPS Ground Advantage (2–5 business days) | 1.6 – 15.99 oz |
2 * (width + height). The length + girth constraint
is length + 2 * (width + height) ≤ 165.
Address Verification
During validation, both ship-from and ship-to addresses are verified against USPS data via the StreetVerify service. The verification can produce three outcomes:
| Status | Meaning |
|---|---|
| verified | Address matches USPS records exactly |
| corrected | Address was auto-corrected (e.g., ZIP+4 added, abbreviation standardized). The response includes both original and corrected values. |
| unverified | Address could not be verified — likely invalid or undeliverable. A reason field is included, and the order's result is failed with error ADDRESS_VERIFICATION_FAILED. |
| pending | Verification service temporarily unavailable. The order proceeds without verification. |
| skipped | Verification disabled for this direction in domain settings |
Address Field Limits
| Field | Limit | Error Code |
|---|---|---|
| Address line 1 | 32 chars, required | ADDR_LINE1_TOO_LONG |
| Address line 2 | 32 chars, optional | ADDR_LINE2_TOO_LONG |
| City | 24 chars, required | CITY_TOO_LONG |
| State | 2-letter code | INVALID_STATE |
| ZIP | 5 or 9 digits | INVALID_ZIP |
| Name | 32 chars max | NAME_TOO_LONG |
| Company | 40 chars, optional | COMPANY_TOO_LONG |
Error Codes
Complete reference of error codes returned by the Gateway API. All codes use UPPERCASE_SNAKE_CASE format.
Authentication Errors
| Code | HTTP | When |
|---|---|---|
| AUTH_MISSING | 401 | No Authorization header provided |
| AUTH_TOKEN_INVALID | 401 | Bearer token is present but not in the sk_live_ key format |
| INVALID_API_KEY | 401 | Key has the right format but is unknown, revoked, or deactivated |
| ACCOUNT_SUSPENDED | 403 | Account is deactivated (details: suspended_at, reason) |
| FEATURE_DISABLED | 403 | The external API is not enabled for your domain (details.feature) |
Validation Errors
| Code | HTTP | When |
|---|---|---|
| VALIDATION_ERROR | 400 | One or more fields failed validation — see details.errors[]. Also returned for a malformed JSON body. |
| ORDERS_REQUIRED | 400 | The request body is missing the top-level orders array |
| ADDR_LINE1_REQUIRED | 400 | Address line 1 is missing |
| ADDR_LINE1_TOO_LONG | 400 | Address line 1 exceeds 32 characters |
| INVALID_STATE | 400 | Not a valid 2-letter US state code |
| INVALID_ZIP | 400 | ZIP code must be 5 or 9 digits |
| CITY_TOO_LONG | 400 | City exceeds 24 characters |
| NAME_TOO_LONG | 400 | Name exceeds 32 characters |
| COMPANY_TOO_LONG | 400 | Company exceeds 40 characters |
Order Errors
| Code | HTTP | When |
|---|---|---|
| WEIGHT_EXCEEDED | 400 | Package exceeds 70 lbs (1120 oz) |
| WEIGHT_TOO_LIGHT | 400 | Package under 0.1 lbs (1.6 oz) |
| VOLUME_EXCEEDED | 400 | Package exceeds 13,000 cubic inches |
| DIMENSION_EXCEEDED | 400 | A dimension exceeds 108 inches |
| LENGTH_GIRTH_EXCEEDED | 400 | Length + girth exceeds 165 inches |
| GROUND_ADVANTAGE_WEIGHT_EXCEEDED | 400 | Ground Advantage max is 15.99 oz |
| DUPLICATE_ORDER | 409 | Matching order within 1-hour window. Pass confirm_duplicate: true to bypass. |
| STATE_NOT_ALLOWED | 400 | Ship-from or ship-to state not in the account's allowlist (checked on both; details.field is ship_from.state or ship_to.state) |
| STATE_BLOCKED | 400 | Ship-from or ship-to state in the account's blocklist (checked on both directions) |
| SERVICE_NOT_ENABLED | 422 | The requested service_type is disabled for your account. Per-order at validate (in details.service_type); enforced again at confirm. |
| CARRIER_NOT_ENABLED | 422 | The service's carrier is not enabled for your domain. Surfaces at confirm when the tenant has an empty carrier set. |
| ORDER_CREATION_FAILED | 422 | System failed to create the order (e.g. no pricing available for the service) |
Batch Errors
| Code | HTTP | When |
|---|---|---|
| BATCH_TOO_LARGE | 400 | Batch exceeds max labels per request (default: 100) |
| BATCH_EXPIRED | 400 | Batch expired (15-minute window). Re-validate. |
| BATCH_ALREADY_CONFIRMED | 409 | Batch was already confirmed |
Billing Errors
| Code | HTTP | When |
|---|---|---|
| INSUFFICIENT_BALANCE | 422 | Balance too low. Details include required_amount, current_balance, shortfall. |
| DAILY_ORDER_LIMIT_EXCEEDED | 422 | Exceeded daily order creation limit |
| SINGLE_ORDER_LIMIT_EXCEEDED | 422 | Order cost exceeds the per-order spend limit |
Address Verification Errors
| Code | HTTP | When |
|---|---|---|
| ADDRESS_VERIFICATION_FAILED | 400 | Address could not be verified — likely undeliverable |
General Errors
| Code | HTTP | When |
|---|---|---|
| RESOURCE_NOT_FOUND | 404 | Order or batch not found (or belongs to another account) |
| RATE_LIMITED | 429 | Too many requests — check Retry-After header |
| INTERNAL_SERVER_ERROR | 500 | Unexpected server error — contact support |
Webhooks
GET /orders/{id}/ and GET /batches/{id}/)
to check order status.