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.

Base URL https://app.{your-domain}/gateway/v1

All requests must include your API key in the Authorization header. All request and response bodies are JSON.

Quick Start

# 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.

Generate your API key from the Help & Support → API page in your dashboard (available once the External API feature is enabled for your account). Each account has a single API key, scoped to your account and domain. Generating a new key immediately replaces (rotates) the previous one — the old key stops working at once.

Header Format

API keys are prefixed with sk_live_ followed by 48 hexadecimal characters (56 characters total). Send the key as a Bearer token:

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

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
If your key is compromised, generate a new one from the dashboard — this immediately revokes the old key. All requests with the old key will then return 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);
200 — Authenticated response
{
  "success": true,
  "data": {
    "balance": 142.50,
    "daily_orders_used": 12,
    "max_bulk_labels": 100
  },
  "error": null,
  "message": "OK"
}
401 — Missing or invalid key
{
  "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

CodeMeaning
200Success — every Gateway endpoint (including validate and confirm) returns 200 on success
400Validation error — check error.details
401Missing or invalid API key
403Account suspended or insufficient permissions
404Resource not found
409Conflict — duplicate order, batch already confirmed
422Insufficient balance or unprocessable
429Rate limit exceeded — check Retry-After header
500Internal server error
Error response format
{
  "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"
}
Insufficient balance (422)
{
  "success": false,
  "data": null,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "details": {
      "required_amount": 45.50,
      "current_balance": 12.00,
      "shortfall": 33.50
    }
  },
  "message": "Insufficient balance"
}
Rate limited (429)
{
  "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.

ScopeLimit
All endpoints100 requests / minute per account

Rate Limit Headers

Every response includes rate limit headers so you can track your usage:

HeaderDescription
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)
Rate limit headers example
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1712150400
Content-Type: application/json
Handling rate limits (Python)
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.

GET /gateway/v1/wallet/

Response Fields

FieldTypeDescription
address string Ethereum wallet address (0x...)
network string Network identifier (always Ethereum (ERC-20))
You must generate a wallet from the dashboard before this endpoint will return an address. If no wallet exists, the API returns 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);
200 — Response
{
  "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.

GET /gateway/v1/account/

Response Fields

FieldTypeDescription
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")}");
200 — Response
{
  "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.

POST /gateway/v1/orders/validate

Request Body

FieldTypeDescription
ordersrequired array Array of order objects (max 100 per batch)

Order Object

FieldTypeDescription
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

FieldTypeDescription
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

FieldTypeDescription
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);
200 — Validation response
{
  "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"
}
Failed order in batch
// 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.

POST /gateway/v1/orders/confirm/{batch_id}

Path Parameters

ParameterTypeDescription
batch_idrequired string Batch ID from the validate response (e.g., batch_6615a3f2...)
Batches expire 15 minutes after validation. After expiry, you must re-validate. A batch can only be confirmed once — attempting to confirm again returns 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);
200 — Confirmation response
{
  "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"
}
400 — Batch expired
{
  "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.

GET /gateway/v1/orders/{order_id}/

Path Parameters

ParameterTypeDescription
order_idrequired string Order ID from the confirm response

Order Statuses

StatusDescription
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
Polling recommendation: Start polling every 2 seconds. Use exponential backoff (1.5x multiplier) up to a 30-second interval. Stop after 2 minutes and show a “check back later” message. For batches, use the Batch Status endpoint instead of polling each order individually.
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);
}
Pending response
{
  "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"
}
Completed response (per-label tracking)
{
  "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"
}
Partial response (some labels failed)
{
  "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.

GET /gateway/v1/batches/{batch_id}/

Order Statuses

StatusMeaning
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;
}
200 — Batch status response
{
  "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.

GET /gateway/v1/orders/

Query Parameters

ParameterTypeDescription
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);
200 — Paginated response
// 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.

GET /gateway/v1/orders/{order_id}/details

Response Includes

  • labels — Per-label breakdown: reference, service_type, and (once processed) tracking_number, status, and error
  • labels_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 invoice
  • error_data — If the order failed, contains the processor error details
Gateway orders are bulk orders (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);
200 — Full detail response
{
  "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

  1. ValidatePOST /orders/validate with your orders. Each order is individually validated (address, dimensions, pricing, dedup). You get back a batch_id and per-order results.
  2. Review — Check the response. Fix any failed orders. Review corrected addresses. Verify the estimated_total_price.
  3. ConfirmPOST /orders/confirm/{batch_id}. All passed and corrected rows are created as a single bulk order, charged once. You get back one order_id; rows that failed validation are returned in rejected.
  4. Poll — Use GET /orders/{order_id}/ or GET /batches/{batch_id}/ to check when labels are ready and read per-label tracking numbers.
  5. 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).
Tip: If an order is 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:

ResultMeaningAction
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.

ConstraintLimitError 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

ValueDescriptionWeight 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
Girth formula: 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:

StatusMeaning
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

FieldLimitError 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

CodeHTTPWhen
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

CodeHTTPWhen
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

CodeHTTPWhen
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

CodeHTTPWhen
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

CodeHTTPWhen
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

CodeHTTPWhen
ADDRESS_VERIFICATION_FAILED 400 Address could not be verified — likely undeliverable

General Errors

CodeHTTPWhen
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

Coming soon. Webhook support is planned for a future release. In the meantime, use the polling endpoints (GET /orders/{id}/ and GET /batches/{id}/) to check order status.