API v1
ShipStack API reference
A JSON REST API for creating orders, quoting rates, buying labels, tracking shipments, managing inventory and returns. All requests use HTTPS against https://shipstack.app/api/v1. Amounts are integer cents; weights are ounces and dimensions inches. The machine-readable spec is at /api/v1/openapi.json (OpenAPI 3.1).
Authentication
Create keys in Settings → API (requires the API keys permission and a plan with API access). Send the key as a bearer token. The full key is shown once; ShipStack stores only its SHA-256 hash.
curl https://shipstack.app/api/v1/orders \
-H "Authorization: Bearer ss_live_…"ss_live_…— full access within the key's scopes.ss_test_…— same data, butPOST /labelsonly works for test-mode (sandbox) shipments, so a test integration can never spend money.
Scopes
orders:readRead ordersorders:writeCreate & cancel ordersshipments:readRead shipmentsrates:readGet rates (creates a shipment)labels:writePurchase & void labelstracking:readRead trackinginventory:readRead inventoryinventory:writeAdjust inventorywarehouses:readRead warehousesreturns:readRead returnsreturns:writeCreate returnsErrors
Every error uses the same envelope. Branch on type, show message, and include request_id (also in the X-Request-Id header) when contacting support.
{
"error": {
"type": "invalid_request_error",
"code": "validation_error",
"message": "parcel.weight_oz: Too small: expected number to be >0",
"param": "parcel.weight_oz",
"request_id": "req_4Zb1yS0a9vQhJx2m"
}
}| type | HTTP | Meaning |
|---|---|---|
invalid_request_error | 400 / 422 | The request was malformed or failed validation. `param` names the offending field. |
authentication_error | 401 | Missing, malformed, expired or revoked API key. |
permission_error | 403 | The key is valid but lacks the scope required for this endpoint, or the organization is suspended. |
plan_error | 402 | The organization's plan doesn't include API access (or the subscription is inactive). |
not_found_error | 404 | The object doesn't exist in this organization. |
conflict_error | 409 | The request conflicts with the object's current state (e.g. cancelling a shipped order). |
idempotency_error | 409 / 400 | Idempotency-Key reused with a different body, still in progress, or missing where required. |
rate_limit_error | 429 | Too many requests for this key. Retry after the X-RateLimit-Reset time. |
provider_error | 409 / 502 | The shipping provider isn't configured or returned an error. Nothing was charged. |
api_error | 500 | Something went wrong on our side. Safe to retry with the same Idempotency-Key. |
Idempotency
Send an Idempotency-Key header (any unique string up to 255 characters, e.g. a UUID) on POST requests to retry safely after a timeout. It is required for POST /labels. Keys are scoped to your organization and endpoint.
- • Same key + same body → the original status and body are replayed, with
Idempotent-Replayed: true. - • Same key + different body →
409 idempotency_key_reused. - • Same key while the first request is still running →
409 idempotency_request_in_progress; retry shortly. - • 5xx responses aren't stored, so retrying with the same key re-attempts the request.
curl -X POST https://shipstack.app/api/v1/labels \
-H "Authorization: Bearer ss_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f0b1c1e-2a47-4f7e-9d0e-6f1f6b0b7a21" \
-d '{"shipment_id":"…","rate_id":"…","label_format":"PDF_4x6","confirmed_amount_cents":842}'Pagination
List endpoints return newest first in a list envelope. Pass limit (1–100, default 25) and, for the next page, starting_after set to the previous page's next_cursor.
{
"object": "list",
"data": [ { "id": "…", "object": "order", … }, { "id": "8c2e…", "object": "order", … } ],
"has_more": true,
"next_cursor": "8c2e…",
"url": "/api/v1/orders"
}Rate limits
120 requests per 60 seconds per API key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). Over the limit you get 429 rate_limit_error with Retry-After.
Webhooks
Add HTTPS endpoints in Settings → API. Each event is POSTed as JSON with a ShipStack-Signature header. Respond 2xx within 10 seconds; anything else is retried with exponential backoff (30 s, 2 m, 8 m, 32 m, ~2 h, then every ~6 h) for 8 attempts, after which the delivery is marked dead. You can retry from the dashboard. Deduplicate on the event id — retries reuse it.
order.created | An order was created (dashboard, import, API or store sync). |
shipment.label_purchased | A shipping label was purchased for a shipment. |
shipment.tracking_updated | The carrier reported a new tracking status. |
shipment.delivered | The carrier reported the shipment delivered. |
return.created | A return (RMA) was created. |
ShipStack-Signature: t=1760000000,v1=<hex HMAC-SHA256 of "1760000000.<raw body>">
ShipStack-Event-Id: evt_…
ShipStack-Event-Type: shipment.label_purchased{
"id": "evt_…",
"object": "event",
"type": "shipment.label_purchased",
"created": 1760000000,
"data": { "object": { "id": "…", "object": "shipment", … } }
}import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the exact request body string (don't re-serialize parsed JSON)
export function verifyShipStack(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}Endpoints
/api/v1/ordersList orders
orders:readPaginatedParameters
limitquery | 1–100, default 25 |
starting_afterquery | next_cursor from the previous page |
statusquery | Filter by status. "pending" | "awaiting_fulfillment" | "processing" | "label_created" | "shipped" | "delivered" | "cancelled" | "exception" |
sourcequery | Filter by source. "manual" | "csv" | "shopify" | "woocommerce" | "api" |
external_idquery | Exact external id. string |
Responses
200A page of orders, newest first.OrderList- Errors use the error envelope.
/api/v1/ordersCreate an order
Creates an order with source `api`, matches SKUs to your catalog, runs automation rules and reserves inventory at the assigned warehouse. Idempotent on `external_id`.
orders:writeIdempotency-Key optionalRequest body
external_id | string | nullmax 200 charsYour id for the order. Re-sending the same external_id returns the existing order (200) instead of creating a duplicate. | ||||||||||
order_number | string | nullmax 64 charsDefaults to the next SS-#### number. | ||||||||||
ordered_at | string | null (date-time) | ||||||||||
currency | string | nullmax 3 chars | ||||||||||
customerrequired | object
| ||||||||||
ship_torequired | AddressInput | ||||||||||
itemsrequired | object[]min 1 items, max 500 items
| ||||||||||
shipping_paid_cents | integer≥ 0 | ||||||||||
tax_cents | integer≥ 0 | ||||||||||
requested_service | string | nullmax 120 chars | ||||||||||
tags | string[]max 30 items | ||||||||||
notes | string | nullmax 5000 chars | ||||||||||
warehouse_id | string | null (uuid) |
Responses
200An order with this external_id already existed; it is returned unchanged.Order201Created.Order- Errors use the error envelope.
/api/v1/orders/{id}Retrieve an order
orders:readParameters
idpath | Order id |
Responses
200The order.Order- Errors use the error envelope.
/api/v1/orders/{id}/cancelCancel an order
Cancels the order and releases reserved inventory. Fails with 409 `label_exists` while a non-voided label exists.
orders:writeIdempotency-Key optionalParameters
idpath | Order id |
Request body
reason | string | nullmax 500 chars |
Responses
200The cancelled order.Order- Errors use the error envelope.
/api/v1/shipmentsList shipments
shipments:readPaginatedParameters
limitquery | 1–100, default 25 |
starting_afterquery | next_cursor from the previous page |
statusquery | Filter by shipment status. string |
order_idquery | Shipments for one order. string (uuid) |
Responses
200A page of shipments.ShipmentList- Errors use the error envelope.
/api/v1/shipments/{id}Retrieve a shipment
shipments:readParameters
idpath | Shipment id |
Responses
200The shipment, including its current label.Shipment- Errors use the error envelope.
/api/v1/ratesGet rates
Creates a shipment and returns the shipping provider's live rates, exactly as quoted (valid for 20 minutes). Without a connected Shippo/EasyPost account the built-in sandbox answers and every rate has `test_mode: true`.
rates:readIdempotency-Key optionalRequest body
order_id | string | null (uuid)Ship-to and warehouse default from the order. |
warehouse_id | string | null (uuid)Ship-from warehouse (defaults to the order's / org default). |
from_address | AddressInput | null |
to_address | AddressInput | null |
parcelrequired | Parcel |
customs | Customs | nullRequired when origin and destination countries differ. |
insurance_amount_cents | integer | null≥ 0, ≤ 1000000 |
signature_confirmation | boolean |
Responses
201The new shipment with its rates (cheapest first).ShipmentWithRates- Errors use the error envelope.
/api/v1/labelsPurchase a label
Buys the label for a rate. Charges your carrier/provider account in live mode. Requires an `Idempotency-Key` and `confirmed_amount_cents` equal to the rate. `ss_test_` keys can only buy test-mode labels.
labels:writeIdempotency-Key requiredRequest body
shipment_idrequired | string (uuid) |
rate_idrequired | string (uuid) |
label_format | "PDF_4x6" | "PDF" | "ZPLII" | "PNG"default "PDF_4x6" |
confirmed_amount_centsrequired | integer≥ 0Must equal the rate's amount_cents. Protects against paying a changed price. |
Responses
200This idempotent purchase already completed.Label201Purchased.Label202The provider hasn't confirmed yet; poll the shipment.PendingPurchase- Errors use the error envelope.
/api/v1/labels/{id}/voidVoid a label
Requests a refund for an unused label. Some carriers approve asynchronously (`void_requested`).
labels:writeIdempotency-Key optionalParameters
idpath | Label id |
Responses
200The label with the void result.VoidLabelResponse- Errors use the error envelope.
/api/v1/tracking/{tracking_number}Track a shipment
tracking:readParameters
tracking_numberpath | Carrier tracking number of one of your shipments |
Responses
200Status and carrier events, newest first.Tracking- Errors use the error envelope.
/api/v1/inventoryList inventory levels
inventory:readPaginatedParameters
limitquery | 1–100, default 25 |
starting_afterquery | next_cursor from the previous page |
skuquery | Exact SKU (case-insensitive). string |
warehouse_idquery | One warehouse. string (uuid) |
Responses
200A page of levels (ordered by id).InventoryLevelList- Errors use the error envelope.
/api/v1/inventory/adjustmentsAdjust inventory
Receive stock, apply a signed adjustment, or record a cycle count. Send an Idempotency-Key so retries can't double-apply.
inventory:writeIdempotency-Key optionalRequest body
product_id | string (uuid)Provide product_id or sku. |
sku | stringmax 64 chars |
warehouse_idrequired | string (uuid) |
moderequired | "receive" | "adjust" | "count"receive: add units · adjust: signed delta (note required) · count: set absolute on-hand |
quantityrequired | integer |
note | string | nullmax 500 chars |
Responses
201The updated level.InventoryLevel- Errors use the error envelope.
/api/v1/warehousesList warehouses
warehouses:readPaginatedParameters
limitquery | 1–100, default 25 |
starting_afterquery | next_cursor from the previous page |
Responses
200A page of warehouses.WarehouseList- Errors use the error envelope.
/api/v1/returnsList returns
returns:readPaginatedParameters
limitquery | 1–100, default 25 |
starting_afterquery | next_cursor from the previous page |
statusquery | Filter by status. string |
Responses
200A page of returns.ReturnList- Errors use the error envelope.
/api/v1/returnsCreate a return
Creates an RMA for shipped items. Your return window and reason policy apply.
returns:writeIdempotency-Key optionalRequest body
order_idrequired | string (uuid) | ||||
reason | "damaged" | "wrong_item" | "not_as_described" | "no_longer_needed" | "defective" | "other" | null | ||||
itemsrequired | object[]min 1 items
| ||||
customer_note | string | nullmax 2000 chars | ||||
refund_requested | boolean |
Responses
201Created.Return- Errors use the error envelope.
/api/v1/returns/{id}Retrieve a return
returns:readParameters
idpath | Return id |
Responses
200The return.Return- Errors use the error envelope.
Objects
Order
id | string (uuid) |
object | "order" |
order_number | string |
status | "pending" | "awaiting_fulfillment" | "processing" | "label_created" | "shipped" | "delivered" | "cancelled" | "exception" |
source | "manual" | "csv" | "shopify" | "woocommerce" | "api" |
external_id | string | null |
integration_id | string | null (uuid) |
currency | string |
subtotal_cents | integer |
shipping_paid_cents | integer |
tax_cents | integer |
total_cents | integer |
requested_service | string | null |
tags | string[] |
warehouse_id | string | null (uuid) |
requires_approval | boolean |
customer | object | null |
ship_to | Address | null |
items | OrderItem[] |
ordered_at | string | null (date-time) |
shipped_at | string | null (date-time) |
delivered_at | string | null (date-time) |
cancelled_at | string | null (date-time) |
created_at | string | null (date-time) |
updated_at | string | null (date-time) |
Shipment
id | string (uuid) | ||||||||
object | "shipment" | ||||||||
order_id | string | null (uuid) | ||||||||
return_id | string | null (uuid) | ||||||||
direction | "outbound" | "return" | ||||||||
status | "draft" | "rated" | "purchasing" | "label_purchased" | "in_transit" | "out_for_delivery" | "delivered" | "exception" | "returned_to_sender" | "void_requested" | "voided" | "failed" | ||||||||
provider | string | null | ||||||||
test_mode | boolean | ||||||||
from_address | Address | ||||||||
to_address | Address | ||||||||
parcel | object
| ||||||||
carrier | string | null | ||||||||
service_code | string | null | ||||||||
service_name | string | null | ||||||||
rate_cents | integer | null | ||||||||
currency | string | ||||||||
tracking_number | string | null | ||||||||
tracking_url | string | null | ||||||||
tracking_status | string | null | ||||||||
estimated_delivery_at | string | null (date-time) | ||||||||
shipped_at | string | null (date-time) | ||||||||
delivered_at | string | null (date-time) | ||||||||
label | Label | null | ||||||||
created_at | string | null (date-time) | ||||||||
updated_at | string | null (date-time) |
Rate
id | string (uuid) |
object | "rate" |
shipment_id | string (uuid) |
provider | string |
carrier | string |
service_code | string |
service_name | string |
amount_cents | integer |
currency | string |
estimated_days | integer | null |
duration_terms | string | null |
attributes | string[] |
test_mode | boolean |
retrieved_at | string | null (date-time) |
expires_at | string | null (date-time) |
Label
id | string (uuid) |
object | "label" |
shipment_id | string (uuid) |
status | "purchased" | "void_requested" | "voided" | "void_rejected" |
format | "PDF" | "PDF_4x6" | "ZPLII" | "PNG" |
label_url | string | null |
cost_cents | integer |
currency | string |
test_mode | boolean |
provider | string |
tracking_number | string | null |
tracking_url | string | null |
carrier | string | null |
service_code | string | null |
purchased_at | string | null (date-time) |
void_requested_at | string | null (date-time) |
voided_at | string | null (date-time) |
void_message | string | null |
Tracking
object | "tracking" | ||||||||||||||||||
tracking_number | string | ||||||||||||||||||
carrier | string | null | ||||||||||||||||||
status | string | ||||||||||||||||||
shipment_id | string (uuid) | ||||||||||||||||||
order_id | string | null (uuid) | ||||||||||||||||||
test_mode | boolean | ||||||||||||||||||
estimated_delivery_at | string | null (date-time) | ||||||||||||||||||
delivered_at | string | null (date-time) | ||||||||||||||||||
last_updated_at | string | null (date-time) | ||||||||||||||||||
events | object[]
|
InventoryLevel
id | string (uuid) |
object | "inventory_level" |
product_id | string (uuid) |
sku | string |
name | string |
warehouse_id | string (uuid) |
warehouse_code | string |
on_hand | integer |
reserved | integer |
available | integer |
bin_location | string | null |
updated_at | string | null (date-time) |
Warehouse
id | string (uuid) |
object | "warehouse" |
name | string |
code | string |
is_default | boolean |
active | boolean |
address | Address |
created_at | string | null (date-time) |
Return
id | string (uuid) | ||||||||||||
object | "return" | ||||||||||||
rma_number | string | ||||||||||||
order_id | string (uuid) | ||||||||||||
status | "requested" | "authorized" | "label_created" | "in_transit" | "received" | "restocked" | "closed" | "rejected" | ||||||||||||
reason | string | ||||||||||||
customer_note | string | null | ||||||||||||
refund_status | string | ||||||||||||
restocking_fee_cents | integer | ||||||||||||
return_shipment_id | string | null (uuid) | ||||||||||||
items | object[]
| ||||||||||||
authorized_at | string | null (date-time) | ||||||||||||
received_at | string | null (date-time) | ||||||||||||
closed_at | string | null (date-time) | ||||||||||||
created_at | string | null (date-time) | ||||||||||||
updated_at | string | null (date-time) |