Relay | Fulfillment platform
Relay API reference
Complete reference for the Relay REST API, webhooks, and native integrations. Written for the engineers building against your fulfillment pipeline.
Getting started
The Relay API is a versioned REST API served over HTTPS. All requests and responses use JSON.
- Base URL
- https://anatar.com/api/v1
- Key format
- ak_live_... / ak_test_...
Authenticate every request with an API key in the Authorization header. Live keys start with ak_live_; sandbox keys start with ak_test_. Both are minted from Studio > Settings > Integrations. The full secret is shown exactly once at mint time - store it in your secret manager, never in source control. The sandbox endpoint is the same host - the token is the gate.
What this key can access
A key is scoped to one company. It can read inventory, create fulfillment orders, file inbound (live key), subscribe to webhooks, place and reorder Anatar production jobs, submit RFQs, and read invoices and payments. It cannot read another company, Studio files, or team membership. Responses never include internal UUIDs, label costs, postage margin, or label PDFs.
/api/v1/meAuth test. Any valid live or sandbox key. Returns mode (live or sandbox), the company name, and this key's scopes. No customer or key UUIDs.
curl https://anatar.com/api/v1/me \
-H "Authorization: Bearer $RELAY_API_KEY"{
"data": {
"mode": "sandbox",
"customer": {
"name": "Acme Industries",
"slug": "acme",
"customerNumber": "CUS-1042"
},
"key": {
"name": "Studio sandbox",
"prefix": "ak_test_a1b2",
"scopes": ["orders:read", "orders:write", "inventory:read", "inventory:write", "webhooks:manage"]
}
}
}API conventions
Every endpoint follows the same envelope, pagination, idempotency, and rate-limit rules.
Product identity
Every catalog item has a Relay product id (PRD-####). That is the identifier you should send and store. sku is an optional customer or UPC code and is often null - do not require it. Inventory, order lines, inbound, and webhooks all return both fields.
Recipient data
Ship-to name, address, phone, and email are recipient PII. They are only returned on authenticated partner routes, only for your company, and only the documented fields (no extra keys). Traffic is HTTPS on anatar.com. API keys are hashed at rest; integration secrets are AES-256-GCM encrypted.
Response envelope
Successful responses wrap the result in a data key. Errors return an error object with a human-readable message and optional structured details. List endpoints that paginate also return a meta object.
// Success
{ "data": { ... } }
// Success with pagination (list endpoints)
{ "data": [ ... ], "meta": { "limit": 100, "offset": 0, "total": 1284 } }
// Error
{ "error": { "message": "Order not found", "details": null } }Idempotency
Order and inbound creation accept externalId + externalSource. Reposting the same pair returns the existing record instead of creating a duplicate, which makes retries safe. Always set these from your own system of record (order id, PO number).
Rate limits
Limits (fixed 60-second window)
Authenticated300 req/min per keyrequired | Counted per API key across all endpoints. Exceeding it returns 429 with retryAfterSeconds in the error details. |
Unauthenticated30 req/min per IPrequired | Invalid or missing tokens are throttled per client IP before any database work. |
{
"error": {
"message": "Rate limit exceeded",
"details": { "limit": 300, "retryAfterSeconds": 42 }
}
}Scopes
Each key carries scopes. Calling an endpoint without the required scope returns 403. Default keys carry all scopes.
Available scopes
orders:readscopeoptional | List and fetch orders and tracking. |
orders:writescopeoptional | Create, reorder, cancel, and confirm delivery of orders. |
inventory:readscopeoptional | List inventory, product detail, movement history, inbound receipts. |
inventory:writescopeoptional | Create inbound shipments, archive products. |
webhooks:managescopeoptional | List, subscribe, and unsubscribe webhook endpoints. |
Order lifecycle
Orders move through a fixed pipeline. Each transition can emit a webhook (see the Webhooks section).
received -> allocated -> packed -> shipped -> delivered
| |
+------------+--> cancelled (only before shipment)Orders
Create fulfillment orders, poll their status, reorder to the same address, cancel before shipment, and confirm delivery.
/api/v1/ordersorders:readList orders newest-first with limit/offset pagination. Response includes a meta object with the total count.
Query parameters
limitintegeroptional | Page size, 1-200. Defaults to 100. |
offsetintegeroptional | Rows to skip. Defaults to 0. |
statusstringoptional | Filter by status: received, allocated, packed, shipped, delivered, cancelled. |
sinceISO 8601 timestampoptional | Only orders created at or after this time. Invalid timestamps return 400. |
shippedSinceISO 8601 timestampoptional | Only orders shipped at or after this time. meta.total is the exact count. Invalid timestamps return 400. |
curl "https://anatar.com/api/v1/orders?status=shipped&since=2026-09-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer $RELAY_API_KEY"/api/v1/orders/{numberOrId}orders:readFetch one order with lines and packaging. Use the order number (RLY-YYYYMM-####).
curl https://anatar.com/api/v1/orders/RLY-202609-0042 \
-H "Authorization: Bearer $RELAY_API_KEY"/api/v1/ordersorders:writeCreate a fulfillment order. Live keys enter the warehouse queue. Sandbox keys (ak_test_) create SBX- orders that never reserve stock or get picked; advance them with POST /orders/{number}/simulate.
Body
shipToobjectrequired | Destination address. See the field reference below. |
linesarray (1-100)required | Items to ship. Each line needs quantity plus productId (PRD-####, preferred) or sku. Most warehouse products only have a product id - sku is optional. |
externalIdstring (max 200)optional | Your system order id. Combined with externalSource for idempotency. |
externalSourcestring (max 100)optional | Origin system tag, e.g. "erp", "shopify", "zapier". |
notesstring (max 5000)optional | Free-form instructions for the warehouse. |
shipTo fields
line1string (max 200)required | Street address. |
citystring (max 100)required | City. |
statestring (max 100)required | State, province, or region. |
postalCodestring (max 20)required | Postal / ZIP code. |
countrystring (2 chars)required | ISO 3166-1 alpha-2 country code, e.g. US. |
namestring (max 200)optional | Recipient name. |
companystring (max 200)optional | Company name. |
line2string (max 200)optional | Suite, unit, floor. |
phonestring (max 30)optional | Contact phone for the carrier. |
emailstring (email)optional | Contact email for delivery notifications. |
curl -X POST https://anatar.com/api/v1/orders \
-H "Authorization: Bearer $RELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "SO-88412",
"externalSource": "erp",
"shipTo": {
"name": "Jordan Blake",
"company": "Acme Retail",
"line1": "500 Commerce Way",
"line2": "Suite 210",
"city": "Austin",
"state": "TX",
"postalCode": "78701",
"country": "US",
"email": "jordan@acmeretail.com"
},
"lines": [
{ "productId": "PRD-1010", "quantity": 25 },
{ "productId": "PRD-1011", "quantity": 25 }
],
"notes": "Include packing slip PO 88412"
}'{
"data": {
"number": "RLY-202609-0042",
"status": "received",
"externalId": "SO-88412",
"externalSource": "erp",
"shipTo": { "name": "Jordan Blake", "city": "Austin", "state": "TX", "country": "US", "postalCode": "78701", "line1": "500 Commerce Way" },
"notes": "Include packing slip PO 88412",
"addressValid": true,
"addressMessages": [],
"lines": [
{ "productId": "PRD-1010", "sku": null, "name": "Black Tee - Medium", "qty": 25, "qtyShipped": 0, "imageUrl": null },
{ "productId": "PRD-1011", "sku": null, "name": "Black Tee - Large", "qty": 25, "qtyShipped": 0, "imageUrl": null }
],
"packagingItems": [],
"package": null,
"tracking": null,
"trackerCode": "TRK7F2K9Q",
"trackerUrl": "https://anatar.com/track/TRK7F2K9Q",
"shippedAt": null,
"deliveredAt": null,
"cancelledAt": null,
"createdAt": "2026-09-03T14:02:11.000Z",
"updatedAt": "2026-09-03T14:02:11.000Z"
}
}/api/v1/addresses/validateorders:writePre-flight address check. Body is { addresses: [shipTo, ...] } (1-200). Results are index-aligned: valid true/false/null, messages, and a normalized shipTo when deliverable.
/api/v1/orders/batchorders:writeCreate up to 200 orders in one request. The body is { orders: [...] } where each entry matches the single-order schema. Each order is processed independently; the response reports per-order success or failure.
/api/v1/orders/{numberOrId}/reorderorders:writeCreate a new order shipping to the same address as an existing one. Send an empty body to clone the original lines exactly, or pass lines to change products or quantities and notes to replace the note. Pass externalId + externalSource to make the call idempotent. Returns the new order with status received.
/api/v1/orders/{numberOrId}/cancelorders:writeCancel an order by number and release any reserved inventory. Orders that already shipped cannot be cancelled - the call returns 409.
/api/v1/orders/{numberOrId}/deliverorders:writeConfirm delivery of a shipped order (for carriers without tracking integration). Emits order.delivered to your webhooks. Live keys only.
/api/v1/orders/{numberOrId}/simulateorders:writeSandbox keys only. Advance a test order to allocated, shipped, or delivered without touching warehouse stock, labels, or billing. Fires the matching sandbox webhook. Body: { status: allocated | shipped | delivered }.
Inventory
Live stock levels for every active product in your Relay catalog, including reserved and incoming quantities. Identify products by product id (PRD-####). SKU is optional and often empty.
/api/v1/inventoryinventory:readList active products that have stock on hand, reserved, or incoming.
curl https://anatar.com/api/v1/inventory \
-H "Authorization: Bearer $RELAY_API_KEY"Item fields
productIdstringrequired | Relay product id (PRD-####). Use this on order lines. |
skustring | nulloptional | Customer or UPC code when one exists. Often null. |
namestringrequired | Product display name. |
qtyOnHandintegerrequired | Physical units in the warehouse. |
qtyReservedintegerrequired | Units allocated to open orders. |
qtyAvailableintegerrequired | On hand minus reserved - what you can sell right now. |
qtyIncomingintegerrequired | Units expected from inbound shipments not yet received. |
lowStockbooleanrequired | True when available quantity is at or below the reorder point. |
/api/v1/inventory/{productId}inventory:readFetch one product by product id (PRD-####) or sku. Add ?include=movements for the 50 most recent inventory movements (receives, allocations, adjustments).
curl "https://anatar.com/api/v1/inventory/PRD-1010?include=movements" \
-H "Authorization: Bearer $RELAY_API_KEY"/api/v1/inventory/{productId}inventory:writeArchive a product. It stops appearing in listings and can no longer be ordered; movement history is preserved.
Inbound shipments
Tell the warehouse what stock is on the way. Received units are checked in at the dock and become available automatically.
/api/v1/inboundinventory:readList inbound receipts with their check-in status.
/api/v1/inboundinventory:writeCreate an inbound shipment (ASN). Identify existing catalog items with productId. To file a new incoming UPC, send sku plus name.
Body
linesarray (1-200)required | Each line needs quantity plus productId or sku. Use productId for catalog items. sku + name creates a new incoming UPC. |
trackingstring (max 200)optional | Carrier tracking number for the inbound freight. |
expectedOndate (YYYY-MM-DD)optional | Expected arrival date. |
notesstring (max 2000)optional | Instructions for receiving. |
externalIdstring (max 200)optional | Your PO or ASN number - idempotency key with externalSource. |
externalSourcestring (max 80)optional | Origin system tag. |
curl -X POST https://anatar.com/api/v1/inbound \
-H "Authorization: Bearer $RELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "PO-2091",
"externalSource": "erp",
"tracking": "1Z999AA10123456784",
"expectedOn": "2026-09-12",
"lines": [
{ "productId": "PRD-1010", "quantity": 500 },
{ "sku": "TEE-NVY-M", "name": "Navy Tee - Medium", "quantity": 300 }
]
}'Tracking
Every order gets a tracker code at creation. Use it for API lookups or hand the public tracking page straight to your customer.
/api/v1/tracking/{code}orders:readFetch shipment status by tracker code: order status, carrier, tracking number, ship and delivery timestamps, destination, and line items.
curl https://anatar.com/api/v1/tracking/TRK7F2K9Q \
-H "Authorization: Bearer $RELAY_API_KEY"https://anatar.com/track/{code}. Safe to embed in transactional email and order-status pages. It deep-links to the carrier's own tracking when available.Webhooks
Push notifications for order lifecycle events. Prefer webhooks over polling for anything user-facing.
Events
order.receivedeventoptional | Order accepted by Relay (created via API, Shopify, Zapier, or manually). |
order.allocatedeventoptional | Inventory reserved; order is ready to pick. |
order.shippedeventoptional | Label purchased and package handed to the carrier. Payload includes tracking. |
order.deliveredeventoptional | Carrier confirmed delivery. |
/api/v1/webhookswebhooks:manageList your active webhook subscriptions for this key's mode. A sandbox key only sees sandbox endpoints.
/api/v1/webhookswebhooks:manageSubscribe an HTTPS endpoint (plain http and private/internal hosts are rejected with 400). A sandbox key registers a sandbox endpoint that only receives test-order events. The signing secret is returned once in this response - store it immediately. Limit: 25 active subscriptions per mode.
curl -X POST https://anatar.com/api/v1/webhooks \
-H "Authorization: Bearer $RELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.acme.com/hooks/relay",
"events": ["order.shipped", "order.delivered"]
}'{
"data": {
"id": "3f1c8b7a-...",
"url": "https://api.acme.com/hooks/relay",
"events": ["order.shipped", "order.delivered"],
"active": true,
"source": "api",
"secret": "whsec_4f6e1a... // shown once - store it now"
}
}/api/v1/webhooks/{id}webhooks:manageUnsubscribe (deactivate) a webhook endpoint.
Delivery format
Deliveries are HTTP POSTs with a JSON body and three headers. Respond with any 2xx within 10 seconds to acknowledge.
Headers
X-Relay-Eventstringrequired | The event name, e.g. order.shipped. |
X-Relay-Signaturehex stringrequired | HMAC-SHA256 of the raw request body, keyed with your webhook secret. |
X-Relay-Deliveryuuidrequired | Unique delivery id. Use it to deduplicate redelivered events. |
{
"event": "order.shipped",
"order": {
"number": "RLY-202609-0042",
"status": "shipped",
"externalId": "SO-88412",
"externalSource": "erp",
"trackerCode": "TRK7F2K9Q",
"trackerUrl": "https://anatar.com/track/TRK7F2K9Q",
"tracking": {
"carrier": "usps",
"number": "9400111899560000000000",
"status": "PRE_TRANSIT",
"statusAt": "2026-09-04T18:22:40.000Z"
},
"shippedAt": "2026-09-04T18:22:40.000Z",
"lines": [
{ "productId": "PRD-1010", "sku": null, "name": "Black Tee - Medium", "qty": 25, "qtyShipped": 25, "imageUrl": null }
]
},
"tracking": { "number": "9400111899560000000000", "carrier": "usps" }
}Verifying signatures
const crypto = require("node:crypto");
function verifyRelaySignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signatureHeader, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express example - capture the raw body, verify, then parse
app.post("/hooks/relay", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.get("X-Relay-Signature") || "";
if (!verifyRelaySignature(req.body, signature, process.env.RELAY_WEBHOOK_SECRET)) {
return res.status(401).end();
}
const payload = JSON.parse(req.body.toString("utf8"));
// handle payload.event ...
res.status(200).end();
});Retry policy
Non-2xx responses and timeouts are retried with exponential backoff (2, 4, 8, ... minutes, capped at 120) for up to 8 attempts, after which the delivery is marked failed. Your handler should be idempotent - the same event can arrive more than once.
Shopify integration
Native two-way sync: Shopify orders flow into Relay for fulfillment, and Relay pushes tracking back to Shopify when the order ships.
Setup (under 2 minutes)
- Sign in to Studio > Settings > Integrations, enter your store domain, and click Connect with Shopify.
- Approve the Anatar Relay app on Shopify's consent screen. It requests only
read_ordersand fulfillment write access - nothing else. - Done. Order webhooks are registered automatically and the access token is stored encrypted (AES-256-GCM). No tokens to copy or manage.
- Make sure your Shopify variant SKUs match your Relay catalog SKUs - that mapping drives everything.
Prefer to run your own credentials? The Advanced option in Studio accepts a custom app token (scopes: read_orders, write_fulfillments, read_fulfillments, read_merchant_managed_fulfillment_orders, write_merchant_managed_fulfillment_orders).
What syncs
orders/createShopify -> Relayrequired | New paid orders are ingested as Relay fulfillment orders. Lines are matched by SKU; orders with no mappable SKUs are skipped. |
orders/cancelledShopify -> Relayrequired | Cancellations propagate to Relay when the order has not shipped yet. |
Fulfillment pushRelay -> Shopifyrequired | When Relay ships an order, the Shopify order is marked fulfilled with carrier and tracking number. |
Zapier
Connect Relay to 6,000+ apps without writing code. The Zapier app authenticates with the same API key as direct API access.
Available in Zapier
New Ordertriggeroptional | Fires when Relay accepts an order (order.received). |
Order Shippedtriggeroptional | Fires with tracking details when an order ships. |
Order Deliveredtriggeroptional | Fires when the carrier confirms delivery. |
Create Orderactionoptional | Creates a fulfillment order from any Zap. |
Find Inventory Itemsearchoptional | Looks up live stock for a SKU. |
One click adds Anatar Relay to your Zapier account, then authenticate with your API key - mint a dedicated key named "Zapier" in Studio > Settings > Integrations so you can revoke it independently. Triggers use REST hooks (webhook subscriptions under the hood), so they fire in seconds rather than on a polling schedule.
Production jobs
Anatar custom production (ORD-####). Warehouse shipments stay on /orders. Create and reorder require a live key - they open real work in Anatar OS.
/api/v1/jobsList this company's production jobs. Scope: jobs:read.
/api/v1/jobsLive keys only. Open an unpriced job. Body: { name?, notes?, lines: [{ description, quantity, detail? }], shipTo? }.
/api/v1/jobs/{number}One job by ORD-####. Lines, stage, tracker URL, related invoices.
/api/v1/jobs/{number}/reorderLive keys only. Clone the job into a new ORD- and a draft invoice. Optional body: { notes, lines: [{ description, quantity }] }.
Quotes and RFQ
Read sent quotes. Submit an RFQ with a live key. Anatar prices it and sends the quote; accept/decline stays on the public review URL.
/api/v1/quotesSent quotes plus your submitted RFQs. Scope: quotes:read.
/api/v1/quotesLive keys only. Submit an RFQ. Body: { name?, notes?, lines: [{ description, quantity, detail? }] }.
/api/v1/quotes/{number}Quote or RFQ detail, including reviewUrl once Anatar has sent it.
Billing
Customer-facing invoices and payments. Drafts stay in Anatar. Pay through the invoice payUrl.
/api/v1/invoicesOpen and paid invoices. Scope: billing:read.
/api/v1/invoices/{number}Invoice detail with lines, payUrl, and pdfUrl.
/api/v1/paymentsSucceeded payments and receipts.
Testing your integration
The sandbox is the same API host - it is gated by your sandbox token, not a public playground. Mint an ak_test_ key in Studio. Those requests never enter the warehouse queue, reserve stock, or bill. Live-only writes (inbound create, inventory archive, mark delivered, production create, RFQ, production reorder) return 403 on a sandbox key.
- Mint a sandbox key. In Studio > Settings > Integrations click Mint sandbox key. Authenticate with
Authorization: Bearer ak_test_...against the same base URL.GET /mereturnsmode: "sandbox". - Create and read sandbox orders.
POST /orderswith the test key creates anSBX-order. Live keys cannot see it. Inventory reads still return your real catalog so you can send realproductIds. - Register a sandbox webhook, then simulate lifecycle.
POST /webhookswith the test key subscribes only to sandbox events.POST /orders/{number}/simulatewith{ "status": "shipped" }advances the order and fires the matching event. Inbound writes and inventory archives stay live-key only.
Errors and status codes
Errors are returned with a conventional HTTP status and a JSON error object. Validation failures include per-field details from the schema validator.
Status codes
200 / 201successoptional | Request succeeded / resource created. |
400bad requestoptional | Malformed parameter or unknown reference - e.g. an invalid since timestamp or an unknown SKU on an order line. |
401unauthorizedoptional | Missing, malformed, or revoked API key. |
403forbiddenoptional | The key lacks the required scope. |
404not foundoptional | Resource does not exist or belongs to another account. |
409conflictoptional | State conflict - e.g. cancelling a shipped order, or exceeding the webhook subscription limit. |
422validationoptional | Request body failed schema validation. error.details.fieldErrors maps each field to its errors. |
429rate limitedoptional | Too many requests. Back off for error.details.retryAfterSeconds. |
500 / 502server erroroptional | Unexpected failure. Safe to retry with idempotency keys set. |
// 422 - schema validation
{
"error": {
"message": "Validation failed",
"details": {
"formErrors": [],
"fieldErrors": {
"shipTo": ["Too big: expected string to have <=2 characters"]
}
}
}
}ak_live_a1b2) - never send the full secret.