# Swaglists API — complete documentation > Swaglists is a print-on-demand manufacturer: you sell custom printed products in your own store, and we print, pack and ship each order under your brand. The REST API (base URL https://swaglists.com/api/v1) lets your backend browse the catalog with your wholesale prices, upload artwork, create products with your own SKUs, quote and create orders paid from a prepaid credit wallet, and receive signed webhooks as orders move through production and shipping. This file contains every page of the Swaglists developer documentation and the complete API reference generated from the OpenAPI document (https://swaglists.com/api/v1/openapi.json), so it can be used on its own to build a full integration. Index: https://swaglists.com/llms.txt. Key facts: - Base URL: `https://swaglists.com/api/v1` (JSON over HTTPS; request and response bodies are JSON objects with snake_case fields; send `Content-Type: application/json` — the only exception is `POST /files`, which also accepts a `multipart/form-data` upload; unknown fields are rejected). - Authentication: `Authorization: Bearer `. Test keys start with `sk_test_`, live keys with `sk_live_`. Keys are secret — server-side only, never in browsers or mobile apps. - Test vs live: the key decides the mode. Test mode has its own data, a separate wallet with free test credits (`POST /test/wallet/fund`) and simulated production (`POST /test/orders/{id}/advance`); nothing is printed, shipped or charged. - Money: integers in cents with a lowercase ISO currency (`usd`), e.g. `1299` = $12.99. Timestamps: ISO-8601 UTC strings. - Idempotency: send an `Idempotency-Key` header (e.g. a UUID) on every POST/PATCH/DELETE; retries with the same key and body within 24 h return the original response (`Idempotent-Replayed: true`). Set `external_id` (your order id) on orders — it is unique per mode. - Lists: cursor pagination with `limit` (1–100, default 20), `starting_after`, `ending_before` → `{ "object": "list", "data": [...], "has_more": bool, "url" }`. - Errors: `{ "error": { "type", "code", "message", "param", "doc_url", "request_id" } }`. Retry only 409 `idempotency_in_progress`, 429 (after `Retry-After`) and 5xx — with the same Idempotency-Key. - Webhooks: Standard Webhooks spec — headers `webhook-id`, `webhook-timestamp`, `webhook-signature` (`v1,`, key = base64-decoded secret after `whsec_`). Verify the raw body; deduplicate on `webhook-id` (= event id). - API version: `2026-10-01` (returned in the `Api-Version` header and in every event). Instructions for AI agents: - Use test keys (`sk_test_…`) in generated code and examples; read keys from environment variables; never print or log live keys. - Never invent ids, SKUs or placement keys: read catalog variants and `placements[].key` from `GET /catalog/products/{id}`, SKUs from your products. - Before creating an order, price it with `POST /orders/estimate` (same body as `POST /orders`); the estimate equals the charge. - Map storefront shipping choices to `economy | standard | express | priority` (default `standard`); check availability with `POST /shipping/rates`. - Always send `Idempotency-Key` and `external_id` when forwarding store orders, so retries and duplicate webhooks never create duplicate orders. A 409 `duplicate_external_id` includes `existing_id`. - Verify webhook signatures on the raw request body before parsing JSON, respond 2xx quickly, and process events idempotently by `id`. - Look orders up by your id with `GET /orders/ext:`. Contents: 1. Introduction — What you can build with the Swaglists API and how an order flows from your store to your customer's door. 2. Quickstart — Go from zero to a shipped test order in about five minutes — catalog, artwork, product, order, simulated production and a signed webhook. 3. Authentication & API keys — Secret API keys, test vs live mode, scopes, rotation, expiry and IP allowlists. 4. Test mode — Build and test your whole integration for free — test keys, test credits, simulated production, lifecycle controls and magic values. 5. Going live checklist — Everything to check before you switch from test keys to live keys and send real orders. 6. Catalog & placements — Catalog products, variants and options, placements (print areas), recommended artwork sizes, DPI and file requirements. 7. Designs & transforms — How artwork is placed on a print area — layers, the position (transform) model with a worked example, defaults and resolution checks. 8. Products & SKUs — Create products from catalog products and your designs, give variants your own SKUs, map them to your store and export them to Shopify or WooCommerce. 9. Orders — Create, confirm, change and cancel orders; the order lifecycle, statuses, hold reasons, item statuses and how credits move. 10. Shipping & tracking — Shipping methods and rates, delivery estimates, shipments and tracking, the branded tracking page and customer shipment emails. 11. Credits & billing — The prepaid credit wallet — available vs held credits, bonus tiers, top-ups, auto-recharge, what happens without enough credits, and refunds. 12. Pricing — Your wholesale prices, what an order costs (items, extra placements, shipping, tax), and the guarantee that an estimate equals the charge. 13. Personalized orders — Send different artwork with every order — names, photos or generated designs — by URL or uploaded file, without creating a product first. 14. Webhooks — Get notified when orders are produced, shipped and delivered — endpoints, events, the payload, signature verification (libraries and manual HMAC), retries, replays and best practices. 15. Idempotency — Retry requests safely with Idempotency-Key, and make order forwarding duplicate-proof with external_id. 16. Pagination — Cursor pagination for list endpoints — limit, starting_after, ending_before and has_more. 17. Errors — The error object, error types and HTTP statuses, which errors to retry, and how to debug with request ids. 18. Rate limits — Per-key request limits, the RateLimit headers on every response, and how to back off from 429s. 19. Versioning — How the API is versioned, what counts as a backwards-compatible change, and how to build an integration that keeps working. 20. Shopify — Forward paid Shopify orders automatically — orders/paid webhook, HMAC verification, SKU mapping, POST /orders — and push tracking back to Shopify. 21. WooCommerce — Forward WooCommerce orders when they are paid — a drop-in PHP plugin on woocommerce_order_status_processing, or a REST webhook to your own service — and push tracking back. 22. BigCommerce — Forward BigCommerce orders when they reach Awaiting Fulfillment — status webhook, fetch the order, one production order per shipping address — and create shipments with tracking. 23. Custom checkout — Integrate any store or app — quote shipping at checkout, create the order after payment, and keep your order status and tracking in sync with webhooks. 24. No-code (Zapier, Make) — Connect any app without writing a server — send orders with an HTTP request step in Zapier or Make, and react to order updates safely. 25. Product catalog export & import — Export your products as Shopify or WooCommerce CSV files, import them into your store, keep SKUs in sync, and bulk-create products from a spreadsheet with the API. 26. API reference — Every endpoint of the REST API with parameters, request and response fields, error codes and examples — generated from the OpenAPI document. 27. Error codes — Every error code the API returns — what it means, how to fix it and whether to retry. 28. Events reference — Every event type, when it fires, what data.object contains, and a complete example payload for each. 29. Objects reference — Every object the API returns — fields, types and descriptions, generated from the API schema. 30. Changelog — Changes to the API and the developer documentation, newest first. 31. FAQ — Short answers to the questions developers ask most. --- # Introduction > What you can build with the Swaglists API and how an order flows from your store to your customer's door. Source: https://swaglists.com/docs/introduction (markdown: https://swaglists.com/docs/introduction.md) Swaglists manufactures print-on-demand products for your brand. You design products once, sell them in your own store, and every time a customer buys, your backend sends us the order. We print it, pack it with your branding and ship it to your customer — your customers never see our name. The API is a JSON REST API at `https://swaglists.com/api/v1`. Everything you can do in the dashboard for products and orders, you can do with it. ## What you can build - **Automatic order forwarding** from Shopify, WooCommerce, BigCommerce or a custom checkout: a paid order in your store becomes a production order here within seconds. See [Integrations](https://swaglists.com/docs/shopify.md). - **Personalized products** — names, photos or generated artwork per order, uploaded on the fly with the order. See [Personalized orders](https://swaglists.com/docs/personalized-orders.md). - **Your own product builder** on top of our catalog: list products with your wholesale prices, render previews from mockup templates, create products with your own SKUs. - **Customer-facing order tracking** in your store or app using our webhooks, shipment objects and branded tracking page. - **Back-office tooling**: reconcile your prepaid credit wallet, export products to your store, reprocess failed orders. ## How it works ```text Your customer Your store / backend Swaglists ───────────── ──────────────────── ───────────────── pays for an order ──► receives "order paid" POST /orders (SKUs, address) ──► price it (estimate = charge) reserve credits from your wallet produce print files at the exact size accept for production (credits spent) webhook order.in_production ◄── print, pack with your branding webhook order.shipped (tracking) ◄── ship gets tracking email ◄─ (or your own email) & branded tracking page webhook order.delivered ``` 1. **Set up once.** Pick products from the [catalog](https://swaglists.com/docs/catalog.md), upload artwork and create [products](https://swaglists.com/docs/products.md) — each variant gets a SKU (yours, or one we generate). Export them to your store as a CSV, or create them in your store yourself with the same SKUs. 2. **Fund your wallet.** Orders are paid from prepaid [credits](https://swaglists.com/docs/billing.md). Bigger top-ups earn bonus credits. 3. **Forward orders.** When a customer pays, call `POST /orders` with the shipping address and the SKUs. Prices are deterministic: [`POST /orders/estimate`](https://swaglists.com/docs/pricing.md) returns exactly what you will be charged. 4. **Track.** Subscribe to [webhooks](https://swaglists.com/docs/webhooks.md) to update your store when the order goes into production, ships and is delivered. ## Key concepts | Concept | What it is | | --- | --- | | [Catalog product](https://swaglists.com/docs/catalog.md) | A blank product we make (a tee, a poster, a mug) with variants (colors, sizes) and print areas called placements. | | [Design](https://swaglists.com/docs/designs.md) | Your artwork placed on a placement, with a position and size. | | [Product](https://swaglists.com/docs/products.md) | A catalog product + your designs + your SKUs. What you sell. | | [Order](https://swaglists.com/docs/orders.md) | Items (SKUs) shipped to one recipient. Moves through `processing` → `in_production` → `shipped` → `delivered`. | | [Wallet](https://swaglists.com/docs/billing.md) | Your prepaid credits. Orders reserve credits when confirmed and spend them once accepted for production. | | [Test mode](https://swaglists.com/docs/test-mode.md) | A full sandbox with test keys, free test credits and simulated production. | ## API at a glance | Endpoint | What it does | | --- | --- | | `GET /account` | Retrieve your account | | `GET /catalog/categories` | List catalog categories | | `GET /catalog/products` | List catalog products | | `GET /catalog/products/{id_or_slug}` | Retrieve a catalog product | | `GET /files` | List files | | `POST /files` | Upload a file | | `GET /files/{id}` | Retrieve a file | | `DELETE /files/{id}` | Delete a file | | `GET /products` | List products | | `POST /products` | Create a product | | `GET /products/{id}` | Retrieve a product | | `PATCH /products/{id}` | Update a product | | `DELETE /products/{id}` | Archive a product | | `GET /products/by_sku/{sku}` | Find a product variant by SKU | | `GET /products/{id}/variants` | List product variants | | `POST /products/{id}/variants` | Add product variants | | `PATCH /products/{id}/variants/{variant_id}` | Update a product variant | | `GET /product_variants/{id}` | Retrieve a product variant | | `POST /shipping/rates` | Get shipping rates | | `POST /orders/estimate` | Estimate an order | | `GET /orders` | List orders | | `POST /orders` | Create an order | | `GET /orders/{id}` | Retrieve an order | | `PATCH /orders/{id}` | Update a draft order | | `POST /orders/{id}/confirm` | Confirm a draft, or release an order waiting for credits | | `POST /orders/{id}/cancel` | Cancel an order | | `GET /orders/{id}/events` | List order events | | `GET /wallet` | Retrieve the wallet | | `GET /wallet/transactions` | List wallet transactions | | `GET /topups` | List top-ups | | `GET /topups/{id}` | Retrieve a top-up | | `GET /webhook_endpoints` | List webhook endpoints | | `POST /webhook_endpoints` | Create a webhook endpoint | | `GET /webhook_endpoints/{id}` | Retrieve a webhook endpoint | | `PATCH /webhook_endpoints/{id}` | Update a webhook endpoint | | `DELETE /webhook_endpoints/{id}` | Delete a webhook endpoint | | `POST /webhook_endpoints/{id}/rotate_secret` | Rotate the signing secret | | `POST /webhook_endpoints/{id}/test` | Send a test event | | `GET /events` | List events | | `GET /events/{id}` | Retrieve an event | | `POST /test/wallet/fund` | Add test credits | | `POST /test/orders/{id}/advance` | Advance a test order | ## Next steps - Make your first test order in the [Quickstart](https://swaglists.com/docs/quickstart.md) — about five minutes. - Read [Authentication & API keys](https://swaglists.com/docs/authentication.md) and [Test mode](https://swaglists.com/docs/test-mode.md). - Building with an AI assistant? Give it [https://swaglists.com/llms-full.txt](https://swaglists.com/llms-full.txt) — the whole documentation in one file. --- # Quickstart > Go from zero to a shipped test order in about five minutes — catalog, artwork, product, order, simulated production and a signed webhook. Source: https://swaglists.com/docs/quickstart (markdown: https://swaglists.com/docs/quickstart.md) You will create a real product and a test order end to end, using **test mode**: nothing is printed, shipped or charged, and test credits are free. Every step shows the request in cURL, Node.js and Python and what to look for in the response. **You need:** a Swaglists account, a terminal, and (for step 8) any URL that can receive HTTPS requests. ## 1. Get a test key 1. In the dashboard, open **Developers → API keys** and click **Create key**. 2. Choose **Test** mode and **Full access**, name it `quickstart`, and copy the secret. It starts with `sk_test_` and is shown only once. 3. Store it in an environment variable and call `GET /account` to check it works: ```bash title="cURL" export API_KEY="sk_test_your_key_here" curl https://swaglists.com/api/v1/account \ -H "Authorization: Bearer $API_KEY" ``` ```js title="Node.js" // quickstart.mjs — Node.js 20+. Run with: API_KEY=sk_test_... node quickstart.mjs import { randomUUID } from "node:crypto"; const API = "https://swaglists.com/api/v1"; async function api(method, path, body) { const res = await fetch(API + path, { method, headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json", ...(method === "GET" ? {} : { "Idempotency-Key": randomUUID() }), }, body: body === undefined ? undefined : JSON.stringify(body), }); const json = await res.json(); if (!res.ok) throw new Error(`${res.status} ${json.error.code}: ${json.error.message}`); return json; } const account = await api("GET", "/account"); console.log(account.name, account.mode); // "Company A" "test" ``` ```python title="Python" # quickstart.py — Python 3.9+, pip install requests. Run with: API_KEY=sk_test_... python quickstart.py import os, uuid, requests API = "https://swaglists.com/api/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['API_KEY']}" def api(method, path, json=None): headers = {} if method == "GET" else {"Idempotency-Key": str(uuid.uuid4())} res = session.request(method, API + path, json=json, headers=headers) body = res.json() if not res.ok: raise RuntimeError(f"{res.status_code} {body['error']['code']}: {body['error']['message']}") return body account = api("GET", "/account") print(account["name"], account["mode"]) # Company A test ``` You should see `"object": "account"` and `"mode": "test"`. A `401` means the key was not sent or mistyped — see [Authentication](https://swaglists.com/docs/authentication.md). > **Note** The Node.js and Python tabs build one script step by step: keep appending each step's code to the same file. ## 2. Add test credits Orders are paid from a prepaid wallet. Test mode has its own wallet; fill it with free test credits ($1,000 per call): ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/test/wallet/fund \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```js title="Node.js" const wallet = await api("POST", "/test/wallet/fund", {}); console.log("Available test credits:", wallet.available / 100); ``` ```python title="Python" wallet = api("POST", "/test/wallet/fund", {}) print("Available test credits:", wallet["available"] / 100) ``` `available` is in cents: `100000` = $1,000. ## 3. Pick a catalog product List the products you can sell. Every variant carries **your wholesale price** in cents: ```bash title="cURL" curl "https://swaglists.com/api/v1/catalog/products?limit=3" \ -H "Authorization: Bearer $API_KEY" ``` ```js title="Node.js" const catalog = await api("GET", "/catalog/products?limit=3"); const catalogProduct = catalog.data[0]; const placement = catalogProduct.placements[0]; console.log(catalogProduct.id, catalogProduct.title, "from", catalogProduct.price_from / 100); console.log("Print on:", placement.key, `${placement.recommended_width_px}x${placement.recommended_height_px}px`); ``` ```python title="Python" catalog = api("GET", "/catalog/products?limit=3") catalog_product = catalog["data"][0] placement = catalog_product["placements"][0] print(catalog_product["id"], catalog_product["title"], "from", catalog_product["price_from"] / 100) print("Print on:", placement["key"], f"{placement['recommended_width_px']}x{placement['recommended_height_px']}px") ``` ```jsonc title="Response (abridged)" { "object": "list", "data": [ { "object": "catalog_product", "id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", // ← keep this "title": "Classic Unisex Tee", "price_from": 1299, "placements": [ { "key": "front", "recommended_width_px": 4500, "recommended_height_px": 5400, "min_dpi": 150 } ], "variants": [ { "id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "price": 1299, "available": true } ] } ], "has_more": true } ``` Keep the product `id` and a placement `key` (usually `front`). Use `catalog/products/{id}` to see every variant, placement and mockup template — see [Catalog & placements](https://swaglists.com/docs/catalog.md). ## 4. Upload artwork Upload a PNG (transparent background recommended). No artwork handy? Download our sample — a 3000 × 3000 px PNG: ```bash title="Download the sample artwork" curl -o artwork.png https://swaglists.com/docs/assets/sample-artwork.png ``` ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/files \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -F "file=@artwork.png" \ -F "purpose=artwork" ``` ```js title="Node.js" import { readFile } from "node:fs/promises"; const form = new FormData(); form.append("file", new Blob([await readFile("artwork.png")], { type: "image/png" }), "artwork.png"); form.append("purpose", "artwork"); // multipart: let fetch set the Content-Type (with the boundary) itself const upload = await fetch(`${API}/files`, { method: "POST", headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Idempotency-Key": randomUUID() }, body: form, }); const file = await upload.json(); console.log(file.id, file.width_px, file.height_px, file.status); // file_… 3000 3000 ready ``` ```python title="Python" with open("artwork.png", "rb") as f: upload = session.post( API + "/files", headers={"Idempotency-Key": str(uuid.uuid4())}, files={"file": ("artwork.png", f, "image/png")}, data={"purpose": "artwork"}, ) file = upload.json() print(file["id"], file["width_px"], file["height_px"], file["status"]) # file_… 3000 3000 ready ``` Already host your artwork? Send JSON instead: `{"url": "https://cdn.example.com/art.png"}` — we download it once. Keep the file `id`. ## 5. Create a product A product = catalog product + your designs + variants with SKUs. Omitting `position` centers the artwork and fits it inside the print area; `"variants": "all"` adds every available color and size: ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/products \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "title": "Quickstart Tee", "designs": [{ "placement": "front", "layers": [{ "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9" }] }], "variants": "all", "retail_price": 2900 }' ``` ```js title="Node.js" const product = await api("POST", "/products", { catalog_product_id: catalogProduct.id, title: "Quickstart Tee", designs: [{ placement: placement.key, layers: [{ file_id: file.id }] }], variants: "all", retail_price: 2900, }); const variant = product.variants.find((v) => v.available) ?? product.variants[0]; const sku = variant.sku; console.log(product.id, "SKU:", sku, "wholesale:", variant.price / 100); ``` ```python title="Python" product = api("POST", "/products", { "catalog_product_id": catalog_product["id"], "title": "Quickstart Tee", "designs": [{"placement": placement["key"], "layers": [{"file_id": file["id"]}]}], "variants": "all", "retail_price": 2900, }) variant = next((v for v in product["variants"] if v["available"]), product["variants"][0]) sku = variant["sku"] print(product["id"], "SKU:", sku, "wholesale:", variant["price"] / 100) ``` In cURL, replace the two ids with yours. The response lists `variants[]`, each with a generated `sku` (you can set your own — see [Products & SKUs](https://swaglists.com/docs/products.md)); use one whose `available` is `true`. It also has a `warnings` array (for example `dpi_low` if the artwork is small for the print area). Mockups render in the background. ## 6. Create a test order Send the end customer's address and the SKU. Always send an `Idempotency-Key` and your own order id as `external_id`, so retries never create duplicates: ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/orders \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "external_id": "quickstart-1001", "shipping_method": "standard", "recipient": { "name": "Jane Doe", "address1": "123 Market St", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "email": "jane@example.com" }, "items": [{ "sku": "YOUR-SKU", "quantity": 1 }] }' ``` ```js title="Node.js" const order = await api("POST", "/orders", { external_id: `quickstart-${Date.now()}`, shipping_method: "standard", recipient: { name: "Jane Doe", address1: "123 Market St", city: "San Francisco", state_code: "CA", postal_code: "94107", country_code: "US", email: "jane@example.com", }, items: [{ sku, quantity: 1 }], }); console.log(order.id, order.status, "total:", order.total / 100); // ord_… processing … ``` ```python title="Python" import time order = api("POST", "/orders", { "external_id": f"quickstart-{int(time.time())}", "shipping_method": "standard", "recipient": { "name": "Jane Doe", "address1": "123 Market St", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "email": "jane@example.com", }, "items": [{"sku": sku, "quantity": 1}], }) print(order["id"], order["status"], "total:", order["total"] / 100) # ord_… processing … ``` The order comes back with `"status": "processing"` and `"funds_status": "held"`: the total was reserved from your test wallet and the order is on its way to (simulated) production. The response also has a `tracking_page_url` — your branded tracking page for the customer. > **Tip** Want the price first? `POST /orders/estimate` takes the same body and returns exactly what the order will cost, without creating it. ## 7. Watch it ship Test orders progress on their own (in production after about 2 minutes, shipped after about 5, delivered after about 15). Skip the wait: each call to the advance endpoint moves the order one step — `in_production` → `shipped` (with tracking) → `delivered`: ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/test/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa/advance \ -H "Authorization: Bearer $API_KEY" ``` ```js title="Node.js" for (let i = 0; i < 3; i++) { const o = await api("POST", `/test/orders/${order.id}/advance`); console.log(o.status, o.shipments[0]?.tracking_number ?? ""); } // in_production // shipped 9400… // delivered 9400… ``` ```python title="Python" for _ in range(3): o = api("POST", f"/test/orders/{order['id']}/advance") tracking = o["shipments"][0]["tracking_number"] if o["shipments"] else "" print(o["status"], tracking) # in_production # shipped 9400… # delivered 9400… ``` After the second call, `shipments[0]` has a carrier, `tracking_number` and `tracking_url`. Fetch the order any time with `GET /orders/{id}` — or by your own id: `GET /orders/ext:quickstart-1001`. ## 8. Receive a webhook Webhooks tell your backend when orders change. Run a small receiver that verifies the signature (it uses the official [Standard Webhooks](https://swaglists.com/docs/webhooks.md) library): ```js title="Node.js" // webhook-server.mjs — npm install standardwebhooks import http from "node:http"; import { Webhook } from "standardwebhooks"; const wh = new Webhook(process.env.WEBHOOK_SECRET); // "whsec_…" http .createServer(async (req, res) => { const chunks = []; for await (const chunk of req) chunks.push(chunk); // the RAW body, decoded once all chunks are in (a character can span two chunks) — verify before parsing const body = Buffer.concat(chunks).toString("utf8"); try { const event = wh.verify(body, req.headers); console.log(event.type, event.data.object.id, event.data.object.status ?? ""); res.writeHead(204).end(); } catch { res.writeHead(400).end("invalid signature"); } }) .listen(3000, () => console.log("Listening on :3000")); ``` ```python title="Python" # webhook_server.py — pip install flask standardwebhooks import os from flask import Flask, request from standardwebhooks.webhooks import Webhook app = Flask(__name__) wh = Webhook(os.environ["WEBHOOK_SECRET"]) # "whsec_…" @app.post("/") def receive(): try: event = wh.verify(request.get_data(), dict(request.headers)) # raw body except Exception: return "invalid signature", 400 print(event["type"], event["data"]["object"]["id"]) return "", 204 app.run(port=3000) ``` Expose it on a public HTTPS URL (for example with a tunnel: `ngrok http 3000` or `cloudflared tunnel --url http://localhost:3000`), then register the URL. The response contains the signing `secret` — **only this once**: ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/webhook_endpoints \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "url": "https://your-tunnel.example.com/", "enabled_events": ["*"] }' ``` Restart the receiver with `WEBHOOK_SECRET` set to that secret, then send a test event (use the endpoint `id` from the response): ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/webhook_endpoints/we_01k8x2m4n6b8v0c2x4z6a8s0d2/test \ -H "Authorization: Bearer $API_KEY" ``` Your receiver prints `webhook_endpoint.test`. Create another order and advance it: you will see `order.created`, `order.processing`, `order.in_production`, `shipment.created`, `order.shipped` and `order.delivered`. ## You did it You created a product, placed an order paid with credits, watched it ship and received signed webhooks. Next: - Connect your store: [Shopify](https://swaglists.com/docs/shopify.md), [WooCommerce](https://swaglists.com/docs/woocommerce.md), [BigCommerce](https://swaglists.com/docs/bigcommerce.md) or a [custom checkout](https://swaglists.com/docs/custom-checkout.md). - Learn the [order lifecycle](https://swaglists.com/docs/orders.md) and how to handle [webhooks](https://swaglists.com/docs/webhooks.md) robustly. - When you are ready, follow the [going live checklist](https://swaglists.com/docs/going-live.md). --- # Authentication & API keys > Secret API keys, test vs live mode, scopes, rotation, expiry and IP allowlists. Source: https://swaglists.com/docs/authentication (markdown: https://swaglists.com/docs/authentication.md) Every request is authenticated with a **secret API key** sent as a Bearer token: ```http GET /api/v1/account HTTP/1.1 Authorization: Bearer sk_test_51Hx… ``` ```bash title="cURL" curl https://swaglists.com/api/v1/account \ -H "Authorization: Bearer $API_KEY" ``` ```js title="Node.js" const res = await fetch("https://swaglists.com/api/v1/account", { headers: { Authorization: `Bearer ${process.env.API_KEY}` }, }); ``` ```python title="Python" res = requests.get("https://swaglists.com/api/v1/account", headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}) ``` HTTP Basic authentication with the key as the username and an empty password also works (`curl -u "$API_KEY:"`), for tools that only support Basic auth. > **Danger** Secret keys can create orders that spend your credits. Use them only on your server. Never put them in browser JavaScript, mobile apps, public repositories or URLs. Store them in environment variables or a secrets manager. ## Test and live keys | | Test mode | Live mode | | --- | --- | --- | | Key prefix | `sk_test_` | `sk_live_` | | Data | Separate test orders, webhooks, events and wallet | Real orders | | Credits | Free test credits (`POST /test/wallet/fund`) | Credits you buy | | Production | Simulated — nothing is printed or shipped | Real production and shipping | | Customer emails | Never sent | Sent if enabled | | Test helpers (`/test/…`) | Available | `403 test_mode_only` | Both modes use the same base URL — **the key decides the mode**. Products, designs and files are shared between modes, so a product you created in test mode can be ordered live without changes. Everything else (orders, webhook endpoints, events, wallet) is separate: a live key cannot see test orders and vice versa. Creating a live key requires a verified email address (see the [going live checklist](https://swaglists.com/docs/going-live.md)). Live keys work while live mode is enabled for your account; if it is not (for example while support reviews a new account), they return `403 live_mode_disabled`. ## Creating keys Create keys in the dashboard under **Developers → API keys** (owners, admins and developers can). For each key you choose: - **Mode** — test or live. - **Access** — full access, read-only, orders only, or individual [scopes](#scopes). - **Expiry** (optional) — the key stops working at that date (at most 5 years ahead). - **IP allowlist** (optional) — the key only works from these addresses. The full secret is shown **once**, when the key is created. We store only a hash; if you lose a key, roll it or create a new one. The dashboard shows a key's prefix and last four characters so you can recognize it, and the **Logs** page shows every request made with it. ## Scopes Give each integration only what it needs. A `:write` scope includes the matching `:read` scope; `*` is full access. | Scope | Grants | | --- | --- | | `*` | Full access to every endpoint (required by `POST /test/wallet/fund`). | | `account:read` | `GET /account`. | | `catalog:read` | Catalog products, categories, variants and wholesale prices. | | `files:read` | List and retrieve files. | | `files:write` | Upload and delete files (includes `files:read`). | | `products:read` | Your products, variants and SKUs. | | `products:write` | Create, update and archive products and variants (includes `products:read`). | | `orders:read` | Orders, order events, shipping rates and order estimates. | | `orders:write` | Create, update, confirm and cancel orders; advance test orders (includes `orders:read`). | | `wallet:read` | Wallet balance, transactions and top-ups. | | `webhooks:read` | Webhook endpoints and events. | | `webhooks:write` | Create, update, delete and test webhook endpoints, rotate secrets (includes `webhooks:read`). | A request outside the key's scopes fails with `403 insufficient_scope`; the error's `required_scope` tells you which scope is missing. ## Rotating keys Roll a key (**Developers → API keys → Roll**) to replace its secret, for example when a team member leaves or a secret may have leaked. You choose when the old secret stops working: **now**, in **1 hour**, **24 hours** or **7 days**. During the overlap both secrets work, so you can deploy the new secret without downtime: 1. Roll the key with an overlap (e.g. 24 hours) and copy the new secret. 2. Deploy the new secret to your servers. 3. Check **Developers → Logs** to confirm no requests use the old key anymore. **Revoke** a key to stop it immediately. Rolled, revoked and expired keys return `401 invalid_api_key` with a message saying why. ## IP allowlists An allowlist accepts up to 50 IPv4/IPv6 addresses or CIDR ranges (for example `203.0.113.7` or `10.0.0.0/8`). Requests from other addresses fail with `403 ip_not_allowed`. > **Warning** Use your server's **public egress** address. Behind NAT, a load balancer or a serverless platform, outgoing requests may come from a different (or changing) address than your server's own. If your platform has no static egress IP, do not use an allowlist. ## Errors | Status | Code | Meaning | | --- | --- | --- | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key) | No `Authorization` header. | | 401 | [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | Unknown, revoked or expired key, or a malformed header. | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope) | The key lacks a scope. | | 403 | [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled) | Live key used before live mode is enabled. | | 403 | [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed) | Request from an address not on the allowlist. | | 403 | [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | The account is suspended. | | 403 | [`test_mode_only`](https://swaglists.com/docs/errors.md#test_mode_only) | A test helper was called with a live key. | --- # Test mode > Build and test your whole integration for free — test keys, test credits, simulated production, lifecycle controls and magic values. Source: https://swaglists.com/docs/test-mode (markdown: https://swaglists.com/docs/test-mode.md) Test mode is a complete copy of the platform for development. Use a **test key** (`sk_test_…`) and every request runs the real pipeline — validation, pricing, credit reservations, print-file generation, events and webhooks — but production is **simulated**: nothing is printed, shipped or charged, and your customers never get emails. ## What is separate in test mode | Shared between test and live | Separate per mode | | --- | --- | | Products, variants and SKUs | Orders, shipments and order events | | Files (artwork) | Wallet, transactions and credits | | Catalog and your wholesale prices | Webhook endpoints, events and deliveries | | Account settings and branding | Idempotency keys and API request logs | Because products are shared, you can build and test everything in test mode and go live by swapping the key — no need to recreate products. ## Test credits The test wallet starts empty. Add free test credits — $1,000 per call by default: ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/test/wallet/fund \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 250000 }' ``` ```js title="Node.js" await fetch("https://swaglists.com/api/v1/test/wallet/fund", { method: "POST", headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ amount: 250000 }), // $2,500.00; omit for the default grant }); ``` ```python title="Python" requests.post( "https://swaglists.com/api/v1/test/wallet/fund", headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}, json={"amount": 250000}, # $2,500.00; omit for the default grant ) ``` This endpoint needs a **full-access** test key. In the dashboard, switch to test mode and use **Wallet → Add test credits**. Orders waiting for credits (`on_hold` / `insufficient_funds`) resume automatically after credits are added — which makes this the easiest way to test your on-hold handling: create an order with an empty test wallet, then fund it. ## Simulated production A confirmed test order goes through the same states as a live one. On its own it advances roughly like this: | After | Order status | What happens | | --- | --- | --- | | seconds | `processing` | Credits reserved, order accepted by the simulator (credits captured). | | ~2 minutes | `in_production` | `order.in_production` | | ~5 minutes | `shipped` | A shipment with a test tracking number: `shipment.created`, `order.shipped` | | ~12 minutes | `shipped` | Shipment `out_for_delivery`: `shipment.updated` | | ~15 minutes | `delivered` | `shipment.delivered`, `order.delivered` | Tracking numbers are generated test numbers — the carrier's tracking page will not find them. The simulator advances on its own clock, and we check its progress periodically — just like the production of a live order. When it moves several steps between two checks, you still get **every** step, in order and exactly once — the same [lifecycle guarantee](https://swaglists.com/docs/webhooks.md#lifecycle-steps) as live orders, where `data.object` already shows the latest state (a late `order.shipped` can carry `"status": "delivered"`). ### Advance an order on demand Don't want to wait? `POST /test/orders/{id}/advance` moves an order one step forward and sends the same webhooks as a real order. Call it repeatedly: `processing` → `in_production` → `shipped` → `delivered`. The `{id}` can also be `ext:`. ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/test/orders/ext:quickstart-1001/advance \ -H "Authorization: Bearer $API_KEY" ``` ```js title="Node.js" const res = await fetch("https://swaglists.com/api/v1/test/orders/ext:quickstart-1001/advance", { method: "POST", headers: { Authorization: `Bearer ${process.env.API_KEY}` }, }); const order = await res.json(); console.log(order.status); // in_production, then shipped, then delivered ``` ```python title="Python" order = requests.post( "https://swaglists.com/api/v1/test/orders/ext:quickstart-1001/advance", headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}, ).json() print(order["status"]) # in_production, then shipped, then delivered ``` It needs the `orders:write` scope and returns `409 order_not_advanceable` for drafts, finished orders (`delivered`, `canceled`, `failed`) and orders on hold for a reason that needs action (for example waiting for credits — fund the wallet instead). Advancing an order moves it ahead of the simulator's clock. When the simulator later reaches the same steps, nothing is sent again: no second shipment, no repeated event. Only real changes — for example an updated estimated delivery date — still produce a `shipment.updated`. ### Simulate problems Send a JSON body to choose what the simulated factory does next. Every outcome goes through the same pipeline as a real factory update — timeline, webhooks, credit refunds and notifications — and the simulator keeps to it afterwards: | `outcome` | When | What happens | | --- | --- | --- | | `next` (default) | any step above | The next step: `in_production` → `shipped` → `delivered`, or resume an order held by the factory. | | `fail_production` | before it ships | The items can't be produced: they become `failed` and their test credits are returned. As with a real factory failure the order goes `on_hold` (`hold_reason: fulfillment_issue`) for our team to follow up. | | `partial_shipment` | before it ships, 2+ units | About half of the units ship now (`shipment.created`, `order.partially_shipped`); the rest ship on the normal schedule. | | `delivery_exception` | a package in transit | The carrier reports a problem: `shipment.updated` with `"status": "exception"`. It stays that way until you advance again, which delivers it. | | `return_to_sender` | a package in transit | The package goes back to the sender: `shipment.updated` with `"status": "returned"`. The order stays `shipped`; there's nothing more to simulate. | ```bash curl -X POST https://swaglists.com/api/v1/test/orders/ext:quickstart-1001/advance \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "outcome": "partial_shipment", "expected_status": "in_production" }' ``` An outcome that doesn't fit the order's current state returns `409 order_not_advanceable`. Add `expected_status` — the status you last saw — to make the call safe to retry: if the order has moved on in the meantime (the simulator also advances on its own), nothing happens and the call returns `409 order_status_changed` with the current status. ## Magic values Put one of these words anywhere in `recipient.name` of a test order to simulate production problems (case-insensitive, e.g. `"Jane FAIL_SUBMIT Doe"`): | `recipient.name` contains | What happens | | --- | --- | | `FAIL_SUBMIT` | Production rejects the order: it ends `failed` (`order.failed`), reserved test credits are released. | | `RETRY_SUBMIT` | The first two attempts to send the order to production fail temporarily, then it succeeds — the order stays `processing` a little longer (shows our automatic retries). | | `HOLD_ORDER` | Production puts the order on hold: `status: on_hold`, `hold_reason: fulfillment_issue` (`order.on_hold`). | | `NO_CANCEL` | Cancellation is refused as if production had started: `POST /orders/{id}/cancel` returns 409 `order_not_cancelable`. | Use them to test that your integration handles failures, holds and refused cancellations — before a real customer runs into them. ## Other ways to test - **Insufficient credits** — create an order with an empty test wallet. With the default setting the order is held (`on_hold`, `hold_reason: insufficient_funds`); if your account rejects such orders (Settings → Orders), the API answers `402 insufficient_funds`. Then fund the wallet — a rejected order must be sent again with a **new** `Idempotency-Key`, because the original key replays the 402 for 24 hours. - **Drafts** — send `"confirm": false`, change the draft with `PATCH /orders/{id}`, then `POST /orders/{id}/confirm`. - **Cancellations** — cancel an order while it is `processing` (before it reaches `in_production`). - **Webhooks** — test endpoints may use `http://` URLs. `POST /webhook_endpoints/{id}/test` sends a `webhook_endpoint.test` event to one endpoint. Replay any event from **Developers → Events**. - **Idempotency** — send the same request twice with the same `Idempotency-Key` and check you get `Idempotent-Replayed: true`. ## Limits of test mode - Test orders are never produced, so print quality, packaging and delivery times can only be checked with a live order — order a sample to yourself before launch. - Test tracking numbers do not work on carrier sites; the branded tracking page (`tracking_page_url`) works. - Customer shipment emails are only sent for live orders. --- # Going live checklist > Everything to check before you switch from test keys to live keys and send real orders. Source: https://swaglists.com/docs/going-live (markdown: https://swaglists.com/docs/going-live.md) Your test integration becomes a live one by swapping the key — products and SKUs are shared between modes. Before you do, work through this list. ## Account - [ ] **Verify your email address.** You need a verified email to create live keys and use live mode in the dashboard (Settings → Profile shows the status). - [ ] **Live mode is enabled.** `GET /account` with a live key returns `"live_mode_enabled": true`. If live keys return `403 live_mode_disabled`, contact support. - [ ] **Branding is set** (Settings → Branding): brand name, logo, support email, packing slip message and return address. They appear on packing slips, the [tracking page](https://swaglists.com/docs/shipping.md#tracking-page) and customer emails — under your brand, never ours. - [ ] **Order settings** (Settings → Orders): auto-confirm, what happens when credits run out (*hold*, *hold and release manually*, or *reject*), customer shipment emails, low-balance alert. - [ ] **Team access**: invite teammates with the least role they need (developers manage keys and webhooks; finance manages the wallet). ## Credits - [ ] **Buy credits** (Wallet → Buy credits). Bigger top-ups earn [bonus credits](https://swaglists.com/docs/billing.md#bonus-tiers). Top-ups start at $100. - [ ] **Turn on auto-recharge** so orders never wait for credits: save a card or bank account and pick a threshold and amount. - [ ] **Subscribe to `wallet.low_balance`** or keep the low-balance email alert on. ## Keys & security - [ ] **Create a live key** (`sk_live_…`) with only the [scopes](https://swaglists.com/docs/authentication.md#scopes) your integration needs. Store it in your secrets manager — never in code or in the browser. - [ ] **Optional: restrict it to your server's egress IPs** with an [IP allowlist](https://swaglists.com/docs/authentication.md#ip-allowlists). - [ ] **Plan rotation**: know how to [roll the key](https://swaglists.com/docs/authentication.md#rotation) with an overlap. ## Webhooks - [ ] **Create a live webhook endpoint** — test endpoints do not receive live events. Live endpoints must use HTTPS on a public host. - [ ] **Store the live signing secret** (it is different from the test one) and [verify every signature](https://swaglists.com/docs/webhooks.md#verify-signatures). - [ ] **Respond 2xx within 15 seconds** and process asynchronously; handle duplicates by event `id`. - [ ] **Reconcile**: if your endpoint was down, list missed events with `GET /events` (30 days). ## Orders - [ ] **Idempotency**: every `POST /orders` sends an `Idempotency-Key` derived from your store's order id, and `external_id` is set. See [Idempotency](https://swaglists.com/docs/idempotency.md). - [ ] **Errors**: you log `error.code` and `request_id`, retry only 409 `idempotency_in_progress`, 429 and 5xx, and alert on the rest. See [Errors](https://swaglists.com/docs/error-handling.md). - [ ] **Holds**: you handle `order.on_hold` (show it in your back office; most holds resolve themselves). - [ ] **Shipping**: every shipping option of your store maps to `economy`, `standard`, `express` or `priority`. - [ ] **Non-production items**: your integration skips store line items whose SKU is not one of your products (gift cards, digital goods). - [ ] **Tracking**: your store is updated from `order.shipped` / `shipment.created` (or you use our customer emails). ## Final test - [ ] **Order a sample** to yourself with the live key to check print quality and packaging. - [ ] **Watch your first orders** in the dashboard (Orders, Developers → Logs, Developers → Webhooks). Questions? Contact support@swaglists.com — include request ids (`Request-Id` header) for API questions. --- # Catalog & placements > Catalog products, variants and options, placements (print areas), recommended artwork sizes, DPI and file requirements. Source: https://swaglists.com/docs/catalog (markdown: https://swaglists.com/docs/catalog.md) The catalog is everything we can make for you: t-shirts, hoodies, posters, mugs and more. Each **catalog product** has **variants** (combinations of options like color and size) and **placements** — the areas you can print on. Prices in the catalog are **your wholesale prices**. ```text catalog_product "Classic Unisex Tee" (cp_…) ├── options Color: Black, White, … Size: S, M, L, … ├── variants cv_… "Black / M" price 1299 available ✓ │ cv_… "White / L" price 1299 available ✓ ├── placements front 15 × 18 in → 4500 × 5400 px at 300 DPI │ back 15 × 18 in → 4500 × 5400 px └── mockup_templates photo + print-area rectangle per placement/color ``` ## Browse the catalog ```bash title="cURL" # Featured first; filter by search term, category, technique curl "https://swaglists.com/api/v1/catalog/products?q=tee&category=t-shirts&limit=10" \ -H "Authorization: Bearer $API_KEY" # One product with every variant, placement and mockup template (id or slug) curl https://swaglists.com/api/v1/catalog/products/classic-unisex-tee \ -H "Authorization: Bearer $API_KEY" ``` ```js title="Node.js" const headers = { Authorization: `Bearer ${process.env.API_KEY}` }; const list = await (await fetch("https://swaglists.com/api/v1/catalog/products?q=tee&limit=10", { headers })).json(); const product = await (await fetch(`https://swaglists.com/api/v1/catalog/products/${list.data[0].id}`, { headers })).json(); for (const v of product.variants) console.log(v.id, v.title, v.price, v.available); ``` ```python title="Python" headers = {"Authorization": f"Bearer {os.environ['API_KEY']}"} items = requests.get("https://swaglists.com/api/v1/catalog/products", params={"q": "tee", "limit": 10}, headers=headers).json() product = requests.get(f"https://swaglists.com/api/v1/catalog/products/{items['data'][0]['id']}", headers=headers).json() for v in product["variants"]: print(v["id"], v["title"], v["price"], v["available"]) ``` | Filter | Example | Notes | | --- | --- | --- | | `q` | `tee` | Searches title, subtitle, brand and tags. | | `category` | `t-shirts` or `cat_…` | Includes sub-categories. List them with `GET /catalog/categories`. | | `technique` | `dtg`, `embroidery` | Print technique. | | `featured` | `true` | Only featured products. | The list is paginated like every list — see [Pagination](https://swaglists.com/docs/pagination.md). Only products you can sell are listed; a variant that is temporarily unavailable shows `"available": false`. ## Variants and options A catalog product's `options` list the option names and values in display order (e.g. `Color` with hex swatches, `Size`). Each **catalog variant** has: - `id` (`cv_…`) — what you reference when you create products or order with inline designs, - `options` — e.g. `{"Color": "Black", "Size": "M"}`, plus `color`, `color_hex`, `size` shortcuts, - `price` — your wholesale unit price in cents with artwork on the **first placement**, - `available` — whether it can be ordered right now. `price_from` on the product is the lowest variant price. Prices can change over time; you get a [`catalog.price_changed`](https://swaglists.com/docs/events.md#catalog.price_changed) event when a variant your products use changes price. ## Placements A **placement** is a printable area. Its `key` (`front`, `back`, `left_sleeve`, `embroidery_chest_left`, …) is what you use in designs. Each placement tells you exactly what artwork it needs: | Field | Meaning | | --- | --- | | `key`, `label` | Identifier for designs, display name. | | `technique` | How it is printed (`dtg`, `embroidery`, `sublimation`, `digital`, …). | | `width_in`, `height_in`, `aspect_ratio` | Physical size of the print area and width ÷ height. | | `recommended_width_px`, `recommended_height_px` | Artwork size that covers the whole area at **300 DPI**. | | `min_dpi` | Below this effective resolution you get a `dpi_low` warning. | | `required` | The product needs artwork on this placement. | | `file_requirements` | Accepted formats, max size, color profile, transparency. | Extra placements can cost extra: the variant `price` includes the first placement; a product with artwork on several placements has a higher `price` on its variants. [Estimate](https://swaglists.com/docs/pricing.md) an order to see the exact total. ## Artwork requirements - **Formats:** PNG (recommended, with transparency), JPEG, WebP, TIFF, GIF (first frame) and SVG (sanitized and rasterized). - **Size:** up to 100 MB and 30,000 px per side. - **Color:** sRGB. Transparent pixels are not printed. - **Resolution:** aim for 300 DPI at the printed size — the placement's `recommended_width_px` × `recommended_height_px` when the artwork fills the area. ### DPI explained Effective DPI is the artwork's pixels divided by the inches it is printed at: ```text effective DPI = artwork width (px) ÷ (placement width (in) × layer width fraction) ``` For example, a 3000 px wide image printed across the full 15 in front of a tee is 3000 ÷ 15 = **200 DPI**. Printed 10 in wide (width fraction 0.667) it is 3000 ÷ 10 = **300 DPI**. | Effective DPI | Result | | --- | --- | | 300 or more | Ideal. | | 150–299 | Fine for most products. | | Below 150 (or the placement's `min_dpi`) | Accepted with a `dpi_low` warning — may look slightly soft. | | Below 75 | Rejected with `artwork_resolution_too_low`. | ## Mockup templates `mockup_templates` let you render previews in your own UI: each has a photo (`image_url`), the placement it shows, an optional `color` it belongs to, and `print_area` — where the print area sits on the photo, as fractions of the photo size. Draw the artwork into that rectangle using the same [transform](https://swaglists.com/docs/designs.md) as your design. When you create a product, we render its mockups for you (`product.mockups`). Catalog image URLs (`images[].url`, variant `image_url`, mockup template `image_url`) don't expire, so you can display them directly in your store. ## Production time `production_days` (`min`/`max` business days) is how long we need before handing the order to the carrier. Shipping rates add transit time; `estimated_delivery_days` on [shipping rates](https://swaglists.com/docs/shipping.md) includes both. --- # Designs & transforms > How artwork is placed on a print area — layers, the position (transform) model with a worked example, defaults and resolution checks. Source: https://swaglists.com/docs/designs (markdown: https://swaglists.com/docs/designs.md) A **design** puts one or more artwork layers on a [placement](https://swaglists.com/docs/catalog.md#placements). Designs are part of [products](https://swaglists.com/docs/products.md) (and of order items with [inline designs](https://swaglists.com/docs/personalized-orders.md)): ```json { "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "position": { "x": 0.1667, "y": 0.05, "width": 0.6667, "height": 0.5556, "rotation": 0 } }, { "url": "https://cdn.example.com/badge.png", "opacity": 0.9 } ] } ] } ``` - One entry per placement (`placement` = the placement `key`), up to 20 layers each, drawn bottom to top. - Each layer references an uploaded file (`file_id`) **or** a public image `url` that we download for you. - `position` (alias `transform`) places the layer; omit it to center the artwork and fit it inside the area. - `opacity` is 0–1 (default 1). We generate the print-ready file for production ourselves, at the exact size the product needs, from your artwork and these positions — so you only ever think in fractions of the print area. ## The position model Positions are **fractions of the print area**, measured from its top-left corner: ```text (0,0) ─────────────────── x ───────────────────► (1,0) │ print area (the placement) │ │ x = 0.1667 │ ├─────┐ │ ┌──────────────────────┐ ┬ y = 0.05 │ │ │ │ y │ artwork │ │ height = 0.5556 │ │ │ │ │ └──────────────────────┘ ┴ │ ├──── width = 0.6667 ───┤ ▼ (0,1) (1,1) ``` | Field | Meaning | | --- | --- | | `x` | Left edge of the artwork box, as a fraction of the print area **width**. | | `y` | Top edge of the artwork box, as a fraction of the print area **height**. | | `width` | Width of the artwork box, as a fraction of the print area width. | | `height` | Height of the artwork box, as a fraction of the print area height. | | `rotation` | Degrees, clockwise, around the center of the artwork box (default 0). | - `x`/`y` describe the **un-rotated** box; rotation turns it around its center. - Values outside 0–1 are allowed (up to ±10) for intentional bleed: whatever falls outside the area is cropped. A layer entirely outside gets a `layer_outside_area` warning; if no layer is visible the design is rejected (`design_not_visible`). - The artwork is scaled to fill the box exactly. **To avoid stretching, keep the box's aspect ratio equal to the artwork's** (see below). ### Keeping the aspect ratio Because `width` and `height` are fractions of different lengths, the box's real aspect ratio is: ```text box aspect = (width × area_width) ÷ (height × area_height) = (width ÷ height) × placement.aspect_ratio ``` So for artwork of `art_w × art_h` pixels, choose a `width` and compute: ```text height = width × placement.aspect_ratio × art_h ÷ art_w ``` ## Worked example A tee's `front` placement is **15 × 18 in** (`aspect_ratio` 0.8333, recommended 4500 × 5400 px). Your logo is a **3000 × 3000 px** PNG. You want it **10 in wide**, centered horizontally, **0.9 in** below the top of the print area. 1. `width` = 10 in ÷ 15 in = **0.6667** 2. `height` = 0.6667 × 0.8333 × 3000 ÷ 3000 = **0.5556** (= 10 in ÷ 18 in, so the logo stays square) 3. `x` = (1 − 0.6667) ÷ 2 = **0.1667** (centered) 4. `y` = 0.9 in ÷ 18 in = **0.05** 5. Effective DPI = 3000 px ÷ 10 in = **300 DPI** ✓ ```json { "x": 0.1667, "y": 0.05, "width": 0.6667, "height": 0.5556, "rotation": 0 } ``` Without a `position`, the same logo would get the default **contain & center** position — as large as possible inside the area, centered: ```json { "x": 0, "y": 0.083333, "width": 1, "height": 0.833333, "rotation": 0 } ``` That prints it 15 in wide (3000 px ÷ 15 in = 200 DPI — fine, above the 150 DPI warning threshold). ```js title="Node.js" // Position for artwork `artW × artH` px, `widthIn` inches wide, centered, `topIn` inches from the top. function position(placement, artW, artH, widthIn, topIn = 0) { const width = widthIn / placement.width_in; const height = width * placement.aspect_ratio * (artH / artW); return { x: (1 - width) / 2, y: topIn / placement.height_in, width, height, rotation: 0 }; } // position({ width_in: 15, height_in: 18, aspect_ratio: 15 / 18 }, 3000, 3000, 10, 0.9) // → { x: 0.1667, y: 0.05, width: 0.6667, height: 0.5556, rotation: 0 } ``` ```python title="Python" def position(placement, art_w, art_h, width_in, top_in=0.0): width = width_in / placement["width_in"] height = width * placement["aspect_ratio"] * art_h / art_w return {"x": (1 - width) / 2, "y": top_in / placement["height_in"], "width": width, "height": height, "rotation": 0} ``` ## Resolution checks When you create or update a product we compute each layer's effective DPI from the placement size and the layer's size: - below **150 DPI** (or the placement's `min_dpi`, if higher): the product is created with a `dpi_low` [warning](https://swaglists.com/docs/errors.md#warnings) — it may print slightly soft; - below **75 DPI**: the request fails with [`artwork_resolution_too_low`](https://swaglists.com/docs/errors.md#artwork_resolution_too_low). Use a larger image or place it smaller. Warnings are returned in the product response's `warnings` array (`code`, `message`, `param`, `placement`, `effective_dpi`). ## Tips for great prints - Export artwork at the placement's `recommended_width_px` × `recommended_height_px` (300 DPI) and position it with fractions — never upscale small images. - Use PNG with a transparent background; anything transparent is not printed (the garment color shows). - Keep important content away from the very edges of the area. - Embroidery placements need simple shapes and few colors; check the placement `technique`. - Changing designs later (`PATCH /products/{id}` with `designs`) re-renders mockups; existing orders keep the designs they were created with. --- # Products & SKUs > Create products from catalog products and your designs, give variants your own SKUs, map them to your store and export them to Shopify or WooCommerce. Source: https://swaglists.com/docs/products (markdown: https://swaglists.com/docs/products.md) A **product** is what you sell: a [catalog product](https://swaglists.com/docs/catalog.md) + your [designs](https://swaglists.com/docs/designs.md) + a set of **variants**, each with a **SKU**. When your store sells a variant, you send us its SKU in an order — that is the whole mapping. ```text product "Company A Logo Tee" (prod_…) ├── catalog_product_id cp_… (Classic Unisex Tee) ├── designs front: [file_… at {x, y, width, height}] └── variants pvar_… SKU CA-TEE-BLK-M → catalog variant Black / M price 1299 pvar_… SKU CA-TEE-BLK-L → catalog variant Black / L price 1299 ``` Products, variants and SKUs are shared between test and live mode. ## Create a product ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/products \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "designs": [ { "placement": "front", "layers": [{ "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9" }] } ], "variants": [ { "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "retail_price": 2900 }, { "catalog_variant_id": "cv_01k8w1a2s9f1g3h5j7k9m1n3p5", "sku": "CA-TEE-BLK-L", "retail_price": 2900 } ], "external_id": "shopify-product-9921" }' ``` ```js title="Node.js" const res = await fetch("https://swaglists.com/api/v1/products", { method: "POST", headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `product-${myProduct.id}`, }, body: JSON.stringify({ catalog_product_id: "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", title: "Company A Logo Tee", designs: [{ placement: "front", layers: [{ file_id: "file_01k8x3y1q3w5e7r9t1y3u5i7o9" }] }], variants: "all", // every available catalog variant, SKUs generated retail_price: 2900, }), }); const product = await res.json(); for (const v of product.variants) console.log(v.sku, v.title, v.price); ``` ```python title="Python" product = requests.post( "https://swaglists.com/api/v1/products", headers={"Authorization": f"Bearer {os.environ['API_KEY']}", "Idempotency-Key": f"product-{my_product_id}"}, json={ "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "title": "Company A Logo Tee", "designs": [{"placement": "front", "layers": [{"file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9"}]}], "variants": "all", "retail_price": 2900, }, ).json() for v in product["variants"]: print(v["sku"], v["title"], v["price"]) ``` | Field | Notes | | --- | --- | | `catalog_product_id` | From `GET /catalog/products`. | | `designs` | At least one; every `required` placement must be covered. See [Designs](https://swaglists.com/docs/designs.md). | | `variants` | `"all"` (every available catalog variant) or a list of `{ catalog_variant_id, sku?, retail_price?, external_id? }`. Default `"all"`. | | `retail_price` | Your selling price in cents — informational (used in exports), never charged. | | `external_id`, `tags`, `metadata` | Your references. `external_id` is unique among your products. | | `status` | `active` (default) or `draft`. Drafts can still be ordered. | The response is the [product](https://swaglists.com/docs/objects.md#product) with its variants and a `warnings` array (resolution or placement issues that did not block creation). Mockups are rendered in the background; `product.updated` fires when they are ready (`mockups[]`). Each variant's `price` is your **wholesale price** for one unit of this product with all its designs — what an order line will cost before shipping and tax. ## Availability Each variant's `available` says whether it can be ordered **right now**: the variant is enabled, the product isn't archived, and the item is in stock. Variants can become temporarily unavailable (out of stock, discontinued); orders that include them are rejected with `400 item_unavailable` until they are back. Whenever `available` changes we send `product.updated` (with the previous values in `previous_attributes.variants`) and notify you in the dashboard (plus an email when variants become unavailable) — mark those variants sold out in your store, and back in stock when the next `product.updated` says so. ## Mockup images `mockups[].url` links **don't expire**, so you can use them directly as product images in your store or headless storefront. Each URL belongs to one rendering: when you change the designs or the enabled colors, the mockups are re-rendered, the new URLs arrive with `product.updated`, and the previous URLs stop working (`404`). Update your listing from that event (or re-fetch the product) instead of keeping old URLs — or download the images into your store, as the Shopify and WooCommerce imports do. ## SKUs SKUs connect your store to us. Rules: - 1–64 printable ASCII characters (no leading/trailing spaces), **unique in your account** across all products. - Omit `sku` and we generate a readable one from the product title and the variant options (e.g. `QUICKSTART-BLACK-M`; a `-2`, `-3`, … suffix is added on collisions). - Disabled variants and archived products keep their SKUs reserved. Already have SKUs in your store? Use them — set `sku` when creating the product, or change it later: ```bash title="Change a variant's SKU" curl -X PATCH https://swaglists.com/api/v1/products/prod_01k8x3z7r1t3y5u7i9o1p3a5s7/variants/pvar_01k8x3z7r4f6g8h0j2k4l6z8x0 \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sku": "STORE-TEE-BLACK-M", "external_id": "shopify-variant-4410" }' ``` Look a variant up by SKU with `GET /products/by_sku/{sku}` (URL-encode special characters). ### Mapping your store's products | Your store has… | Do this | | --- | --- | | No products yet | Create products here (API or designer), then [export them](#exports) to your store. The CSV carries our SKUs. | | Products with SKUs | Create matching products here with `variants: [{ catalog_variant_id, sku: "" }]`. | | Products without SKUs | Add our generated SKUs to your store's variants (Shopify and WooCommerce have a SKU field per variant). | Order line items whose SKU is not one of your products return `400 resource_missing` with `param` = `items[i].sku`; skip non-print items (gift cards, digital goods) before sending the order. ## Update, add variants, archive | Request | What it does | | --- | --- | | `PATCH /products/{id}` | Change `title`, `description`, `tags`, `metadata`, `external_id`, `retail_price`, `status`, or replace all `designs` (mockups are re-rendered). | | `POST /products/{id}/variants` | Add catalog variants (or re-enable disabled ones): `{ "variants": "all" }` or a list. | | `PATCH /products/{id}/variants/{variant_id}` | Change a variant's `sku`, `retail_price`, `external_id` or `enabled`. | | `DELETE /products/{id}` | Archive: it can no longer be ordered; SKUs stay reserved; existing orders are unaffected. Restore with `PATCH` `{ "status": "active" }`. | Archived products cannot be edited (`409 product_archived` — product fields, designs and variants) until restored: send `PATCH /products/{id}` with a `status` (other changes may be included in that same request). Orders keep a snapshot of the designs and prices at order time, so later product changes never affect orders already placed. ## Exports Export products as a CSV ready to import into your store: in the dashboard open **Products → Export** and choose: | Format | Import it in | Notes | | --- | --- | --- | | Shopify CSV | Shopify admin → Products → Import | One row per variant; `SKU` = our SKU; `Fulfillment service` = manual; mockups as images; prices = your retail prices. | | WooCommerce CSV | WooCommerce → Products → Import | Variable products with one variation per variant; `SKU` = our SKU; weights in your chosen unit. | | CSV | Spreadsheets, other platforms | One row per variant with ids, SKU, options, retail price and image URL. | Select products in the list to export only those. See [Product catalog export & import](https://swaglists.com/docs/catalog-export.md) for the column mapping and how to keep your store in sync. --- # Orders > Create, confirm, change and cancel orders; the order lifecycle, statuses, hold reasons, item statuses and how credits move. Source: https://swaglists.com/docs/orders (markdown: https://swaglists.com/docs/orders.md) An **order** ships items to one recipient. You create it with the SKUs your customer bought and their address; we price it, reserve the total from your wallet, produce it and ship it. ## Create an order ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/orders \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: store-order-1042" \ -d '{ "external_id": "1042", "shipping_method": "standard", "recipient": { "name": "Jane Doe", "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": "+1 415 555 0100", "email": "jane@example.com" }, "items": [ { "sku": "CA-TEE-BLK-M", "quantity": 2, "external_line_id": "line-1" } ], "packing_slip": { "message": "Thanks for shopping with Company A!", "customer_order_number": "#1042" }, "metadata": { "channel": "web" } }' ``` ```js title="Node.js" const res = await fetch("https://swaglists.com/api/v1/orders", { method: "POST", headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `store-order-${storeOrder.id}`, // stable per store order }, body: JSON.stringify({ external_id: String(storeOrder.id), shipping_method: "standard", recipient: { name: "Jane Doe", address1: "123 Market St", city: "San Francisco", state_code: "CA", postal_code: "94107", country_code: "US", email: "jane@example.com", }, items: [{ sku: "CA-TEE-BLK-M", quantity: 2, external_line_id: "line-1" }], }), }); const order = await res.json(); if (!res.ok) throw new Error(`${order.error.code}: ${order.error.message}`); console.log(order.id, order.status, order.total); ``` ```python title="Python" res = requests.post( "https://swaglists.com/api/v1/orders", headers={"Authorization": f"Bearer {os.environ['API_KEY']}", "Idempotency-Key": f"store-order-{store_order_id}"}, json={ "external_id": str(store_order_id), "shipping_method": "standard", "recipient": { "name": "Jane Doe", "address1": "123 Market St", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "email": "jane@example.com", }, "items": [{"sku": "CA-TEE-BLK-M", "quantity": 2, "external_line_id": "line-1"}], }, ) order = res.json() res.raise_for_status() print(order["id"], order["status"], order["total"]) ``` ### Items Identify each item with **exactly one** of: | Reference | Use it when | | --- | --- | | `sku` | You sell [products](https://swaglists.com/docs/products.md) you created — the usual case. | | `product_variant_id` | Same, by our variant id (`pvar_…`). | | `catalog_variant_id` + `designs` | The artwork is different for every order — see [Personalized orders](https://swaglists.com/docs/personalized-orders.md). | Plus `quantity` (1–1,000 per line), and optionally `external_line_id` (your line item id), `metadata` and `retail_price` — what your customer paid per unit, in cents. The retail price (default: the product variant's or product's retail price) is what international packing slips show and what is declared for customs; send it so neither shows our price. An order has 1–100 items. ### Item images (`mockup_url`) Every item has a `mockup_url`: an image of the item as it is produced, which you can show in your order history or emails (the link doesn't expire). - We render it in the background from the order's own `designs` — the snapshot taken when the order was created — a few seconds after you create the order. Editing a product later never changes the image of an order that was already placed. - Until the rendering is done, `mockup_url` is the product's mockup (`sku` / `product_variant_id` items) or the catalog photo of the variant (personalized items). The `order.created` webhook is built when the order is created, so it carries this first image; `GET /orders/{id}` returns the rendered one a few seconds later. - Rendering never delays production. ### Recipient `name`, `address1`, `city` and `country_code` (ISO 3166-1 alpha-2) are required. `state_code` (preferably the ISO subdivision code, e.g. `CA`, `NY`, `ON`, `NSW`) is required for the US, Canada and Australia and must be a valid state or province for the US and Canada. Full names such as `British Columbia` or `New South Wales` are converted to the code for those three countries; elsewhere the region is kept as sent. `postal_code` is required unless the country has none (e.g. HK, AE). `email` is used for [shipment notifications](https://swaglists.com/docs/shipping.md#customer-emails) if you enable them; `phone` helps carriers. ### Other fields | Field | Notes | | --- | --- | | `external_id` | Your order id (up to 128 printable ASCII characters). **Unique per mode** — a second order with the same id fails with `409 duplicate_external_id` (with `existing_id`). Fetch with `GET /orders/ext:`. | | `shipping_method` | `economy`, `standard` (default), `express` or `priority`. See [Shipping](https://swaglists.com/docs/shipping.md). | | `packing_slip` | `message`, `brand_name`, `logo_file_id`, `customer_order_number` — override your account branding for this order. | | `gift_message`, `customer_note` | Free text (500 / 1,000 characters). | | `metadata` | Up to 50 string key/value pairs for your own references. | | `confirm` | `true`: reserve credits and start production now. `false`: create a draft. Default: your account's auto-confirm setting (on by default). | The response is the [order](https://swaglists.com/docs/objects.md#order) (`201`). Always send an `Idempotency-Key` — see [Idempotency](https://swaglists.com/docs/idempotency.md). ## Lifecycle ```text POST /orders POST /orders (confirm: false) (confirm: true — the default) │ │ ▼ ▼ ┌─────────┐ POST …/confirm ┌──────────────────────┐ │ draft │ ──────────────────► │ enough credits? │ └─────────┘ └──────────────────────┘ yes │ │ no (or held for review) ▼ ▼ ┌────────────┐ ┌───────────┐ │ processing │◄─│ on_hold │ hold resolved (e.g. credits added) └────────────┘ └───────────┘ │ ▼ ┌───────────────┐ │ in_production │ ← from here on: no cancellation └───────────────┘ │ ▼ ┌───────────────────┐ │ partially_shipped │ (only when parcels ship separately) └───────────────────┘ │ ▼ ┌──────────┐ ┌───────────┐ │ shipped │ ────► │ delivered │ └──────────┘ └───────────┘ canceled ◄── POST /orders/{id}/cancel while draft, on_hold or processing failed ◄── the order could not be produced (reserved credits are released) ``` `delivered`, `canceled` and `failed` are final. Every change sends a [webhook](https://swaglists.com/docs/webhooks.md) (`order.processing`, `order.in_production`, `order.shipped`, …) and adds an entry to the order timeline (`GET /orders/{id}/events`). ## Statuses | `status` | Meaning | | --- | --- | | `draft` | Created with `confirm: false` (or your account does not auto-confirm). Nothing is reserved; edit with `PATCH /orders/{id}`, then `POST /orders/{id}/confirm`. | | `on_hold` | Waiting — see `hold_reason`. Held orders resume automatically when the reason is resolved (e.g. credits arrive). | | `processing` | Confirmed: the total is reserved from your wallet and the order is being sent to production. Still cancelable. | | `in_production` | Being produced. Can no longer be canceled. | | `partially_shipped` | Some parcels shipped, others are still in production. | | `shipped` | Every item shipped. Tracking is in `shipments`. | | `delivered` | Every shipment was delivered. Final. | | `canceled` | Canceled before production; reserved credits released and spent credits refunded. Final. | | `failed` | Could not be produced; reserved credits released. Final. | ## Hold reasons An order is `on_hold` when it cannot continue yet. `hold_reason` says why: | `hold_reason` | What it means | How to resolve | | --- | --- | --- | | `insufficient_funds` | Your available credits did not cover the order total when it was confirmed. | Add credits (top-up or auto-recharge). Held orders resume automatically, oldest first. In test mode: `POST /test/wallet/fund`. | | `manual_review` | The order is being reviewed by our team before production (for example while your account is new). | No action needed; it continues automatically after review. Contact support if it takes longer than a business day. | | `artwork_issue` | A print file could not be produced from the artwork. | Our team is notified. Cancel the order and re-create it with corrected artwork, or contact support. | | `address_issue` | The shipping address could not be validated or is undeliverable. | Contact support with the corrected address, or cancel and re-create the order. | | `fulfillment_issue` | A production problem occurred (for example an item became unavailable). We are working on it. | Usually no action needed — the order continues or failed items are refunded to your wallet. Contact support if it stays on hold. | You receive `order.on_hold` when an order is held and `order.processing` when it continues. Orders on hold can be canceled. ## Credits and `funds_status` Confirming an order **reserves** its `total` from your wallet (it moves from `available` to `held`). The reserved credits are **spent** (captured) once the order is accepted for production, and **returned** if it is canceled or fails. See [Credits & billing](https://swaglists.com/docs/billing.md). | `funds_status` | Meaning | | --- | --- | | `none` | Nothing reserved (drafts). | | `awaiting` | On hold until enough credits are available. | | `held` | The order total is reserved (counted in your wallet's `held`). | | `partially_captured` | Part of the reserved amount was spent (some items accepted for production). | | `captured` | The order total was spent. | | `released` | Reserved credits were returned to `available` (canceled or failed before being spent). | | `partially_refunded` | Part of the spent amount was refunded to your wallet. | | `refunded` | Everything spent on the order was refunded to your wallet. | Not enough credits? By default the order is held (`on_hold` / `insufficient_funds`) and continues automatically — oldest first — as soon as credits arrive (top-up, auto-recharge, refund). With *hold and release manually*, held orders wait until you release them (dashboard **Release**, single or bulk, or `POST /orders/{id}/confirm`). If you set **Settings → Orders → When credits run out** to *reject*, `POST /orders` fails with `402 insufficient_funds` instead and nothing is created; after adding credits, send the order again with a **new** `Idempotency-Key` ([why](https://swaglists.com/docs/error-handling.md#retries)). ## Item statuses Each `items[]` entry has its own `status` and `shipped_quantity` — useful for orders that ship in several parcels: | Item `status` | Meaning | | --- | --- | | `draft` | The order is a draft. | | `pending` | Waiting to go into production. | | `in_production` | Being produced. | | `shipped` | Every unit shipped (`shipped_quantity` = `quantity`). | | `delivered` | Every unit was delivered. | | `canceled` | Canceled (its credits were returned). | | `failed` | Could not be produced (its credits were returned). | ## Drafts and confirmation Create an order with `"confirm": false` (or turn off auto-confirm in Settings → Orders) to get a `draft`. Drafts reserve nothing and can be changed: ```bash title="Edit a draft, then confirm it" curl -X PATCH https://swaglists.com/api/v1/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "shipping_method": "express", "items": [{ "sku": "CA-TEE-BLK-L", "quantity": 1 }] }' curl -X POST https://swaglists.com/api/v1/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa/confirm \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: confirm-ord_01k8x4b2q9w7e3r5t1y8u6i0pa" ``` `PATCH` accepts `external_id`, `recipient`, `shipping_method`, `items` (replaces all items), `packing_slip`, `gift_message`, `customer_note` and `metadata`, and returns `409 order_not_editable` once the order is confirmed. Confirming reserves credits exactly like creating with `confirm: true`. > **Tip** Use drafts when a human reviews orders before production, or to show your customer the final price: `POST /orders/estimate` gives the same totals without creating anything. ## Estimate an order `POST /orders/estimate` takes the same body as `POST /orders` and returns an [order estimate](https://swaglists.com/docs/objects.md#order_estimate): item prices, shipping, tax, `total`, every available shipping method with its price, the delivery estimate and whether your wallet covers it. Nothing is created and nothing is reserved. The estimate is exactly what the order will cost — see [Pricing](https://swaglists.com/docs/pricing.md). It runs the same checks as `POST /orders` (items, designs, files, packing-slip logo) except that `url` artwork is not downloaded and `external_id` is not checked for duplicates, so those can still fail when you place the order. ## Cancel an order ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa/cancel \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reason": "Customer changed their mind" }' ``` - You can cancel orders that are `draft`, `on_hold` or `processing` — as long as production has not started. - Once an order is `in_production` (or shipped) it cannot be canceled: `409 order_not_cancelable`. The same code, with a "try again in a minute" message, is returned while the order is being sent to production — retry shortly. - Reserved credits are released and any spent credits refunded to your wallet immediately; you receive `order.canceled`. - Canceling an order that is already canceled returns it unchanged (safe to retry). ## Retrieve and list | Request | Notes | | --- | --- | | `GET /orders/{id}` | By id, or by your id: `GET /orders/ext:1042`. | | `GET /orders` | Newest first. Filters: `status` (comma-separated), `external_id`, `q` (order number, external id, recipient name or email), `created[gte]`, `created[lte]` (Unix seconds or ISO-8601). | | `GET /orders/{id}/events` | The order timeline, oldest first. | ## After shipping `shipments[]` lists each parcel with carrier, tracking number and tracking URL, and `tracking_page_url` is your branded tracking page — see [Shipping & tracking](https://swaglists.com/docs/shipping.md). Problems after delivery (damaged or misprinted items)? Contact support with the order id; refunds are credited to your wallet (`order.refunded`). --- # Shipping & tracking > Shipping methods and rates, delivery estimates, shipments and tracking, the branded tracking page and customer shipment emails. Source: https://swaglists.com/docs/shipping (markdown: https://swaglists.com/docs/shipping.md) We ship every order from production straight to your customer, under your brand: the tracking page and shipment emails show your brand name, logo and support email (Settings → Branding) — never ours — and parcels ship without our branding. ## Shipping methods | `shipping_method` | Meaning | | --- | --- | | `economy` | Cheapest, slowest (not available everywhere). | | `standard` | Default. Tracked, available for every destination we ship to. | | `express` | Faster tracked delivery. | | `priority` | Fastest available service. | Set `shipping_method` on the order (default `standard`). Not every method is available for every destination and product — use the rates endpoint to see what is. ## Get shipping rates `POST /shipping/rates` returns the available methods with their price for a destination and a set of items. Use it in your checkout: the price of the method you pick is exactly what the order will charge for shipping. Items are identified like [order items](https://swaglists.com/docs/orders.md#items); nothing is created. ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/shipping/rates \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "recipient": { "country_code": "US", "state_code": "CA", "postal_code": "94107" }, "items": [{ "sku": "CA-TEE-BLK-M", "quantity": 2 }] }' ``` ```js title="Node.js" const res = await fetch("https://swaglists.com/api/v1/shipping/rates", { method: "POST", headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ recipient: { country_code: "US", state_code: "CA", postal_code: "94107" }, items: [{ sku: "CA-TEE-BLK-M", quantity: 2 }], }), }); const { data: rates } = await res.json(); for (const r of rates) console.log(r.method, r.price / 100, `${r.estimated_delivery_days.min}-${r.estimated_delivery_days.max} days`); ``` ```python title="Python" rates = requests.post( "https://swaglists.com/api/v1/shipping/rates", headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}, json={ "recipient": {"country_code": "US", "state_code": "CA", "postal_code": "94107"}, "items": [{"sku": "CA-TEE-BLK-M", "quantity": 2}], }, ).json()["data"] for r in rates: print(r["method"], r["price"] / 100, r["estimated_delivery_days"]) ``` ```json title="Response" { "object": "list", "data": [ { "object": "shipping_rate", "method": "standard", "name": "Standard", "price": 475, "currency": "usd", "min_days": 3, "max_days": 5, "estimated_delivery_days": { "min": 5, "max": 9 } } ], "has_more": false, "url": "/api/v1/shipping/rates" } ``` - `price` — shipping price in cents for **these items** to this destination. - `min_days` / `max_days` — carrier transit time in business days. - `estimated_delivery_days` — business days from order to delivery, **including production time**. Shipping is priced per parcel group: the first item of a group pays the first-item rate, every additional unit a lower additional-item rate. Ordering several units together is cheaper than separate orders. [`POST /orders/estimate`](https://swaglists.com/docs/orders.md#estimate) also returns every method with its price for the full order. A method that is not available for the destination fails on order creation with `400 shipping_method_unavailable` (the message lists the available ones). ## Shipments & tracking When a parcel leaves production, the order gets a [shipment](https://swaglists.com/docs/objects.md#shipment) and you receive `shipment.created` together with `order.shipped` (or `order.partially_shipped` when more parcels follow): ```json title="order.shipments[0]" { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [{ "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 }], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null } ``` `tracking_url` always points at the **carrier's** public tracking page. Push the carrier, number and URL to your store (see the [integration guides](https://swaglists.com/docs/shopify.md)) or let us email your customer. `shipment.updated` fires when the tracking status changes and `shipment.delivered` when the parcel is delivered; `order.delivered` follows once every parcel is delivered. | Shipment `status` | Meaning | | --- | --- | | `pending` | Created, not yet handed to the carrier. | | `label_created` | Shipping label created; waiting for carrier pickup. | | `in_transit` | On its way. | | `out_for_delivery` | With the carrier for delivery today. | | `delivered` | Delivered. | | `exception` | The carrier reported a problem (address, damage, delay). | | `returned` | Returned to sender. | ## Branded tracking page Every confirmed order has a `tracking_page_url` (`https://swaglists.com/track/…`): a page with your brand name, logo and colors that shows the order's progress and parcels — no login, no prices, and nothing about us. Link it from your order confirmation email or your customer account pages. Drafts have `tracking_page_url: null` until you confirm them. ## Customer shipment emails Turn on **Settings → Orders → Email customers when their order ships** and we email the recipient (`recipient.email`) once per parcel when it ships, with the carrier, tracking link and your tracking page — sent under your brand name, with replies going to your support email. Turn it off if your store already sends shipping confirmations. Emails are only sent for live orders. ## Packing slips & branding Packing slips carry your branding from Settings → Branding (brand name, logo, message, return address) wherever the product ships with a printed slip. Override it per order with `packing_slip`: ```json { "packing_slip": { "brand_name": "Company A Outlet", "message": "Thank you!", "customer_order_number": "#1042", "logo_file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9" } } ``` `gift_message` is printed for gift orders. Upload logos with `POST /files` (`purpose: logo`). --- # Credits & billing > The prepaid credit wallet — available vs held credits, bonus tiers, top-ups, auto-recharge, what happens without enough credits, and refunds. Source: https://swaglists.com/docs/billing (markdown: https://swaglists.com/docs/billing.md) Orders are paid from a **prepaid credit wallet**: you buy credits, and each order spends credits. There are no invoices to chase and no card charge per order — your integration only needs to keep the wallet funded. One credit cent = one US cent. ## The wallet ```bash title="cURL" curl https://swaglists.com/api/v1/wallet -H "Authorization: Bearer $API_KEY" ``` ```json title="Response" { "object": "wallet", "mode": "live", "livemode": true, "currency": "usd", "available": 482550, "held": 12480, "total": 495030, "low_balance_threshold": 10000 } ``` | Field | Meaning | | --- | --- | | `available` | Spendable credits, in cents. New orders reserve from here. Can be negative after a payment dispute. | | `held` | Credits reserved for confirmed orders that have not been accepted for production yet. | | `total` | `available` + `held`. | | `low_balance_threshold` | You get a `wallet.low_balance` event (and an email) when `available` drops below it. Set it in Settings → Orders. | Live and test mode have separate wallets. Test credits are free — see [Test mode](https://swaglists.com/docs/test-mode.md#test-credits). ## How an order uses credits 1. **Confirm** — the order total is moved from `available` to `held` (a `hold` transaction). 2. **Accepted for production** — the held credits are spent (`capture`). An order produced in several parts captures each part separately. 3. **Canceled or failed before that** — the held credits go back to `available` (`release`). Canceled after capture? The spent credits are refunded (`refund`). Every change is an immutable [transaction](https://swaglists.com/docs/objects.md#transaction) — list them with `GET /wallet/transactions` (filter by `type` or `order_id`): | Transaction `type` | Meaning | | --- | --- | | `topup` | Credits you bought (the paid amount). | | `bonus` | Bonus credits of a top-up (a separate entry). | | `hold` | Credits reserved for an order (`amount` −X, `held_delta` +X). | | `release` | Reserved credits returned to available (canceled/failed order). | | `capture` | Reserved credits spent when production of (part of) an order was accepted (`held_delta` −X). | | `charge` | Credits spent directly, without a reservation. | | `refund` | Credits refunded for an order. | | `adjustment` | A correction by support (positive or negative), with a reason. | | `reversal` | Credits taken back after a top-up was refunded or disputed (can make `available` negative). | | `test_credit` | Free test credits (test mode only). | ## Bonus tiers Bigger top-ups earn bonus credits on top of what you pay: | You pay | Bonus | Example: credits you receive | | --- | --- | --- | | Less than $1,000 | 0% | $500 → $500 | | $1,000 – $2,499.99 | 3% | $1,000 → $1,030 | | $2,500 – $4,999.99 | 4% | $2,500 → $2,600 | | $5,000 and above | 5% | $5,000 → $5,250 | For example, paying **$5,000** gives **$5,250** in credits. The bonus is added as a separate `bonus` transaction. Tiers can change; the **Buy credits** page always shows the current ones before you pay. Bonus credits are not refundable. ## Buying credits (top-ups) Top-ups are made in the dashboard (**Wallet → Buy credits**) by owners, admins and finance members. Pay by card or US bank account (ACH); bank transfer may also be available. The minimum top-up is $100. - **Card** — credits arrive within seconds. - **US bank account (ACH)** — cheaper for large amounts; credits arrive when the payment clears (usually a few business days). The top-up is `processing` meanwhile. You receive `topup.succeeded` and `wallet.credited` events when credits arrive (`topup.failed` if a payment fails). List top-ups with `GET /topups`. **Invoices.** Every card or bank purchase comes with an invoice (download it from the purchase in the wallet). It shows the legal name, billing address and tax ID (VAT, EIN, GST…) from **Settings → Billing** — fill them in before buying. Reselling? Upload your resale or exemption certificate on the same page; once our team approves it, no sales tax is added to your orders. ## Auto-recharge Turn on **Wallet → Recharge automatically**: when `available` falls below your threshold, we charge your saved card or bank account for the amount you chose (bonus tiers apply). Failed recharges notify you and auto-recharge pauses after repeated failures. With auto-recharge on, orders rarely wait for credits. ## When credits run out What happens to an order your available credits cannot cover is a setting (**Settings → Orders → When credits run out**): | Setting | Result | | --- | --- | | **Hold** (default) | The order is created `on_hold` with `hold_reason: insufficient_funds` and `funds_status: awaiting`. You get `order.on_hold`. As soon as credits arrive (top-up, auto-recharge, refund), held orders continue automatically, **oldest first**, and you get `order.processing`. | | **Hold, release manually** (`hold_manual`) | The order is created `on_hold` / `insufficient_funds` like *Hold*, but adding credits does **not** release it. You release held orders yourself: one at a time or in bulk from **Orders** in the dashboard, or with `POST /orders/{id}/confirm` from the API. Releasing reserves the order total first; if your credits still don't cover it, it stays on hold (the API answers `402 insufficient_funds`). New orders your credits cover are not blocked by held ones. | | **Reject** | `POST /orders` (and `POST /orders/{id}/confirm`) fail with `402 insufficient_funds`; nothing is created. The error includes `required_amount` and `available_amount`. | Whatever the setting, **nothing is ever sent to production unless its credits are reserved in your wallet first.** With *Hold* you can also release a held order early (after a top-up, before the automatic retry runs) the same way: dashboard **Release** or `POST /orders/{id}/confirm`. Holding is friendlier when your customer has already paid you; rejecting suits integrations that want to retry themselves. When you retry a rejected request after adding credits, send it with a **new** `Idempotency-Key` (for example `order-1042:2`): the 402 is stored under the original key for 24 hours, so reusing that key returns the same error. `external_id` still prevents a duplicate order. Check `GET /account` → `settings.insufficient_funds_behavior` to see which applies. `POST /orders/estimate` returns `wallet_available` and `sufficient_funds` so you can check before ordering. ## Refunds - **Order refunds go to your wallet.** Canceling an order releases or refunds its credits automatically. If support refunds an order (for example a damaged item), the credits are added to `available` (`refund` transaction, `order.refunded` and `wallet.credited` events). - **Unused credits** can be refunded to the original payment method on request — use **Settings → Billing → Refund unused credits** (or contact support). Only the paid portion of unused credits is refundable; bonus credits from that top-up are removed proportionally. Owners can also ask to close the account there. - **Payment disputes** (chargebacks) on a top-up remove its credits, which can make `available` negative; new orders wait until the balance is positive again. ## Credits API | Request | Returns | | --- | --- | | `GET /wallet` | The [wallet](https://swaglists.com/docs/objects.md#wallet) of the key's mode. | | `GET /wallet/transactions` | Every balance change, newest first (`type`, `order_id` filters). | | `GET /topups`, `GET /topups/{id}` | Credit purchases (live mode). | | `POST /test/wallet/fund` | Free test credits (test mode). | All need the `wallet:read` scope (the test helper needs full access). --- # Pricing > Your wholesale prices, what an order costs (items, extra placements, shipping, tax), and the guarantee that an estimate equals the charge. Source: https://swaglists.com/docs/pricing (markdown: https://swaglists.com/docs/pricing.md) Every price you see in the API is **your wholesale price** — what we charge you. You set your own retail prices in your store; the difference is your margin. ## What an order costs ```text total = Σ items (unit price × quantity) + shipping + tax ``` | Part | Where you see it | | --- | --- | | **Unit price** of a catalog variant (artwork on one placement) | `catalog_products[].variants[].price`, `price_from` | | **Unit price** of your product variant (with all its designs) | `products[].variants[].price` | | **Extra placements** | Included in the product variant price. Some products include one placement and charge for each additional one; others charge per placement. | | **Shipping** | `POST /shipping/rates` — first item + each additional item, per destination and method. See [Shipping](https://swaglists.com/docs/shipping.md#rates). | | **Tax** | US sales tax for the shipping address (state, county and city rates, and each product's taxability: clothing is exempt or reduced in some states), calculated automatically in the states where we collect, and paid from your credits with the order. `0` elsewhere. Accounts with an approved resale certificate (Settings → Billing) are tax-exempt and collect tax from their own customers. The tax in an estimate is the tax you're charged when you place that order. | All amounts are integers in cents (`usd`). ## Estimate = charge Prices are computed from our price tables, never from a live quote at order time. `POST /orders/estimate` runs the **exact same calculation** as `POST /orders`, so the estimate's `total` is what the order will charge — as long as nothing changes between the two calls (prices, items, address, shipping method). ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/orders/estimate \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "recipient": { "name": "Jane Doe", "address1": "123 Market St", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US" }, "shipping_method": "standard", "items": [{ "sku": "CA-TEE-BLK-M", "quantity": 2 }] }' ``` ```js title="Node.js" const res = await fetch("https://swaglists.com/api/v1/orders/estimate", { method: "POST", headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify(orderBody), // the same body you would POST to /orders }); const estimate = await res.json(); console.log(estimate.total, estimate.sufficient_funds, estimate.available_shipping_methods.map((m) => [m.method, m.price])); ``` ```python title="Python" estimate = requests.post( "https://swaglists.com/api/v1/orders/estimate", headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}, json=order_body, # the same body you would POST to /orders ).json() print(estimate["total"], estimate["sufficient_funds"]) ``` ```json title="Response (abridged)" { "object": "order_estimate", "currency": "usd", "shipping_method": "standard", "items": [{ "index": 0, "sku": "CA-TEE-BLK-M", "quantity": 2, "unit_price": 1299, "total_price": 2598 }], "subtotal": 2598, "shipping": 700, "tax": 0, "total": 3298, "available_shipping_methods": [{ "method": "standard", "price": 700 }, { "method": "express", "price": 1450 }], "estimated_delivery_days": { "min": 5, "max": 9 }, "wallet_available": 482550, "sufficient_funds": true } ``` Once an order is created, its prices are locked: items keep the `unit_price` they were created with, even if catalog prices change later. ## Price changes Wholesale prices can change (for example when our material costs change or your account moves to a different price list). When a catalog variant used by one of your products changes price you receive a [`catalog.price_changed`](https://swaglists.com/docs/events.md#catalog.price_changed) event with the affected `catalog_variant_ids`. Re-read the prices (`GET /products/{id}` or `GET /catalog/products/{id}`) and update your retail prices if needed. Orders already created are not affected. You don't need webhooks to hear about it: every change to the price of one of your SKUs is also listed, with the old and new price, under **Products → Price changes** in the dashboard, and your team gets a dashboard notification and an email digest (choose who receives it in **Settings → Notifications → Price changes**). New prices apply immediately to orders created after the change. ## Volume pricing Prices depend on your account's price list. Large accounts can get custom pricing — contact support@swaglists.com. Separately, larger credit top-ups earn [bonus credits](https://swaglists.com/docs/billing.md#bonus-tiers) — extra credits that lower your effective cost. --- # Personalized orders > Send different artwork with every order — names, photos or generated designs — by URL or uploaded file, without creating a product first. Source: https://swaglists.com/docs/personalized-orders (markdown: https://swaglists.com/docs/personalized-orders.md) For products that are different for every customer (a name on the back of a jersey, a pet photo on a mug, a generated poster), order a **catalog variant with inline designs** instead of a SKU. You do not need to create a product first. ```json { "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "quantity": 1, "designs": [ { "placement": "front", "layers": [{ "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9" }] }, { "placement": "back", "layers": [{ "url": "https://cdn.example.com/personalized/order-1042-back.png" }] } ] } ``` - `catalog_variant_id` — the exact color/size from `GET /catalog/products/{id}` (`variants[].id`). - `designs` — the same format as product [designs](https://swaglists.com/docs/designs.md): one entry per placement, layers with `file_id` **or** `url`, optional `position` and `opacity`. Every `required` placement must have artwork. Inline items are validated like products (artwork type, size, resolution) and priced like products — the estimate includes extra placements. ## Two ways to send the artwork | | Upload first (`file_id`) | Send a URL (`url`) | | --- | --- | --- | | How | `POST /files` (multipart or `{ "url": … }`) → use the `file_…` id | Put the public image URL in the layer | | When it is downloaded | During the upload call | While we create the order | | Errors surface | On upload (`file_invalid`, `file_too_large`, `download_failed`, `download_not_an_image`, …) | On order creation: a `400` (`download_failed`, `download_not_an_image`, `file_invalid`, `file_download_failed`, …) with `param` = `items[i].designs[j].layers[k].url` | | Best for | Retries, large files, reusing artwork | Simplicity — one request | URLs must be publicly reachable over http(s) (no login, no short-lived signed URL that might expire before we fetch it) and return the image itself. A link to a web page — a file host's share page, for example — fails with `download_not_an_image`: use the host's direct download link instead. - We follow up to 5 redirects, and retry once when the host answers with a temporary error (a dropped connection or HTTP 429, 502, 503, 504) — so a single CDN hiccup doesn't fail your order. - Images are checked before they are decoded: at most 30,000 px on a side and 250 megapixels in total (`file_too_large`). - Photos with an EXIF rotation are stored upright, and CMYK images are converted to RGB, so what you see in the preview is what is printed. - Downloading the same image again (for example when you retry an order) reuses the file we already stored instead of keeping a copy. ## Example: a name on the back Generate the personalized image in your backend (any image library), host it or upload it, then order: ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/orders \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: store-order-2087" \ -d '{ "external_id": "2087", "recipient": { "name": "Sam Lee", "address1": "9 Elm St", "city": "Austin", "state_code": "TX", "postal_code": "78701", "country_code": "US" }, "items": [{ "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "quantity": 1, "external_line_id": "line-1", "designs": [ { "placement": "front", "layers": [{ "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9" }] }, { "placement": "back", "layers": [{ "url": "https://cdn.example.com/names/sam-lee-23.png", "position": { "x": 0.1, "y": 0.1, "width": 0.8, "height": 0.3 } }] } ] }] }' ``` ```js title="Node.js" const res = await fetch("https://swaglists.com/api/v1/orders", { method: "POST", headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `store-order-${storeOrder.id}`, }, body: JSON.stringify({ external_id: String(storeOrder.id), recipient, items: [ { catalog_variant_id: "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", quantity: 1, designs: [ { placement: "front", layers: [{ file_id: LOGO_FILE_ID }] }, { placement: "back", layers: [{ url: personalizedImageUrl }] }, // centered & fitted when no position ], }, ], }), }); ``` ```python title="Python" order = requests.post( "https://swaglists.com/api/v1/orders", headers={"Authorization": f"Bearer {os.environ['API_KEY']}", "Idempotency-Key": f"store-order-{store_order_id}"}, json={ "external_id": str(store_order_id), "recipient": recipient, "items": [{ "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "quantity": 1, "designs": [ {"placement": "front", "layers": [{"file_id": LOGO_FILE_ID}]}, {"placement": "back", "layers": [{"url": personalized_image_url}]}, ], }], }, ).json() ``` The position above makes the name box 80% of the area wide and 30% high, near the top — make the image's aspect ratio match the box (see [keeping the aspect ratio](https://swaglists.com/docs/designs.md#position)) or omit `position` to fit it automatically. ## Mixing personalized and regular items One order can contain both SKU items and inline items: ```json "items": [ { "sku": "CA-TEE-BLK-M", "quantity": 2 }, { "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "quantity": 1, "designs": [ … ] } ] ``` ## Tips - **Price check:** send the same body to `POST /orders/estimate` first, or show your customers a fixed price per product and placement. - **Resolution:** generate images at the placement's `recommended_width_px` (300 DPI); low-resolution artwork is rejected below 75 DPI. - **Storefront personalization fields:** Shopify line item `properties` and WooCommerce item meta carry the customer's input — render the image from them in your webhook handler, then send the order. See the [Shopify recipe](https://swaglists.com/docs/shopify.md). - Inline items show `product_id: null` and `sku: null` on the order, and `designs` is exactly what will be printed. - **Previews:** a few seconds after the order is created, `mockup_url` shows your design on the product, rendered from the order's own `designs` (it never changes when you later edit a product). Until it is rendered — and in the `order.created` webhook, which is built when the order is created — `mockup_url` is the catalog photo of the variant. Fetch the order again (`GET /orders/{id}`) to get the rendered image; `mockup_url` links don't expire. --- # Webhooks > Get notified when orders are produced, shipped and delivered — endpoints, events, the payload, signature verification (libraries and manual HMAC), retries, replays and best practices. Source: https://swaglists.com/docs/webhooks (markdown: https://swaglists.com/docs/webhooks.md) Webhooks push [events](https://swaglists.com/docs/events.md) to your server as they happen — an order went into production, shipped, was delivered; a product's mockups are ready; credits arrived. Deliveries follow the **[Standard Webhooks](https://www.standardwebhooks.com/)** specification, so you can verify them with an official library in any language. ## 1. Create an endpoint Register an HTTPS URL in **Developers → Webhooks** or with the API. Each mode has its own endpoints: test endpoints receive test events, live endpoints live events. ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/webhook_endpoints \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "url": "https://example.com/webhooks/print", "enabled_events": ["order.in_production", "order.shipped", "order.delivered", "order.on_hold", "order.failed", "order.canceled"], "description": "Order status sync" }' ``` ```js title="Node.js" const res = await fetch("https://swaglists.com/api/v1/webhook_endpoints", { method: "POST", headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://example.com/webhooks/print", enabled_events: ["*"] }), }); const endpoint = await res.json(); console.log(endpoint.id, endpoint.secret); // store the secret now — it is not shown again ``` ```python title="Python" endpoint = requests.post( "https://swaglists.com/api/v1/webhook_endpoints", headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}, json={"url": "https://example.com/webhooks/print", "enabled_events": ["*"]}, ).json() print(endpoint["id"], endpoint["secret"]) # store the secret now — it is not shown again ``` - `enabled_events` — a list of event types or `["*"]` for everything (the default). New event types are added over time; with `["*"]` your handler must ignore types it does not know. - The response includes the signing **`secret`** (`whsec_…`) **once**. Store it like a password. Lost it? [Rotate it](#rotation). - Live endpoints must use `https://` and a public host. Test endpoints may use `http://`. - Up to 16 endpoints per mode. Send a test event to check your handler: `POST /webhook_endpoints/{id}/test` (or **Send test event** in the dashboard) delivers a `webhook_endpoint.test` event to that endpoint only. ## 2. Events | Event | When it fires | `data.object` | | --- | --- | --- | | [`order.created`](https://swaglists.com/docs/events.md#order.created) | An order was created through the API or the dashboard — as a draft (`confirm: false`) or confirmed. A confirmed order also sends `order.processing` or `order.on_hold`. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.updated`](https://swaglists.com/docs/events.md#order.updated) | A draft's details changed (recipient, items, shipping method, metadata, external id), or something about the order changed that has no more specific event. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.confirmed`](https://swaglists.com/docs/events.md#order.confirmed) | A draft order was confirmed (`POST /orders/{id}/confirm`). Followed by `order.processing` or `order.on_hold`. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.on_hold`](https://swaglists.com/docs/events.md#order.on_hold) | The order is waiting: not enough credits (`hold_reason: insufficient_funds`), or held for review, artwork, address or a production issue. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.processing`](https://swaglists.com/docs/events.md#order.processing) | Credits were reserved and the order was sent to production — right after confirmation, or when a held order is released (e.g. after you added credits). | [order](https://swaglists.com/docs/objects.md#order) | | [`order.in_production`](https://swaglists.com/docs/events.md#order.in_production) | Production started. From now on the order can no longer be canceled. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.partially_shipped`](https://swaglists.com/docs/events.md#order.partially_shipped) | Some items shipped; more parcels follow. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.shipped`](https://swaglists.com/docs/events.md#order.shipped) | Every item shipped. `shipments` has the carrier, tracking number and tracking URL of each parcel. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.delivered`](https://swaglists.com/docs/events.md#order.delivered) | Every shipment was delivered. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.canceled`](https://swaglists.com/docs/events.md#order.canceled) | The order was canceled (by you or by support). Reserved credits were released and spent credits refunded to your wallet. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.failed`](https://swaglists.com/docs/events.md#order.failed) | The order could not be produced. Reserved credits were released; no further events follow. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.refunded`](https://swaglists.com/docs/events.md#order.refunded) | Credits were refunded to your wallet for this order (full or partial), for example when support accepts a claim for a damaged or misprinted item. | [order](https://swaglists.com/docs/objects.md#order) | | [`shipment.created`](https://swaglists.com/docs/events.md#shipment.created) | A parcel was handed to the carrier and has tracking information. Sent together with `order.partially_shipped` or `order.shipped`. | [shipment](https://swaglists.com/docs/objects.md#shipment) | | [`shipment.updated`](https://swaglists.com/docs/events.md#shipment.updated) | A parcel's tracking status changed (e.g. `in_transit` → `out_for_delivery`, `exception`, `returned`). | [shipment](https://swaglists.com/docs/objects.md#shipment) | | [`shipment.delivered`](https://swaglists.com/docs/events.md#shipment.delivered) | A parcel was delivered. | [shipment](https://swaglists.com/docs/objects.md#shipment) | | [`product.created`](https://swaglists.com/docs/events.md#product.created) | A product was created (API or designer). Mockups are rendered afterwards and announced with `product.updated`. | [product](https://swaglists.com/docs/objects.md#product) | | [`product.updated`](https://swaglists.com/docs/events.md#product.updated) | A product changed: details, designs, variants, or its mockups finished rendering. | [product](https://swaglists.com/docs/objects.md#product) | | [`product.deleted`](https://swaglists.com/docs/events.md#product.deleted) | A product was archived (`DELETE /products/{id}`). Its SKUs stay reserved; restoring it sends `product.updated`. | [product](https://swaglists.com/docs/objects.md#product) | | [`catalog.price_changed`](https://swaglists.com/docs/events.md#catalog.price_changed) | The wholesale price of catalog variants used by your products changed (our costs or your price list changed). Re-fetch prices and adjust your store if needed. | [catalog_price_change](https://swaglists.com/docs/objects.md#catalog_price_change) | | [`wallet.credited`](https://swaglists.com/docs/events.md#wallet.credited) | Credits were added to the wallet: a top-up and its bonus (one event each), a refund for an order, an adjustment by support, or test credits. | [transaction](https://swaglists.com/docs/objects.md#transaction) | | [`wallet.low_balance`](https://swaglists.com/docs/events.md#wallet.low_balance) | Available credits fell below your low-balance threshold (sent once until the balance recovers). | [wallet](https://swaglists.com/docs/objects.md#wallet) | | [`topup.succeeded`](https://swaglists.com/docs/events.md#topup.succeeded) | A credit purchase was paid and the credits (plus any bonus) were added. | [topup](https://swaglists.com/docs/objects.md#topup) | | [`topup.failed`](https://swaglists.com/docs/events.md#topup.failed) | A credit purchase failed (e.g. the card was declined or a bank debit was returned). | [topup](https://swaglists.com/docs/objects.md#topup) | | [`file.processed`](https://swaglists.com/docs/events.md#file.processed) | An uploaded file finished processing (`status` is `ready`, or `failed`). `url` is null in the event — fetch the file for a download URL. | [file](https://swaglists.com/docs/objects.md#file) | | [`webhook_endpoint.test`](https://swaglists.com/docs/events.md#webhook_endpoint.test) | Sent on request with `POST /webhook_endpoints/{id}/test` (or the dashboard) to that endpoint only. Use it to check your signature verification. Not listed by `GET /events` unless you filter for it. | [webhook_endpoint](https://swaglists.com/docs/objects.md#webhook_endpoint) | Full descriptions and an example payload for each type are in the [events reference](https://swaglists.com/docs/events.md). ### Every lifecycle step, exactly once An order moves through `order.in_production` → `order.shipped` → `order.delivered` (with `order.partially_shipped` when it ships in several parcels), and each parcel through `shipment.created` → `shipment.updated` (once per tracking change) → `shipment.delivered`. You receive **every one of these events exactly once**, even when production moves faster than our status updates — for example when an order is produced, shipped and delivered between two of our status checks. The steps are then recorded together, with that update, in lifecycle order: | Update received | Events, in order | | --- | --- | | `processing` → delivered | `order.in_production`, `shipment.created`, `order.shipped`, `shipment.delivered`, `order.delivered` | | `processing` → shipped | `order.in_production`, `shipment.created`, `order.shipped` | | `in_production` → delivered | `shipment.created`, `order.shipped`, `shipment.delivered`, `order.delivered` | | `processing` → one of two parcels shipped | `order.in_production`, `shipment.created`, `order.partially_shipped` | | `shipped` → out for delivery | `shipment.updated` | - **`data.object` is the current state**, as of the end of the update — the same rule as for every event. A back-filled `order.shipped` can therefore already show `"status": "delivered"`. Use the event `type` to know which step happened and `data.object` for where the order is now. - **`data.previous_attributes.status` forms a chain** across the events of one update: `processing` on `order.in_production`, `in_production` on `order.shipped`, `shipped` on `order.delivered`. - **Deliveries are spaced about one second apart**, so a healthy endpoint receives such a burst in lifecycle order. Retries can still reorder events (see [best practices](#best-practices)). - **Subscriptions are respected per event.** An endpoint subscribed only to `order.shipped` receives it even if the order has already been delivered by then. - **`updated_at` always moves forward.** Every change to an order — including a parcel-only change such as "out for delivery" — sets a later `updated_at` on the order. The order events of one such burst carry the same `updated_at`; events of later updates always carry a later one. - The order timeline (`GET /orders/{id}/events` and the dashboard) shows one entry per step as well. ## 3. The request We `POST` the [event](https://swaglists.com/docs/objects.md#event) as JSON: ```http POST /webhooks/print HTTP/1.1 Content-Type: application/json User-Agent: Swaglists-Webhooks/1.0 webhook-id: evt_01k8x4b2r1t3y5u7i9o1p3a5s7 webhook-timestamp: 1761057125 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= ``` ```jsonc title="Body (abridged)" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.shipped", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": true, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "external_id": "1042", "status": "shipped", "shipments": [ … ] }, "previous_attributes": { "status": "in_production" } } } ``` | Header | Value | | --- | --- | | `webhook-id` | The event id. **Identical on every retry** — use it to deduplicate. | | `webhook-timestamp` | Unix time (seconds) of this delivery attempt. | | `webhook-signature` | `v1,` — several space-separated signatures during a [secret rotation](#rotation). | - `data.object` is the full resource as of the event, in the same shape the API returns (an [order](https://swaglists.com/docs/objects.md#order), [shipment](https://swaglists.com/docs/objects.md#shipment), [product](https://swaglists.com/docs/objects.md#product), …). - `data.previous_attributes` (on status changes and `*.updated` events) holds the old values of fields that changed. - The same event objects are listed by `GET /events` for 30 days. ## 4. Verify signatures Every delivery is signed with your endpoint's secret: an HMAC-SHA256 over `webhook-id`, `webhook-timestamp` and the **raw request body**. Always verify before trusting a webhook — anyone can send a request to your URL. > **Important** Verify the **raw body bytes** exactly as received. Parsing the JSON first and re-serializing it changes the bytes and the signature will not match. In Express use `express.raw()`; in Next.js `await request.text()`; in Flask `request.get_data()`; in PHP `file_get_contents('php://input')`. ### With the official library (recommended) ```js title="Node.js" // npm install standardwebhooks express import express from "express"; import { Webhook } from "standardwebhooks"; const app = express(); const wh = new Webhook(process.env.WEBHOOK_SECRET); // "whsec_…" app.post("/webhooks/print", express.raw({ type: "*/*" }), (req, res) => { let event; try { event = wh.verify(req.body.toString("utf8"), req.headers); // throws if invalid or too old } catch { return res.status(400).send("invalid signature"); } enqueue(event); // process asynchronously res.sendStatus(204); }); app.listen(3000); ``` ```python title="Python" # pip install standardwebhooks flask import os from flask import Flask, request from standardwebhooks.webhooks import Webhook app = Flask(__name__) wh = Webhook(os.environ["WEBHOOK_SECRET"]) # "whsec_…" @app.post("/webhooks/print") def webhook(): try: event = wh.verify(request.get_data(), dict(request.headers)) # raw bytes except Exception: return "invalid signature", 400 enqueue(event) # process asynchronously return "", 204 ``` ```php title="PHP" $_SERVER['HTTP_WEBHOOK_ID'] ?? '', 'webhook-timestamp' => $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '', 'webhook-signature' => $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '', ]; try { $event = (new Webhook(getenv('WEBHOOK_SECRET')))->verify($payload, $headers); } catch (Exception $e) { http_response_code(400); exit('invalid signature'); } enqueue($event); // process asynchronously http_response_code(204); ``` ```ruby title="Ruby" # gem install standardwebhooks (Rails controller shown) require "standardwebhooks" class PrintWebhooksController < ActionController::API WEBHOOK = StandardWebhooks::Webhook.new(ENV.fetch("WEBHOOK_SECRET")) def create event = WEBHOOK.verify(request.raw_post, { "webhook-id" => request.headers["webhook-id"], "webhook-timestamp" => request.headers["webhook-timestamp"], "webhook-signature" => request.headers["webhook-signature"], }) PrintWebhookJob.perform_later(event) # process asynchronously head :no_content rescue StandardError head :bad_request end end ``` ```go title="Go" // go get github.com/standard-webhooks/standard-webhooks/libraries/go package main import ( "io" "log" "net/http" "os" standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go" ) func main() { wh, err := standardwebhooks.NewWebhook(os.Getenv("WEBHOOK_SECRET")) if err != nil { log.Fatal(err) } http.HandleFunc("/webhooks/print", func(w http.ResponseWriter, r *http.Request) { payload, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if err := wh.Verify(payload, r.Header); err != nil { http.Error(w, "invalid signature", http.StatusBadRequest) return } enqueue(payload) // process asynchronously w.WriteHeader(http.StatusNoContent) }) log.Fatal(http.ListenAndServe(":3000", nil)) } ``` The libraries also reject deliveries whose `webhook-timestamp` is more than 5 minutes away from your clock (replay protection) — keep your server's clock in sync. ### Manually (HMAC-SHA256) If you cannot add a dependency, verification is a few lines: 1. Take `webhook-id`, `webhook-timestamp` and the raw body. 2. Reject the request if the timestamp is not an integer or is more than 300 seconds from now. 3. Decode the secret: base64-decode everything after `whsec_` — those bytes are the HMAC key. 4. Compute `base64(HMAC-SHA256(key, ".."))`. 5. Compare it, in constant time, with each `v1,` entry of `webhook-signature` (space-separated). One match is enough; ignore other versions. ```js title="Node.js" import { createHmac, timingSafeEqual } from "node:crypto"; /** headers: a Fetch `Headers` object or Node's lowercase `req.headers`. Returns the parsed event or throws. */ export function verifyWebhook(rawBody, headers, secret, toleranceSeconds = 300) { const get = (name) => (typeof headers.get === "function" ? headers.get(name) : headers[name]); const id = get("webhook-id"); const timestamp = get("webhook-timestamp"); const signatures = get("webhook-signature"); if (!id || !timestamp || !signatures) throw new Error("missing webhook headers"); if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) { throw new Error("timestamp outside tolerance"); } const key = Buffer.from(secret.startsWith("whsec_") ? secret.slice(6) : secret, "base64"); const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest(); for (const entry of signatures.split(" ")) { const [version, signature] = entry.split(","); if (version !== "v1" || !signature) continue; const received = Buffer.from(signature, "base64"); if (received.length === expected.length && timingSafeEqual(received, expected)) return JSON.parse(rawBody); } throw new Error("invalid signature"); } // Next.js route handler (app/api/print-webhooks/route.js) export async function POST(request) { const raw = await request.text(); // raw body BEFORE parsing let event; try { event = verifyWebhook(raw, request.headers, process.env.WEBHOOK_SECRET); } catch { return new Response("invalid signature", { status: 400 }); } await enqueue(event); return new Response(null, { status: 204 }); } ``` ```python title="Python" import base64, hashlib, hmac, json, time def verify_webhook(raw: bytes, headers, secret: str, tolerance: int = 300) -> dict: h = {k.lower(): v for k, v in headers.items()} msg_id, ts, signatures = h.get("webhook-id"), h.get("webhook-timestamp"), h.get("webhook-signature") if not (msg_id and ts and signatures): raise ValueError("missing webhook headers") if not ts.isdigit() or abs(int(time.time()) - int(ts)) > tolerance: raise ValueError("timestamp outside tolerance") key = base64.b64decode(secret[6:] if secret.startswith("whsec_") else secret) expected = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + raw, hashlib.sha256).digest()).decode() for entry in signatures.split(" "): version, _, signature = entry.partition(",") if version == "v1" and hmac.compare_digest(signature, expected): return json.loads(raw) raise ValueError("invalid signature") # Flask: event = verify_webhook(request.get_data(), request.headers, os.environ["WEBHOOK_SECRET"]) ``` ```php title="PHP" $tolerance) throw new RuntimeException('timestamp outside tolerance'); $key = base64_decode(str_starts_with($secret, 'whsec_') ? substr($secret, 6) : $secret, true); $expected = base64_encode(hash_hmac('sha256', "{$id}.{$ts}.{$raw}", $key, true)); foreach (explode(' ', $signatures) as $entry) { [$version, $signature] = array_pad(explode(',', $entry, 2), 2, ''); if ($version === 'v1' && hash_equals($expected, $signature)) return json_decode($raw, true); } throw new RuntimeException('invalid signature'); } // $event = verify_webhook(file_get_contents('php://input'), getallheaders(), getenv('WEBHOOK_SECRET')); ``` ## 5. Respond quickly, process asynchronously - Return any **2xx** status within **15 seconds**. Anything else — 3xx (redirects are not followed), 4xx, 5xx, a timeout or a connection error — is a failure and will be retried. - Do the work **after** responding: put the event on a queue or into a database table and process it in the background. - Answer `410 Gone` to stop deliveries permanently: the endpoint is disabled immediately. - To slow us down, answer `429` or `503` with a `Retry-After` header; we wait at least that long before the next attempt. ## 6. Retries Failed deliveries are retried with increasing delays: | Attempt | Delay after the previous failure | Time since the event (approx.) | | --- | --- | --- | | 1 | — (immediately) | 0 | | 2 | 5 seconds | 5 seconds | | 3 | 5 minutes | 5 minutes | | 4 | 30 minutes | 35 minutes | | 5 | 2 hours | 2.6 hours | | 6 | 5 hours | 7.6 hours | | 7 | 10 hours | 17.6 hours | | 8 | 10 hours | 27.6 hours | That is 8 attempts over about 28 hours. Each attempt times out after 15 seconds. If your endpoint answers with a `Retry-After` header longer than the next delay, we wait that long instead (up to 24 hours). An endpoint whose deliveries all fail for 5 days is disabled automatically (you get an email); an endpoint that answers `410 Gone` is disabled immediately. Events are kept for 30 days. You can see every attempt with its response status in **Developers → Webhooks** (per endpoint) and **Developers → Events** (per event). ## 7. Replay and reconcile - **Resend from the dashboard** — open the event in **Developers → Events** and resend it to every subscribed endpoint, or resend a single delivery. Resends use the same event id, so your deduplication still works. - **Catch up after downtime** — list what you missed with `GET /events` (newest first, last 30 days), for example `GET /events?type=order.*&created[gte]=2026-10-20T00:00:00Z`. The events are the same objects as the webhook bodies. Process them with the same idempotent handler. Start a little before the last event you processed — `created[gte]` = its `created_at` minus 10 minutes — and skip the event ids you already have: an event is listed only once the change that created it is saved, so it can appear slightly after events with a later `created_at`. ## 8. Rotate the secret `POST /webhook_endpoints/{id}/rotate_secret` (or **Rotate secret** on the endpoint page) returns a new secret, once. With `expire_old_in_seconds` (up to 86,400) we sign with **both** secrets until the old one expires, so you can deploy without dropping deliveries: ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/webhook_endpoints/we_01k8x2m4n6b8v0c2x4z6a8s0d2/rotate_secret \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "expire_old_in_seconds": 3600 }' ``` During the overlap `webhook-signature` carries two `v1,` signatures; verification succeeds with either secret. Deploy the new secret, then let the old one expire. ## Best practices - **Deduplicate by event id.** Deliveries are *at least once*: the same event can arrive more than once (retries, resends). Store processed `id`s (or `webhook-id`) and skip duplicates. - **Don't rely on order.** Events can arrive out of order. Use `data.object.status` and timestamps (`updated_at`, `shipped_at`, …) to decide what is newest — never move an order backwards (for example from `shipped` to `in_production`). An event whose `data.object.updated_at` is older than the one you stored is stale; events of the [same update](#lifecycle-steps) share one `updated_at`, so treat equal values as current. When in doubt, fetch the current object with `GET /orders/{id}`. - **Ignore what you don't know.** New event types and new fields can appear at any time. - **Verify every request** and reject unsigned or stale ones. - **Subscribe narrowly** in production (only the types you handle) and keep separate endpoints for test and live. - **Monitor failures.** We email your team when an endpoint is disabled; re-enable it with `PATCH /webhook_endpoints/{id}` `{"status": "enabled"}` after fixing it, then reconcile with `GET /events`. ## Managing endpoints | Request | What it does | | --- | --- | | `GET /webhook_endpoints` | List endpoints of the key's mode (`status` filter). | | `GET /webhook_endpoints/{id}` | Retrieve an endpoint (without its secret). | | `PATCH /webhook_endpoints/{id}` | Change `url`, `enabled_events`, `description`; disable or re-enable with `status`. | | `DELETE /webhook_endpoints/{id}` | Delete the endpoint and its delivery history. | | `POST /webhook_endpoints/{id}/rotate_secret` | New signing secret (optional overlap). | | `POST /webhook_endpoints/{id}/test` | Send a `webhook_endpoint.test` event to this endpoint. | | `GET /events`, `GET /events/{id}` | Events of the last 30 days. | Scopes: `webhooks:read` to read, `webhooks:write` to manage. --- # Idempotency > Retry requests safely with Idempotency-Key, and make order forwarding duplicate-proof with external_id. Source: https://swaglists.com/docs/idempotency (markdown: https://swaglists.com/docs/idempotency.md) Networks fail. A request can time out after we processed it, and retrying blindly could create a second order. Two mechanisms make retries safe: 1. **`Idempotency-Key` header** — on every `POST`, `PATCH` and `DELETE`, so a retried request returns the original result instead of running twice. 2. **`external_id`** on orders — your own order id, unique per mode, so the same store order can never become two orders. Use both. ## Idempotency-Key ```bash title="cURL" curl -X POST https://swaglists.com/api/v1/orders \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 5d2c8b7e-9a4f-4f0e-8b1d-6c3a2e7f9b10" \ -d @order.json ``` How it works: | Situation | Result | | --- | --- | | First request with a key | Runs normally; the response is stored for **24 hours**. | | Same key, same method, path and body, after it finished | The **stored response** is returned (same status and body) with the header `Idempotent-Replayed: true`. Nothing runs again. | | Same key while the first request is still running | `409 idempotency_in_progress` with `Retry-After: 1` — wait and retry with the same key. | | Same key with a different method, path or body | `409 idempotency_key_reused`. Use a new key for a different request. | | The first attempt was rejected before it ran — invalid or malformed parameters (`400 parameter_invalid`, `invalid_json`, `415`), missing scope — or failed with a 5xx | Not stored — retry (or fix and retry) with the **same** key. | | The first attempt ran and returned a 4xx (for example an unknown SKU, or `402 insufficient_funds`) | Stored like a success: retrying the same request returns the same error, even after you fixed the cause. After changing the request — or fixing the cause outside it, such as adding credits — use a **new** key. | - Keys are up to 255 printable ASCII characters (no spaces). A UUID v4 is ideal. - Keys are scoped to your account and mode. Body comparison ignores JSON key order and whitespace. - `GET` requests are naturally idempotent and ignore the header. ### Choosing keys Derive the key from the thing you are doing, so every retry — even after a crash or a redeploy — uses the same key: | Operation | Good key | | --- | --- | | Forward store order 1042 | `order-1042` or `shopify:my-shop:1042` | | Confirm order `ord_…` | `confirm-ord_…` | | Create product for store product 9921 | `product-9921` | | One-off action from a script | A fresh UUID, reused on retries of that action | > **Warning** Don't generate a new random key on each retry of a network error, `429` or `5xx` — that defeats the purpose. Generate it once, store it with the job, reuse it. > **Tip** If a request failed with a 4xx because of something you then fixed outside the request (you added credits after a `402 insufficient_funds`, or created the missing SKU), retrying within 24 hours with the same key replays the stored error. Add a suffix for the new attempt (`order-1042:2`); `external_id` still prevents duplicates. ## external_id for orders Set `external_id` to your store's order id on every order. It is unique per mode: a second `POST /orders` with the same `external_id` fails with: ```json { "error": { "type": "conflict_error", "code": "duplicate_external_id", "message": "An order with external_id '1042' already exists.", "param": "external_id", "existing_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "doc_url": "https://swaglists.com/docs/errors#duplicate_external_id", "request_id": "req_01k8x4b2q7w9e1r3t5y7u9i1o3" } } ``` Treat this as success: the order already exists — use `existing_id`, or fetch it with `GET /orders/ext:1042`. This protects you even when your store sends the same "order paid" webhook twice days apart (after the 24-hour idempotency window). ## Retry recipe ```js title="Node.js" import { setTimeout as sleep } from "node:timers/promises"; /** POST with retries on network errors, 409 idempotency_in_progress, 429 and 5xx — always with the same key. */ async function postWithRetry(path, body, idempotencyKey, attempts = 5) { for (let attempt = 1; ; attempt++) { let res; try { res = await fetch(`https://swaglists.com/api/v1${path}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, }, body: JSON.stringify(body), }); } catch (err) { if (attempt >= attempts) throw err; // network error: retry await sleep(2 ** attempt * 250 + Math.random() * 250); continue; } const json = await res.json(); if (res.ok) return json; if (json.error?.code === "duplicate_external_id") return { id: json.error.existing_id, duplicate: true }; const retryable = res.status === 429 || res.status >= 500 || json.error?.code === "idempotency_in_progress"; if (!retryable || attempt >= attempts) throw Object.assign(new Error(json.error.message), { error: json.error }); const retryAfter = Number(res.headers.get("retry-after")); await sleep(retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 250 + Math.random() * 250); } } const order = await postWithRetry("/orders", orderBody, `order-${storeOrder.id}`); ``` ```python title="Python" import os, random, time, requests def post_with_retry(path, body, idempotency_key, attempts=5): for attempt in range(1, attempts + 1): try: res = requests.post( "https://swaglists.com/api/v1" + path, json=body, headers={"Authorization": f"Bearer {os.environ['API_KEY']}", "Idempotency-Key": idempotency_key}, timeout=30, ) except requests.RequestException: if attempt == attempts: raise time.sleep(2 ** attempt * 0.25 + random.random() * 0.25) continue data = res.json() if res.ok: return data error = data.get("error", {}) if error.get("code") == "duplicate_external_id": return {"id": error["existing_id"], "duplicate": True} retryable = res.status_code == 429 or res.status_code >= 500 or error.get("code") == "idempotency_in_progress" if not retryable or attempt == attempts: raise RuntimeError(f"{error.get('code')}: {error.get('message')}") retry_after = float(res.headers.get("Retry-After") or 0) time.sleep(retry_after or 2 ** attempt * 0.25 + random.random() * 0.25) order = post_with_retry("/orders", order_body, f"order-{store_order_id}") ``` ## Webhooks are idempotent too Our webhook deliveries can arrive more than once. Each event has a stable `id` (also the `webhook-id` header) — see [Webhooks best practices](https://swaglists.com/docs/webhooks.md#best-practices). --- # Pagination > Cursor pagination for list endpoints — limit, starting_after, ending_before and has_more. Source: https://swaglists.com/docs/pagination (markdown: https://swaglists.com/docs/pagination.md) List endpoints (`GET /orders`, `GET /products`, `GET /catalog/products`, `GET /events`, …) return a **list object** and use cursor pagination: ```json { "object": "list", "data": [ { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa" } ], "has_more": true, "url": "/api/v1/orders" } ``` | Parameter | Meaning | | --- | --- | | `limit` | Objects per page, 1–100 (default 20). | | `starting_after` | An object id: return the page **after** it (the next page). Use the `id` of the last object you received. | | `ending_before` | An object id: return the page **before** it (the previous page). Use the `id` of the first object of the current page. | Use one cursor at a time (`400 parameter_invalid` if you send both). `has_more` tells you whether another page exists in the direction you are paging. Lists are newest first (except order events, which are oldest first). Filters (for example `status` on orders) combine with cursors — keep the same filters while paging. Query parameters are **strict**: a parameter an endpoint does not document is rejected with `400 parameter_invalid` naming it (`Received unknown parameter: updated_since (query parameter).`), so a misspelled filter never silently returns everything. The one exception is `_`, which every endpoint accepts and ignores (for cache-busters added by HTTP tools). ## Cursors A cursor is the `id` of an object of the same list, so it carries the list's id prefix: | List | Cursor prefix | | --- | --- | | `GET /orders` | `ord_` | | `GET /products` | `prod_` | | `GET /catalog/products` | `cp_` | | `GET /files` | `file_` | | `GET /events` | `evt_` | | `GET /wallet/transactions` | `txn_` | | `GET /topups` | `top_` | | `GET /webhook_endpoints` | `we_` | A cursor with another prefix (for example a product id on `/orders`) is a `400 parameter_invalid` on `starting_after` / `ending_before`. A well-formed id that does not exist (an object deleted since) is accepted: the page simply starts after (or ends before) where that id would sort. The catalog is the exception — its order is curated (featured first), so `/catalog/products` cursors must be ids of products in the catalog. ## Iterate over everything ```js title="Node.js" async function* listAll(path, params = {}) { let startingAfter; while (true) { const query = new URLSearchParams({ ...params, limit: "100", ...(startingAfter ? { starting_after: startingAfter } : {}) }); const res = await fetch(`https://swaglists.com/api/v1${path}?${query}`, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); const page = await res.json(); if (!res.ok) throw new Error(`${page.error.code}: ${page.error.message}`); yield* page.data; if (!page.has_more || page.data.length === 0) return; startingAfter = page.data.at(-1).id; } } for await (const order of listAll("/orders", { status: "on_hold" })) { console.log(order.id, order.hold_reason); } ``` ```python title="Python" def list_all(path, **params): starting_after = None while True: query = {**params, "limit": 100} if starting_after: query["starting_after"] = starting_after res = requests.get("https://swaglists.com/api/v1" + path, params=query, headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}) res.raise_for_status() page = res.json() yield from page["data"] if not page["has_more"] or not page["data"]: return starting_after = page["data"][-1]["id"] for order in list_all("/orders", status="on_hold"): print(order["id"], order["hold_reason"]) ``` ```bash title="cURL" curl "https://swaglists.com/api/v1/orders?limit=100" -H "Authorization: Bearer $API_KEY" # next page: curl "https://swaglists.com/api/v1/orders?limit=100&starting_after=ord_01k8x4b2q9w7e3r5t1y8u6i0pa" -H "Authorization: Bearer $API_KEY" ``` ## Polling for new objects To fetch only what is new since your last sync, page **backwards** from the newest object you already have: `GET /events?ending_before=` returns newer events. Or filter by time: `GET /orders?created[gte]=2026-10-20T00:00:00Z` (see [time filters](#time-filters)). For near-real-time updates, prefer [webhooks](https://swaglists.com/docs/webhooks.md). ## Time filters `created[gte]` (at or after) and `created[lte]` (at or before) on `GET /orders` and `GET /events` accept: | Format | Example | Meaning | | --- | --- | --- | | Unix seconds | `1761955200` | Seconds since 1970-01-01 UTC (fractions allowed). | | Unix milliseconds | `1761955200000` | Any number above 100000000000 is read as milliseconds. | | ISO-8601 date | `2026-10-01` | Midnight **UTC** of that day. | | ISO-8601 date-time with an offset | `2026-10-01T09:30:00Z`, `2026-10-01T11:30:00+02:00` | That instant. | | ISO-8601 date-time without an offset | `2026-10-01T09:30:00` | Read as **UTC** — never as the server's or your local time. | URL-encode `+` offsets (`%2B02:00`); an unencoded `+` (which query strings turn into a space) is understood as well. Anything else — other formats, impossible dates such as `2026-02-30`, years outside 2000–2100 — is a `400 parameter_invalid` on the parameter. ## Full collections A few endpoints return a complete collection in the same list shape with `has_more: false` and no paging: `GET /catalog/categories`, `GET /products/{id}/variants`, `GET /orders/{id}/events` and `POST /shipping/rates`. --- # Errors > The error object, error types and HTTP statuses, which errors to retry, and how to debug with request ids. Source: https://swaglists.com/docs/error-handling (markdown: https://swaglists.com/docs/error-handling.md) Errors use conventional HTTP statuses (`2xx` success, `4xx` a problem with the request, `5xx` a problem on our side) and always have the same JSON body: ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "Invalid items[0].quantity: must be greater than or equal to 1.", "param": "items[0].quantity", "doc_url": "https://swaglists.com/docs/errors#parameter_invalid", "request_id": "req_01k8x4b2q7w9e1r3t5y7u9i1o3", "errors": [ { "param": "items[0].quantity", "code": "parameter_invalid", "message": "Invalid items[0].quantity: must be greater than or equal to 1." }, { "param": "recipient.country_code", "code": "parameter_invalid", "message": "Invalid recipient.country_code: must be an ISO 3166-1 alpha-2 country code." } ] } } ``` | Field | Meaning | | --- | --- | | `type` | Category of the error (table below). | | `code` | Stable, machine-readable code. **Branch on this**, not on the message. Every code is documented in the [error reference](https://swaglists.com/docs/errors.md). | | `message` | Human-readable explanation for developers (not meant for your end customers; wording may change). | | `param` | The parameter at fault, as a path into your request: `recipient.postal_code`, `items[1].sku`, `designs[0].layers[0].url`. | | `doc_url` | Link to the code's documentation. | | `request_id` | Id of the request, also in the `Request-Id` response header. Include it when you contact support. | | `errors` | Validation errors only: **every** problem found in the request (at most 50), each with `param`, `code` and `message`. The first entry is the top-level `code` / `param` / `message`. Fix them all before you retry — a 100-line order with five bad SKUs needs one round trip, not five. | Some errors carry extra fields: | Field | Sent with | Meaning | | --- | --- | --- | | `allowed_values` | a value outside a fixed set (e.g. `unknown_placement`, an enum such as `shipping_method` or the `status` filter) | The values that are accepted. | | `available_shipping_methods` | `shipping_method_unavailable` | The shipping methods you can use for this destination and these items. | | `max_length` | a field that is too long for every way we can fulfill the order | The longest value (in characters) that can be accepted — shorten the field to at most this. | | `max_quantity` | a quantity that is too large | The largest quantity that can be accepted. | | `existing_id` | `duplicate_external_id` | The id of the object that already has this external id. | | `required_amount` / `available_amount` / `currency` | `insufficient_funds` | Credits needed and available (cents). | | `required_scope` | `insufficient_scope` | The scope the API key is missing. | New fields may be added to the error object at any time — ignore the ones you don't use. ## Types and statuses | Status | `type` | Typical codes | Retry? | | --- | --- | --- | --- | | 400 | `invalid_request_error` | `parameter_invalid`, `resource_missing` (a referenced object does not exist), `invalid_json`, business rules like `variant_unavailable` | No — fix the request. | | 401 | `authentication_error` | `missing_api_key`, `invalid_api_key` | No — fix the key. | | 402 | `insufficient_funds_error` | `insufficient_funds` | After adding credits, with a **new** Idempotency-Key ([why](#retries)). | | 403 | `permission_error` | `insufficient_scope`, `live_mode_disabled`, `ip_not_allowed`, `test_mode_only` | No. | | 404 | `not_found_error` | `resource_missing`, `route_not_found` | No. | | 409 | `conflict_error`, `idempotency_error`, `invalid_request_error` | `duplicate_external_id`, `sku_taken`, `idempotency_key_reused`, `idempotency_in_progress`, `order_not_cancelable` | Only `idempotency_in_progress` (and "try again in a minute" cases). | | 413 / 415 | `invalid_request_error` | `request_too_large`, `unsupported_media_type` | No. | | 429 | `rate_limit_error` | `rate_limited` | Yes, after `Retry-After`. | | 500–503 | `api_error` | `internal_error` | Yes, with backoff and the **same** Idempotency-Key. | **404 vs 400 for missing objects:** a 404 means the object in the **URL** does not exist (`GET /orders/ord_unknown`). An unknown object referenced in the **body** (an unknown `sku`, `file_id`, `catalog_variant_id`) is a `400` with `code: resource_missing` and `param` pointing at it. ## Handling errors in code ```js title="Node.js" const res = await fetch("https://swaglists.com/api/v1/orders", { method: "POST", headers, body: JSON.stringify(order) }); const body = await res.json(); if (!res.ok) { const { code, message, param, request_id } = body.error; switch (code) { case "duplicate_external_id": return body.error.existing_id; // already forwarded — treat as success case "resource_missing": if (param?.endsWith(".sku")) return skipUnknownSku(param); // not one of your products break; case "insufficient_funds": return alertFinance(body.error.required_amount, body.error.available_amount); } console.error(`API error ${res.status} ${code} (${param ?? "-"}): ${message} [${request_id}]`); throw new Error(code); } ``` ```python title="Python" res = requests.post("https://swaglists.com/api/v1/orders", json=order, headers=headers) if not res.ok: error = res.json()["error"] if error["code"] == "duplicate_external_id": return error["existing_id"] # already forwarded — treat as success logger.error("API error %s %s (%s): %s [%s]", res.status_code, error["code"], error.get("param"), error["message"], error["request_id"]) raise RuntimeError(error["code"]) ``` ## What to retry - **Retry** network errors, timeouts, `429` (wait `Retry-After` seconds), `409 idempotency_in_progress` (wait 1 second) and `5xx` — with exponential backoff and jitter, and **the same Idempotency-Key**, so a request that actually succeeded is not repeated. See the [retry recipe](https://swaglists.com/docs/idempotency.md#retry-recipe). - **Don't retry** other `4xx` errors unchanged — they will fail again. Fix the request, or surface the problem (log it with `request_id`, alert, or show it in your back office). - **After fixing a `4xx`, send the request with a new Idempotency-Key** — also when the fix happened outside the request, such as adding credits after `402 insufficient_funds` or creating a missing SKU. A request that ran is stored with its key for 24 hours, so a retry with the original key returns the original error again (with `Idempotent-Replayed: true`). Derive the new key from the old one (`order-1042:2`); `external_id` still prevents a duplicate order. ## Validation messages `parameter_invalid` messages name the rule that failed: | Message | Meaning | | --- | --- | | `Missing required parameter: recipient.city.` | Required field absent. | | `Received unknown parameter: items[0].colour.` | Request bodies are strict: unknown fields are rejected (check spelling). | | `Received unknown parameter: updated_since (query parameter).` | Query strings are strict too (only `_`, a cache-buster, is ignored) — see [Pagination](https://swaglists.com/docs/pagination.md). | | `Invalid metadata.note: must not contain NUL characters or invalid (unpaired surrogate) Unicode.` | Text can't contain U+0000 or half of a surrogate pair (a broken emoji, usually from cutting a string by UTF-16 units). Applies to every field, query parameter, path id and the `Idempotency-Key`. | | `Invalid starting_after: must be the id of an object of this list (ids start with "ord_")…` | A cursor from another list. | | `Invalid shipping_method: must be one of: economy, standard, express, priority.` | Wrong enum value. | | `Identify each item with exactly one of product_variant_id, sku, or catalog_variant_id (+ designs).` | Item reference missing or ambiguous. | ## Debugging - Every response has a `Request-Id` header (`req_…`); errors repeat it as `request_id`. - **Developers → Logs** in the dashboard shows recent requests with status, error code, duration and (redacted) bodies — search by request id. - Responses also carry `Api-Version` (the API version that served the request). The complete list of codes, with what each means and how to fix it, is the [error reference](https://swaglists.com/docs/errors.md). --- # Rate limits > Per-key request limits, the RateLimit headers on every response, and how to back off from 429s. Source: https://swaglists.com/docs/rate-limits (markdown: https://swaglists.com/docs/rate-limits.md) Each API key may make **300 requests per minute** by default (your account may have a different limit). The limit is a token bucket: it holds a full minute of requests and refills continuously, so short bursts up to the limit are fine. ## Headers Every authenticated response tells you where you stand: ```http RateLimit-Limit: 300 RateLimit-Remaining: 287 RateLimit-Reset: 3 RateLimit-Policy: "per-minute";q=300;w=60 RateLimit: "per-minute";r=287;t=3 ``` | Header | Meaning | | --- | --- | | `RateLimit-Limit` | Requests per minute for this key. | | `RateLimit-Remaining` | Requests left right now. | | `RateLimit-Reset` | Seconds until the bucket is full again. | | `RateLimit-Policy`, `RateLimit` | The same information in the IETF structured-header format. | ## When you hit the limit The API answers `429` with `code: rate_limited` and a `Retry-After` header (seconds): ```http HTTP/1.1 429 Too Many Requests Retry-After: 2 ``` Wait at least `Retry-After` seconds, then retry the same request (same `Idempotency-Key`). Add exponential backoff with jitter if you keep getting 429s. ```js title="Node.js" async function withRateLimit(doRequest, attempts = 5) { for (let i = 1; ; i++) { const res = await doRequest(); if (res.status !== 429 || i >= attempts) return res; const wait = Number(res.headers.get("retry-after")) || 2 ** i; await new Promise((r) => setTimeout(r, wait * 1000 + Math.random() * 250)); } } ``` ```python title="Python" import random, time def with_rate_limit(do_request, attempts=5): for i in range(1, attempts + 1): res = do_request() if res.status_code != 429 or i == attempts: return res wait = float(res.headers.get("Retry-After") or 2 ** i) time.sleep(wait + random.random() * 0.25) ``` ## Staying under the limit - **Use webhooks instead of polling** for order status changes. - **Page with `limit=100`** to fetch lists in fewer requests. - **Cache the catalog** — it changes rarely; listen to `catalog.price_changed` for price updates. - **Spread bulk jobs** (product imports, backfills) over time, or use separate keys per integration — limits are per key. - Need more? Contact support@swaglists.com with your expected volume. --- # Versioning > How the API is versioned, what counts as a backwards-compatible change, and how to build an integration that keeps working. Source: https://swaglists.com/docs/versioning (markdown: https://swaglists.com/docs/versioning.md) The current API version is **`2026-10-01`**, served under `/api/v1`. Every response carries it in the `Api-Version` header, and every event and webhook payload in its `api_version` field. ## Backwards-compatible changes We add to the API without a new version. Your integration must tolerate: - new endpoints, and new optional request parameters; - new fields in responses and event payloads (and new values in `metadata`-like maps); - new event types (endpoints subscribed to `["*"]` receive them); - new error codes, and new values of fields documented as open-ended (for example `OrderEvent.type`, carrier codes); - new enum values for statuses — handle unknown values gracefully (for example treat an unknown order status as "in progress"); - changes to error messages (branch on `code`, never on `message`); - the order of fields in JSON objects and of keys in maps. ## Breaking changes Removing or renaming a field, changing a type or the meaning of a value, or making an optional parameter required would break integrations. Such changes ship only in a **new dated API version**, announced in the [changelog](https://swaglists.com/docs/changelog.md) and by email well ahead of time, with a migration guide and a transition period. ## Building a resilient integration - Parse responses leniently: ignore unknown fields and unknown event types. - Validate the fields you depend on, and log (don't crash on) unexpected values. - Pin your SDK or HTTP client code to the fields documented in the [objects reference](https://swaglists.com/docs/objects.md). - Watch the [changelog](https://swaglists.com/docs/changelog.md) — or the `api_version` of incoming events. --- # Shopify > Forward paid Shopify orders automatically — orders/paid webhook, HMAC verification, SKU mapping, POST /orders — and push tracking back to Shopify. Source: https://swaglists.com/docs/shopify (markdown: https://swaglists.com/docs/shopify.md) This recipe connects a Shopify store with a small server you run (any Node.js host). When a customer pays, Shopify calls your server; your server forwards the order to us; when we ship, we call your server and it marks the order fulfilled in Shopify with tracking. ```text Shopify ── orders/paid webhook ──► your server ── POST /orders ─────────► Swaglists Shopify ◄── fulfillmentCreate ──── your server ◄── shipment.created webhook ──┘ ``` ## Before you start 1. **Products in Shopify with our SKUs.** Export your products as a Shopify CSV (**Products → Export → Shopify CSV** in our dashboard) and import it in Shopify — the variant SKUs are ours. Already have Shopify products? Set their variant SKUs to your SKUs here (see [Products & SKUs](https://swaglists.com/docs/products.md#skus)). Line items whose SKU is not one of your products are skipped by the code below. 2. **A Shopify app** for your store. Create it in the Shopify **Dev Dashboard** (or with the Shopify CLI), install it on your store, and give it these access scopes: `read_orders`, `read_merchant_managed_fulfillment_orders`, `write_merchant_managed_fulfillment_orders`. Note its **client id** and **client secret** — the secret also signs Shopify's webhooks. 3. **Our keys and webhook endpoint.** A live key (`sk_live_…`), a test key for Shopify test orders (`sk_test_…`), and a webhook endpoint pointing at your server's `/webhooks/print` URL subscribed to `shipment.created` (create one in each mode; the secrets differ). ## Subscribe to Shopify webhooks In your app's `shopify.app.toml`, then `shopify app deploy`: ```toml title="shopify.app.toml" [webhooks] api_version = "2026-10" [[webhooks.subscriptions]] topics = ["orders/paid", "orders/cancelled"] uri = "https://your-server.example.com/webhooks/shopify" ``` ## The server One file, Node.js 20+ (`npm install express standardwebhooks`): ```js title="server.mjs" import crypto from "node:crypto"; import express from "express"; import { Webhook } from "standardwebhooks"; const { SHOPIFY_SHOP, // "your-store.myshopify.com" SHOPIFY_CLIENT_ID, SHOPIFY_CLIENT_SECRET, // also the HMAC key of Shopify webhooks PRINT_LIVE_KEY, // sk_live_… PRINT_TEST_KEY, // sk_test_… (Shopify test orders go to test mode) PRINT_LIVE_WEBHOOK_SECRET, // whsec_… of your live endpoint PRINT_TEST_WEBHOOK_SECRET, // whsec_… of your test endpoint } = process.env; const API = "https://swaglists.com/api/v1"; const SHIPPING = { Standard: "standard", Express: "express", Priority: "priority", Economy: "economy" }; // your Shopify rate names → our methods const CARRIERS = { usps: "USPS", ups: "UPS", fedex: "FedEx", dhl: "DHL Express", canada_post: "Canada Post", royal_mail: "Royal Mail", australia_post: "Australia Post" }; const app = express(); /* ---------- helpers ---------- */ // Shopify ids exceed JavaScript's safe integers: always take them from the string GraphQL id. const gidTail = (gid) => String(gid).split("/").pop(); function shopifyHmacValid(rawBody, header) { if (!header) return false; const expected = crypto.createHmac("sha256", SHOPIFY_CLIENT_SECRET).update(rawBody).digest(); const received = Buffer.from(header, "base64"); return received.length === expected.length && crypto.timingSafeEqual(received, expected); } async function printApi(key, method, path, body, idempotencyKey) { const res = await fetch(API + path, { method, headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}), }, body: body === undefined ? undefined : JSON.stringify(body), }); const json = await res.json(); if (!res.ok) throw Object.assign(new Error(json.error.message), { status: res.status, error: json.error }); return json; } let token = { value: null, expiresAt: 0 }; async function shopifyToken() { // Client credentials grant (app and store in the same Shopify organization); tokens last 24 hours. if (token.value && Date.now() < token.expiresAt) return token.value; const res = await fetch(`https://${SHOPIFY_SHOP}/admin/oauth/access_token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: SHOPIFY_CLIENT_ID, client_secret: SHOPIFY_CLIENT_SECRET }), }); const json = await res.json(); token = { value: json.access_token, expiresAt: Date.now() + (json.expires_in - 300) * 1000 }; return token.value; } async function shopifyGraphql(query, variables) { const res = await fetch(`https://${SHOPIFY_SHOP}/admin/api/2026-10/graphql.json`, { method: "POST", headers: { "X-Shopify-Access-Token": await shopifyToken(), "Content-Type": "application/json" }, body: JSON.stringify({ query, variables }), }); const json = await res.json(); if (json.errors) throw new Error(JSON.stringify(json.errors)); return json.data; } /* ---------- 1. Shopify → us: forward paid orders, cancel canceled ones ---------- */ app.post("/webhooks/shopify", express.raw({ type: "*/*" }), (req, res) => { if (!shopifyHmacValid(req.body, req.get("x-shopify-hmac-sha256"))) return res.sendStatus(401); res.sendStatus(200); // Shopify waits at most 5 s — acknowledge, then work (use a real queue in production) const topic = req.get("x-shopify-topic"); const order = JSON.parse(req.body.toString("utf8")); handleShopifyOrder(topic, order).catch((err) => console.error(`Shopify ${topic} ${gidTail(order.admin_graphql_api_id)} failed:`, err.error ?? err)); }); async function handleShopifyOrder(topic, order) { const orderId = gidTail(order.admin_graphql_api_id); const key = order.test ? PRINT_TEST_KEY : PRINT_LIVE_KEY; const externalId = `shopify:${orderId}`; if (topic === "orders/cancelled") { try { await printApi(key, "POST", `/orders/ext:${externalId}/cancel`, { reason: order.cancel_reason || "Canceled in Shopify" }, `cancel-${externalId}`); } catch (err) { if (err.status !== 404) throw err; // 404: never forwarded, nothing to cancel } return; } if (topic !== "orders/paid" || order.cancelled_at) return; const address = order.shipping_address; // null for orders without shipping let items = order.line_items .filter((li) => li.requires_shipping && li.sku && li.fulfillable_quantity > 0) .map((li) => ({ sku: li.sku, quantity: li.fulfillable_quantity, external_line_id: gidTail(li.admin_graphql_api_id) })); if (!address) return; const body = (lineItems) => ({ external_id: externalId, recipient: { name: address.name || `${address.first_name ?? ""} ${address.last_name ?? ""}`.trim(), company: address.company || null, address1: address.address1, address2: address.address2 || null, city: address.city, state_code: address.province_code || null, postal_code: address.zip || null, country_code: address.country_code, phone: address.phone || order.phone || null, email: order.email || order.contact_email || null, }, shipping_method: SHIPPING[order.shipping_lines?.[0]?.title] ?? "standard", items: lineItems, packing_slip: { customer_order_number: order.name }, // "#1001" metadata: { source: "shopify", shopify_order_id: orderId }, }); // Line items that are not our products (other SKUs) are dropped and the order is retried without them. for (let attempt = 1; items.length > 0; attempt++) { try { const created = await printApi(key, "POST", "/orders", body(items), `shopify-order-${orderId}-${attempt}`); console.log(`Shopify order ${order.name} → ${created.id} (${created.status})`); return; } catch (err) { const e = err.error ?? {}; if (e.code === "duplicate_external_id") return; // already forwarded const unknown = e.code === "resource_missing" && /^items\[(\d+)\]\.sku$/.exec(e.param ?? ""); if (!unknown) throw err; items = items.filter((_, i) => i !== Number(unknown[1])); } } } /* ---------- 2. Us → Shopify: push tracking when a parcel ships ---------- */ const verifiers = { live: new Webhook(PRINT_LIVE_WEBHOOK_SECRET), test: new Webhook(PRINT_TEST_WEBHOOK_SECRET) }; app.post("/webhooks/print", express.raw({ type: "*/*" }), (req, res) => { const raw = req.body.toString("utf8"); let event; for (const wh of Object.values(verifiers)) { try { event = wh.verify(raw, req.headers); break; } catch { /* try the other mode's secret */ } } if (!event) return res.sendStatus(400); res.sendStatus(204); if (event.type === "shipment.created") { pushTracking(event.data.object, event.livemode).catch((err) => console.error(`tracking for ${event.data.object.id} failed:`, err)); } }); async function pushTracking(shipment, livemode) { const order = await printApi(livemode ? PRINT_LIVE_KEY : PRINT_TEST_KEY, "GET", `/orders/${shipment.order_id}`); if (!order.external_id?.startsWith("shopify:")) return; // Shopify line item gid → quantity in this parcel const parcel = new Map(); for (const part of shipment.items) { const item = order.items.find((i) => i.id === part.order_item_id); if (item?.external_line_id) parcel.set(`gid://shopify/LineItem/${item.external_line_id}`, part.quantity); } const data = await shopifyGraphql( `query ($id: ID!) { order(id: $id) { fulfillmentOrders(first: 20) { nodes { id status lineItems(first: 100) { nodes { id remainingQuantity lineItem { id } } } } } } }`, { id: `gid://shopify/Order/${order.external_id.slice("shopify:".length)}` }, ); // One call per location: if your store fulfills from several locations, group by location first. const lineItemsByFulfillmentOrder = []; for (const fo of data.order.fulfillmentOrders.nodes) { if (fo.status !== "OPEN" && fo.status !== "IN_PROGRESS") continue; const fulfillmentOrderLineItems = fo.lineItems.nodes .filter((li) => parcel.has(li.lineItem.id) && li.remainingQuantity > 0) .map((li) => ({ id: li.id, quantity: Math.min(li.remainingQuantity, parcel.get(li.lineItem.id)) })); if (fulfillmentOrderLineItems.length) lineItemsByFulfillmentOrder.push({ fulfillmentOrderId: fo.id, fulfillmentOrderLineItems }); } if (!lineItemsByFulfillmentOrder.length) return; // already fulfilled (duplicate delivery) const result = await shopifyGraphql( `mutation ($f: FulfillmentInput!) { fulfillmentCreate(fulfillment: $f) { fulfillment { id status } userErrors { field message } } }`, { f: { notifyCustomer: true, // Shopify emails the customer; turn off our shipment emails in that case trackingInfo: { company: CARRIERS[shipment.carrier] ?? shipment.carrier, number: shipment.tracking_number, url: shipment.tracking_url }, lineItemsByFulfillmentOrder, }, }, ); if (result.fulfillmentCreate.userErrors.length) throw new Error(JSON.stringify(result.fulfillmentCreate.userErrors)); } app.listen(process.env.PORT ?? 3000); ``` ## Field mapping | Our field | Shopify `orders/paid` payload | | --- | --- | | `external_id` | `shopify:` + the numeric tail of `admin_graphql_api_id` | | `recipient.name` | `shipping_address.name` (or `first_name` + `last_name`) | | `recipient.company`, `address1`, `address2`, `city` | `shipping_address.company`, `address1`, `address2`, `city` | | `recipient.state_code` | `shipping_address.province_code` | | `recipient.postal_code` | `shipping_address.zip` | | `recipient.country_code` | `shipping_address.country_code` | | `recipient.phone` | `shipping_address.phone` or `phone` | | `recipient.email` | `email` or `contact_email` | | `items[].sku` | `line_items[].sku` | | `items[].quantity` | `line_items[].fulfillable_quantity` (reflects edits and removals) | | `items[].external_line_id` | numeric tail of `line_items[].admin_graphql_api_id` | | `shipping_method` | map `shipping_lines[0].title` (your rate names) | | `packing_slip.customer_order_number` | `name` (e.g. `#1001`) | ## Things to know - **Respond fast.** Shopify gives your endpoint 5 seconds and retries 8 times over 4 hours. Acknowledge, then process — in production through a queue (BullMQ, SQS, a database table) so a crash never loses an order. Deduplicate on the `X-Shopify-Webhook-Id` header. - **Duplicates are harmless.** `external_id` (`shopify:`) is unique, so a second delivery returns `409 duplicate_external_id`, which the code treats as done. - **Test orders.** Orders paid with Shopify's test gateway have `test: true`; the code sends them to test mode. - **Customer data.** If `shipping_address` arrives without a name or address, your app lacks access to protected customer data — enable it for your app in the Dev Dashboard. - **Personalization.** Customer inputs from product page fields arrive in `line_items[].properties`; render the artwork from them and send the item with [inline designs](https://swaglists.com/docs/personalized-orders.md) instead of a SKU. - **Shipping emails.** With `notifyCustomer: true` Shopify sends the shipping confirmation; turn off **Email customers when their order ships** in our Settings → Orders to avoid two emails. - **Order edits** after payment are not forwarded by this code; edit or cancel the order here (before production) if needed. --- # WooCommerce > Forward WooCommerce orders when they are paid — a drop-in PHP plugin on woocommerce_order_status_processing, or a REST webhook to your own service — and push tracking back. Source: https://swaglists.com/docs/woocommerce (markdown: https://swaglists.com/docs/woocommerce.md) Two ways to connect WooCommerce: | | A. PHP plugin (recommended) | B. WooCommerce webhook | | --- | --- | --- | | Runs | Inside WordPress | On your own server | | Trigger | `woocommerce_order_status_processing` hook | `order.updated` webhook, filtered to `processing` | | Tracking back | Same plugin (REST route) | Your server calls the WooCommerce REST API | Paid WooCommerce orders move to **Processing** — that is when we forward them. `pending` (unpaid) and `on-hold` (awaiting bank transfer) orders are ignored until they are paid. **Before you start:** your WooCommerce products need our SKUs (export a **WooCommerce CSV** from **Products → Export** in our dashboard and import it, or set your variation SKUs to your SKUs here). Create a live key and a webhook endpoint subscribed to `shipment.created` and `order.shipped` pointing at `https://your-store.example.com/wp-json/print/v1/webhook`. ## A. PHP plugin Save as `wp-content/mu-plugins/print-fulfillment.php` and add the constants to `wp-config.php`: ```php title="wp-config.php" define('PRINT_API_KEY', 'sk_live_…'); // sk_test_… on staging define('PRINT_WEBHOOK_SECRET', 'whsec_…'); // secret of your webhook endpoint ``` ```php title="print-fulfillment.php" get_meta('_print_order_id')) return; // already forwarded $items = []; foreach ($order->get_items() as $item_id => $item) { $product = $item->get_product(); $sku = $product ? $product->get_sku() : ''; if ($sku === '' || ($product && $product->is_virtual())) continue; // not a print product $items[] = ['sku' => $sku, 'quantity' => (int) $item->get_quantity(), 'external_line_id' => (string) $item_id]; } if (!$items) return; $has_shipping = $order->get_shipping_address_1() !== ''; $get = fn($field) => $has_shipping ? $order->{"get_shipping_$field"}() : $order->{"get_billing_$field"}(); $body = [ 'external_id' => 'woo:' . wp_parse_url(home_url(), PHP_URL_HOST) . ':' . $order_id, 'recipient' => [ 'name' => trim($get('first_name') . ' ' . $get('last_name')), 'company' => $get('company') ?: null, 'address1' => $get('address_1'), 'address2' => $get('address_2') ?: null, 'city' => $get('city'), 'state_code' => $get('state') ?: null, 'postal_code' => $get('postcode') ?: null, 'country_code' => $get('country'), 'phone' => ($has_shipping ? $order->get_shipping_phone() : '') ?: $order->get_billing_phone() ?: null, 'email' => $order->get_billing_email() ?: null, ], 'shipping_method' => 'standard', // map $order->get_shipping_methods() to economy | standard | express | priority 'items' => $items, 'packing_slip' => ['customer_order_number' => '#' . $order->get_order_number()], 'metadata' => ['source' => 'woocommerce'], ]; $json_body = wp_json_encode($body); $response = wp_remote_post(PRINT_API . '/orders', [ 'timeout' => 20, 'headers' => [ 'Authorization' => 'Bearer ' . PRINT_API_KEY, 'Content-Type' => 'application/json', // one key per distinct request: a retry replays the first response, a corrected order is a new request 'Idempotency-Key' => 'woo-' . hash('sha256', $json_body), ], 'body' => $json_body, ]); $status = is_wp_error($response) ? 0 : wp_remote_retrieve_response_code($response); $json = is_wp_error($response) ? null : json_decode(wp_remote_retrieve_body($response), true); if ($status === 201 || ($json['error']['code'] ?? '') === 'duplicate_external_id') { $print_id = $json['id'] ?? $json['error']['existing_id']; $order->update_meta_data('_print_order_id', $print_id); $order->add_order_note('Sent to production: ' . $print_id); $order->save(); return; } if ($status === 0 || $status === 429 || $status >= 500 || ($json['error']['code'] ?? '') === 'idempotency_in_progress') { // temporary problem: retry in 5 minutes as_schedule_single_action(time() + 300, 'print_forward_order', [$order_id], 'print'); return; } $order->add_order_note(sprintf('Could not send to production (%s): %s', $json['error']['code'] ?? $status, $json['error']['message'] ?? '')); } add_action('woocommerce_order_status_processing', 'print_forward_order'); add_action('print_forward_order', 'print_forward_order'); /** Cancel in production when the store order is canceled (only possible before production starts). */ add_action('woocommerce_order_status_cancelled', function ($order_id) { $order = wc_get_order($order_id); $print_id = $order ? $order->get_meta('_print_order_id') : ''; if (!$print_id) return; $response = wp_remote_post(PRINT_API . "/orders/$print_id/cancel", [ 'timeout' => 20, 'headers' => ['Authorization' => 'Bearer ' . PRINT_API_KEY, 'Content-Type' => 'application/json'], 'body' => wp_json_encode(['reason' => 'Canceled in WooCommerce']), ]); $code = wp_remote_retrieve_response_code($response); $order->add_order_note($code === 200 ? 'Production order canceled.' : 'Production order could not be canceled (already in production).'); }); /** Our webhooks: add tracking and complete the order when a parcel ships. */ add_action('rest_api_init', function () { register_rest_route('print/v1', '/webhook', [ 'methods' => 'POST', 'permission_callback' => '__return_true', // authenticated by the signature below 'callback' => function (WP_REST_Request $request) { $raw = $request->get_body(); if (!print_verify_webhook($raw, $request)) return new WP_REST_Response('invalid signature', 400); $event = json_decode($raw, true); $object = $event['data']['object']; $print_order_id = $event['type'] === 'shipment.created' ? $object['order_id'] : $object['id']; $orders = wc_get_orders(['meta_key' => '_print_order_id', 'meta_value' => $print_order_id, 'limit' => 1]); if (!$orders) return new WP_REST_Response(null, 204); $order = $orders[0]; if ($event['type'] === 'shipment.created') { // one note per parcel; true = also email it to the customer $order->add_order_note(sprintf('Shipped with %s: %s %s', strtoupper((string) $object['carrier']), $object['tracking_number'], $object['tracking_url']), true); } elseif ($event['type'] === 'order.shipped') { $order->update_status('completed'); // every parcel has shipped } return new WP_REST_Response(null, 204); }, ]); }); function print_verify_webhook(string $raw, WP_REST_Request $request, int $tolerance = 300): bool { $id = (string) $request->get_header('webhook-id'); $ts = (string) $request->get_header('webhook-timestamp'); $signatures = (string) $request->get_header('webhook-signature'); if ($id === '' || !ctype_digit($ts) || $signatures === '' || abs(time() - (int) $ts) > $tolerance) return false; $key = base64_decode(substr(PRINT_WEBHOOK_SECRET, strlen('whsec_')), true); $expected = base64_encode(hash_hmac('sha256', "$id.$ts.$raw", $key, true)); foreach (explode(' ', $signatures) as $entry) { [$version, $sig] = array_pad(explode(',', $entry, 2), 2, ''); if ($version === 'v1' && hash_equals($expected, $sig)) return true; } return false; } ``` Notes: - `as_schedule_single_action` is Action Scheduler, which ships with WooCommerce. - `external_id` is derived from the order id, so a hook that fires twice never creates two orders. The idempotency key is a hash of the request body: an Action Scheduler retry of the same request replays the first response, while an order you corrected after a failed forward (for example a fixed SKU, then set to Processing again) is sent as a new request instead of failing with `409 idempotency_key_reused`. - States: WooCommerce stores ISO state codes for countries that have them (US, CA, AU, …), which is what `state_code` expects. - If customers choose a faster shipping method, map `$order->get_shipping_methods()` (method ids/titles) to `express` or `priority`. ## B. WooCommerce webhook to your server Prefer to keep WordPress untouched? Create a webhook in **WooCommerce → Settings → Advanced → Webhooks**: topic **Order updated**, delivery URL `https://glue.example.com/webhooks/woo`, a long random **secret** (avoid `&`, `<`, `>` and quotes). Then forward paid orders from your server: ```php title="webhooks/woo.php" "woo:$host:{$o['id']}", 'recipient' => [ 'name' => trim($ship['first_name'] . ' ' . $ship['last_name']), 'company' => $ship['company'] ?: null, 'address1' => $ship['address_1'], 'address2' => $ship['address_2'] ?: null, 'city' => $ship['city'], 'state_code' => $ship['state'] ?: null, 'postal_code' => $ship['postcode'] ?: null, 'country_code' => $ship['country'], 'phone' => ($o['shipping']['phone'] ?? '') ?: ($o['billing']['phone'] ?: null), 'email' => $o['billing']['email'] ?: null, ], 'items' => array_values(array_map( fn($li) => ['sku' => $li['sku'], 'quantity' => $li['quantity'], 'external_line_id' => (string) $li['id']], array_filter($o['line_items'], fn($li) => !empty($li['sku'])) )), 'shipping_method' => 'standard', 'packing_slip' => ['customer_order_number' => '#' . $o['number']], ]; $body = json_encode($payload); $ch = curl_init('https://swaglists.com/api/v1/orders'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('PRINT_API_KEY'), 'Content-Type: application/json', // one key per distinct payload (it contains external_id) — never one fixed key per order 'Idempotency-Key: woo-' . hash('sha256', $body), ], CURLOPT_POSTFIELDS => $body, ]); $response = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); $code = $response['error']['code'] ?? ''; // 201 created, or 409 duplicate_external_id: already forwarded (a later update of the order) — both are fine. if ($status === 201 || $code === 'duplicate_external_id') { http_response_code(200); exit; } error_log(sprintf('Order %s not forwarded: HTTP %d %s %s', $payload['external_id'], $status, $code, $response['error']['message'] ?? '')); http_response_code(500); ``` `order.updated` fires many times per order, and the key is a hash of the payload, so every repeat is harmless: - The same payload again replays the first response (`Idempotent-Replayed: true`). - A later update of an order that was already forwarded (for example a new phone number) has a new key and is answered with `409 duplicate_external_id` thanks to `external_id` — treated as done. - An order you corrected after a failed forward (for example a fixed SKU) is sent as a new request and created. Don't derive the key from the order id alone here: the next edit of the order would reuse it with a different body and get `409 idempotency_key_reused`, and a corrected order would be refused for 24 hours. WooCommerce does not retry failed deliveries (the next update of the order is the next attempt) and disables a webhook after more than 5 consecutive failures — check its status if orders stop arriving. For automatic retries, use the plugin (A). To push tracking back from your server, handle our `shipment.created` webhook ([verify it](https://swaglists.com/docs/webhooks.md#verify-signatures)), find the WooCommerce order id in the order's `external_id`, then: ```bash title="Add a customer note and complete the order" curl -X POST https://your-store.example.com/wp-json/wc/v3/orders/1042/notes -u ck_xxx:cs_xxx \ -H "Content-Type: application/json" \ -d '{ "note": "Shipped with USPS: 9400111899223197428490 https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "customer_note": true }' curl -X PUT https://your-store.example.com/wp-json/wc/v3/orders/1042 -u ck_xxx:cs_xxx \ -H "Content-Type: application/json" -d '{ "status": "completed" }' ``` ## Field mapping | Our field | WooCommerce order | | --- | --- | | `external_id` | `woo::` | | `recipient.*` | `shipping.*` (fall back to `billing.*` when the order has no shipping address) | | `recipient.state_code` / `postal_code` / `country_code` | `state` / `postcode` / `country` | | `recipient.phone` | `shipping.phone`, else `billing.phone` | | `recipient.email` | `billing.email` | | `items[].sku`, `quantity`, `external_line_id` | `line_items[].sku`, `quantity`, `id` | | `packing_slip.customer_order_number` | `#` + `number` | --- # BigCommerce > Forward BigCommerce orders when they reach Awaiting Fulfillment — status webhook, fetch the order, one production order per shipping address — and create shipments with tracking. Source: https://swaglists.com/docs/bigcommerce (markdown: https://swaglists.com/docs/bigcommerce.md) BigCommerce webhooks are thin (they carry only the order id) and unsigned, so the recipe is: authenticate the webhook with a secret header you choose, fetch the order from the BigCommerce API, and forward it. ```text BigCommerce ── store/order/statusUpdated ──► your server ── GET order, products, addresses (BigCommerce API) └─ POST /orders (one per shipping address) ──► Swaglists BigCommerce ◄── POST /v2/orders/{id}/shipments ── your server ◄── shipment.created webhook ``` **Before you start:** your BigCommerce variants need our SKUs (see [Products & SKUs](https://swaglists.com/docs/products.md#skus)). Create a BigCommerce API account (**Settings → API → Store-level API accounts**) with **Orders: modify**, and note the store hash and access token. ## Register the webhook Choose a long random secret; BigCommerce sends your custom headers with every callback: ```bash title="cURL" curl -X POST https://api.bigcommerce.com/stores/$STORE_HASH/v3/hooks \ -H "X-Auth-Token: $BC_TOKEN" -H "Content-Type: application/json" -H "Accept: application/json" \ -d '{ "scope": "store/order/statusUpdated", "destination": "https://your-server.example.com/webhooks/bigcommerce", "is_active": true, "headers": { "X-Glue-Secret": "a-long-random-secret" } }' ``` The destination must be HTTPS on port 443. ## The server ```js title="bigcommerce.mjs" // Node.js 20+, npm install express import crypto from "node:crypto"; import express from "express"; const { STORE_HASH, BC_TOKEN, GLUE_SECRET, PRINT_API_KEY } = process.env; const BC = `https://api.bigcommerce.com/stores/${STORE_HASH}`; const API = "https://swaglists.com/api/v1"; const AWAITING_FULFILLMENT = 11; // paid and ready — status ids are stable even if you rename statuses const CANCELLED = 5; const bc = async (path) => { const res = await fetch(BC + path, { headers: { "X-Auth-Token": BC_TOKEN, Accept: "application/json" } }); if (!res.ok) throw new Error(`BigCommerce ${path}: ${res.status}`); return res.json(); }; const safeEqual = (a = "", b = "") => { const x = Buffer.from(a), y = Buffer.from(b); return x.length === y.length && crypto.timingSafeEqual(x, y); }; async function printApi(method, path, body, idempotencyKey) { const res = await fetch(API + path, { method, headers: { Authorization: `Bearer ${PRINT_API_KEY}`, "Content-Type": "application/json", ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}) }, body: body === undefined ? undefined : JSON.stringify(body), }); const json = await res.json(); if (!res.ok && json.error?.code !== "duplicate_external_id") throw Object.assign(new Error(json.error.message), { status: res.status, error: json.error }); return json; } const app = express(); app.use(express.json()); app.post("/webhooks/bigcommerce", (req, res) => { if (!safeEqual(req.get("x-glue-secret"), GLUE_SECRET)) return res.sendStatus(401); res.sendStatus(200); // respond fast; BigCommerce retries failures for ~48 hours const { id, status } = req.body.data ?? {}; handle(id, status?.new_status_id).catch((err) => console.error(`BigCommerce order ${id}:`, err.error ?? err)); }); async function handle(orderId, newStatus) { if (newStatus === CANCELLED) { const { data } = await printApi("GET", `/orders?q=bc:${STORE_HASH}:${orderId}:&limit=10`); for (const o of data) await printApi("POST", `/orders/${o.id}/cancel`, { reason: "Canceled in BigCommerce" }).catch(() => {}); return; } if (newStatus !== AWAITING_FULFILLMENT) return; const [order, products, addresses] = await Promise.all([ bc(`/v2/orders/${orderId}`), bc(`/v2/orders/${orderId}/products`), bc(`/v2/orders/${orderId}/shipping_addresses`), ]); // One production order per shipping address (BigCommerce orders can ship to several addresses). for (const address of addresses) { const items = products .filter((p) => p.order_address_id === address.id && p.type === "physical" && p.sku) .map((p) => ({ sku: p.sku, quantity: p.quantity, external_line_id: String(p.id) })); if (!items.length) continue; const externalId = `bc:${STORE_HASH}:${orderId}:${address.id}`; await printApi( "POST", "/orders", { external_id: externalId, recipient: { name: `${address.first_name} ${address.last_name}`.trim(), company: address.company || null, address1: address.street_1, address2: address.street_2 || null, city: address.city, state_code: address.state || null, // the full name — US, Canadian and Australian names are converted to ISO codes for you postal_code: address.zip || null, country_code: address.country_iso2, phone: address.phone || null, email: address.email || order.billing_address?.email || null, }, shipping_method: /express/i.test(address.shipping_method ?? "") ? "express" : "standard", items, packing_slip: { customer_order_number: `#${orderId}` }, }, externalId, ); } } app.listen(process.env.PORT ?? 3000); ``` ## Push tracking back When our `shipment.created` webhook arrives ([verify it](https://swaglists.com/docs/webhooks.md#verify-signatures)), fetch the order (`GET /orders/{order_id}`) and parse its `external_id` (`bc:::
`). Then create the shipment in BigCommerce — `external_line_id` holds the BigCommerce order product id: ```js await fetch(`${BC}/v2/orders/${bcOrderId}/shipments`, { method: "POST", headers: { "X-Auth-Token": BC_TOKEN, "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ order_address_id: Number(bcAddressId), tracking_number: shipment.tracking_number, tracking_link: shipment.tracking_url, shipping_provider: "", // or one of BigCommerce's providers (usps, ups, fedex, …) items: shipment.items.map((part) => ({ order_product_id: Number(order.items.find((i) => i.id === part.order_item_id).external_line_id), quantity: part.quantity, })), }), }); ``` ## Field mapping | Our field | BigCommerce | | --- | --- | | trigger | `store/order/statusUpdated` with `data.status.new_status_id == 11` (Awaiting Fulfillment) | | cancel trigger | `new_status_id == 5` (Cancelled) | | `external_id` | `bc:::` | | `recipient.name` | `first_name` + `last_name` of the shipping address | | `address1` / `address2` | `street_1` / `street_2` | | `state_code` | `state` (a full name such as `New South Wales` — sent as is; we convert US, Canadian and Australian names to the ISO code) | | `postal_code` / `country_code` | `zip` / `country_iso2` | | `items[].sku` / `quantity` / `external_line_id` | order products `sku` / `quantity` / `id`, filtered by `order_address_id` | ## Things to know - **No signatures** on BigCommerce webhooks: the secret header is the authentication — compare it in constant time and keep it secret. - **Thin payloads**: always re-fetch the order; never trust state from the webhook body alone. - **Statuses are renameable** in BigCommerce — match on ids, not names. - **New hooks** can take about a minute to activate. Hooks are deactivated after repeated failures — check `GET /v3/hooks`. --- # Custom checkout > Integrate any store or app — quote shipping at checkout, create the order after payment, and keep your order status and tracking in sync with webhooks. Source: https://swaglists.com/docs/custom-checkout (markdown: https://swaglists.com/docs/custom-checkout.md) Built your own store, app or checkout? The integration has three touch points, all from your **backend** (your secret key must never reach the browser): 1. **At checkout** — show shipping options and prices: `POST /shipping/rates` (or `POST /orders/estimate` for the full total). 2. **After payment succeeds** — create the order: `POST /orders`. 3. **Afterwards** — receive [webhooks](https://swaglists.com/docs/webhooks.md) and update your order, email your customer, show tracking. ## 1. Shipping options at checkout ```js title="Node.js" // GET /api/shipping-options?country=US&state=CA&zip=94107 — your endpoint, called by your checkout page export async function shippingOptions(cart, address) { const res = await fetch("https://swaglists.com/api/v1/shipping/rates", { method: "POST", headers: { Authorization: `Bearer ${process.env.PRINT_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ recipient: { country_code: address.country, state_code: address.state, postal_code: address.zip }, items: cart.lines.map((l) => ({ sku: l.sku, quantity: l.quantity })), }), }); const { data } = await res.json(); // Add your own margin/handling if you want; data[].price is what you pay us. return data.map((r) => ({ id: r.method, label: r.name, cost: r.price, days: r.estimated_delivery_days })); } ``` ```python title="Python" def shipping_options(cart, address): res = requests.post( "https://swaglists.com/api/v1/shipping/rates", headers={"Authorization": f"Bearer {os.environ['PRINT_API_KEY']}"}, json={ "recipient": {"country_code": address["country"], "state_code": address.get("state"), "postal_code": address.get("zip")}, "items": [{"sku": l["sku"], "quantity": l["quantity"]} for l in cart["lines"]], }, ) res.raise_for_status() return [{"id": r["method"], "label": r["name"], "cost": r["price"], "days": r["estimated_delivery_days"]} for r in res.json()["data"]] ``` ```php title="PHP" function shipping_options(array $cart, array $address): array { $ch = curl_init('https://swaglists.com/api/v1/shipping/rates'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('PRINT_API_KEY'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'recipient' => ['country_code' => $address['country'], 'state_code' => $address['state'] ?? null, 'postal_code' => $address['zip'] ?? null], 'items' => array_map(fn($l) => ['sku' => $l['sku'], 'quantity' => $l['quantity']], $cart['lines']), ]), ]); $rates = json_decode(curl_exec($ch), true)['data'] ?? []; return array_map(fn($r) => ['id' => $r['method'], 'label' => $r['name'], 'cost' => $r['price']], $rates); } ``` ## 2. Create the order after payment Call `POST /orders` from the place where you learn that payment succeeded — for example your payment provider's "payment succeeded" webhook handler. Use your order id for both `external_id` and the `Idempotency-Key`, so retries and duplicate payment webhooks never create two orders. ```js title="Node.js" export async function forwardOrder(localOrder) { const res = await fetch("https://swaglists.com/api/v1/orders", { method: "POST", headers: { Authorization: `Bearer ${process.env.PRINT_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `order-${localOrder.id}`, }, body: JSON.stringify({ external_id: String(localOrder.id), recipient: { name: localOrder.shipping.name, address1: localOrder.shipping.line1, address2: localOrder.shipping.line2 || null, city: localOrder.shipping.city, state_code: localOrder.shipping.state || null, postal_code: localOrder.shipping.postalCode || null, country_code: localOrder.shipping.country, email: localOrder.email || null, }, shipping_method: localOrder.shippingOptionId, // "standard", "express", … from step 1 // ids and order numbers are strings in the API — convert numeric ones items: localOrder.lines.map((l) => ({ sku: l.sku, quantity: l.quantity, external_line_id: String(l.id) })), packing_slip: { customer_order_number: `#${localOrder.number}` }, }), }); const body = await res.json(); if (res.ok) return body.id; if (body.error.code === "duplicate_external_id") return body.error.existing_id; // already forwarded throw new Error(`${body.error.code}: ${body.error.message} (${body.error.request_id})`); } ``` ```python title="Python" def forward_order(local_order): res = requests.post( "https://swaglists.com/api/v1/orders", headers={"Authorization": f"Bearer {os.environ['PRINT_API_KEY']}", "Idempotency-Key": f"order-{local_order['id']}"}, json={ "external_id": str(local_order["id"]), "recipient": local_order["shipping"], # name, address1, address2, city, state_code, postal_code, country_code, email "shipping_method": local_order["shipping_option_id"], "items": [{"sku": l["sku"], "quantity": l["quantity"], "external_line_id": str(l["id"])} for l in local_order["lines"]], "packing_slip": {"customer_order_number": f"#{local_order['number']}"}, }, timeout=30, ) body = res.json() if res.ok: return body["id"] if body["error"]["code"] == "duplicate_external_id": return body["error"]["existing_id"] raise RuntimeError(f"{body['error']['code']}: {body['error']['message']} ({body['error']['request_id']})") ``` ```php title="PHP" function forward_order(array $local): string { $ch = curl_init('https://swaglists.com/api/v1/orders'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('PRINT_API_KEY'), 'Content-Type: application/json', 'Idempotency-Key: order-' . $local['id'], ], CURLOPT_POSTFIELDS => json_encode([ 'external_id' => (string) $local['id'], 'recipient' => $local['shipping'], // name, address1, city, state_code, postal_code, country_code, email 'shipping_method' => $local['shipping_option_id'], 'items' => array_map(fn($l) => ['sku' => $l['sku'], 'quantity' => (int) $l['quantity'], 'external_line_id' => (string) $l['id']], $local['lines']), 'packing_slip' => ['customer_order_number' => '#' . $local['number']], ]), ]); $body = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($status === 201) return $body['id']; if (($body['error']['code'] ?? '') === 'duplicate_external_id') return $body['error']['existing_id']; throw new RuntimeException(($body['error']['code'] ?? $status) . ': ' . ($body['error']['message'] ?? '')); } ``` Store our order id (`ord_…`) on your order. If the call fails with a network error, 429 or 5xx, retry it later with the same key (a job queue is ideal) — see the [retry recipe](https://swaglists.com/docs/idempotency.md#retry-recipe). ## 3. Keep your order in sync Subscribe a [webhook endpoint](https://swaglists.com/docs/webhooks.md) to the events you care about and update your order from `data.object`: | Event | Typical action | | --- | --- | | `order.in_production` | Show "In production"; disable cancellation in your UI. | | `shipment.created` | Save carrier, `tracking_number`, `tracking_url`; email your customer (or enable our [shipment emails](https://swaglists.com/docs/shipping.md#customer-emails)). | | `order.delivered` | Mark delivered. | | `order.on_hold` | Flag for your team; `insufficient_funds` resolves when you add credits. | | `order.failed`, `order.canceled` | Refund or contact your customer. | ```js title="Map our status to yours" const STATUS = { draft: "pending", on_hold: "processing", processing: "processing", in_production: "processing", partially_shipped: "partially_shipped", shipped: "shipped", delivered: "delivered", canceled: "canceled", failed: "failed", }; const RANK = ["pending", "processing", "partially_shipped", "shipped", "delivered"]; // Webhooks can arrive out of order: never move an order backwards. function nextStatus(current, incoming) { const next = STATUS[incoming] ?? current; if (next === "canceled" || next === "failed") return next; return RANK.indexOf(next) > RANK.indexOf(current) ? next : current; } ``` ## Checklist - [ ] Secret key only on the server; test key in development, live key in production. - [ ] `external_id` + `Idempotency-Key` from your order id. - [ ] Shipping options from `POST /shipping/rates`, mapped to `shipping_method`. - [ ] Webhook endpoint with signature verification and deduplication by event id. - [ ] Our order id stored on your order; `GET /orders/ext:` for lookups. --- # No-code (Zapier, Make) > Connect any app without writing a server — send orders with an HTTP request step in Zapier or Make, and react to order updates safely. Source: https://swaglists.com/docs/no-code (markdown: https://swaglists.com/docs/no-code.md) Automation platforms can call the API with their generic HTTP step. That is enough to forward orders from almost any app (stores, forms, spreadsheets, CRMs). ## Send an order ### Zapier 1. **Trigger:** your app's "new paid order" (or a form submission, a new spreadsheet row, …). 2. **Action:** *Webhooks by Zapier* → **Custom Request**. - **Method:** `POST` - **URL:** `https://swaglists.com/api/v1/orders` - **Data pass-through:** off - **Data:** the JSON body below, with fields from the trigger mapped in. - **Headers:** - `Authorization` → `Bearer sk_live_…` (store it in the step; anyone with access to the Zap can see it) - `Content-Type` → `application/json` - `Idempotency-Key` → the trigger's order id (e.g. `order-` + *Order ID*) 3. **Test** the step with a test key first (`sk_test_…`) and check the order in the dashboard (test mode). ### Make Use **HTTP → Make a request**: URL `https://swaglists.com/api/v1/orders`, method `POST`, headers as above, **Body type** *Raw*, **Content type** *JSON (application/json)*, **Request content** the JSON body below, and **Parse response** on. ### The body ```json title="Request content" { "external_id": "{{order id from the trigger}}", "recipient": { "name": "{{shipping name}}", "address1": "{{shipping address line 1}}", "address2": "{{shipping address line 2}}", "city": "{{shipping city}}", "state_code": "{{shipping state code, e.g. CA}}", "postal_code": "{{shipping zip}}", "country_code": "{{shipping country code, e.g. US}}", "email": "{{customer email}}" }, "shipping_method": "standard", "items": [ { "sku": "{{line item SKU}}", "quantity": {{line item quantity}} } ] } ``` The `{{…}}` parts are the fields you map from the trigger in the editor. Things to get right: - **Country and state must be codes** (`US`, `CA`), not names. Most store triggers offer a "country code" field; otherwise add a lookup/formatter step. - **Empty optional fields** — leave them out, send `null`, or send an empty string: an empty `address2`, `company`, `phone` or `email` (a trigger without a customer email) is fine, and `postal_code` may be empty for countries without postal codes. Two fields must not be empty strings: `state_code` (rejected with `parameter_invalid` — for countries without states, remove that line from the body) and `external_id` (map an id that every order has: it is what prevents duplicate orders). - **Several line items** — Zapier: use *Code by Zapier* (or the line-item fields of your trigger) to build the `items` array; Make: use an *Iterator* + *Array aggregator* (or `map()`) to build it. - **Errors** — a `4xx` response stops the run with the error body; `error.code` and `error.message` tell you what to fix ([error reference](https://swaglists.com/docs/errors.md)). Turn on autoreplay/retries for `429` and `5xx`. Replaying a run after a `4xx` sends the same `Idempotency-Key`, which can return the stored error for 24 hours — even when you fixed the cause outside the request (for example added credits after `402 insufficient_funds`). Change the key for that replay (for example `order-` + *Order ID* + `-2`); `external_id` still prevents a duplicate order. ## React to order updates Create a "catch hook" in Zapier (*Webhooks by Zapier* → **Catch Raw Hook**) or Make (*Webhooks* → **Custom webhook**) and register its URL as a [webhook endpoint](https://swaglists.com/docs/webhooks.md) subscribed to the events you need (e.g. `shipment.created`). > **Warning** No-code webhook steps cannot verify our signatures, so anyone who learns the hook URL could send fake events. Use the incoming event only as a **trigger**: take `data.object.id` (or `data.object.order_id` for shipments) and fetch the real object with an HTTP step — `GET https://swaglists.com/api/v1/orders/` with your key — before acting on it. Keep the hook URL private. Typical follow-ups: update the order in your store with the tracking number, send a Slack message when an order is on hold, add a row to a spreadsheet when an order is delivered. ## When to graduate to code No-code is great for low volume. Move to a small server (see [Custom checkout](https://swaglists.com/docs/custom-checkout.md), [Shopify](https://swaglists.com/docs/shopify.md)) when you need signature verification, reliable retries, multi-item mapping with SKU filtering, or more than a few hundred orders a day. --- # Product catalog export & import > Export your products as Shopify or WooCommerce CSV files, import them into your store, keep SKUs in sync, and bulk-create products from a spreadsheet with the API. Source: https://swaglists.com/docs/catalog-export (markdown: https://swaglists.com/docs/catalog-export.md) Your store needs products whose variant **SKUs match your SKUs here** — that is how orders find their way back to us. The fastest way to get there is to create products here and export them to your store. ## Export to your store In the dashboard open **Products**, optionally select products, then **Export** and pick a format. The download is a CSV ready for your store's importer: | Format | File | Import it in | | --- | --- | --- | | **Shopify CSV** | UTF-8, one row per variant, extra rows for additional images | Shopify admin → **Products → Import** | | **WooCommerce CSV** | UTF-8 with BOM, a `variable` parent row + one `variation` row per variant | WooCommerce → **Products → Import** | | **CSV** | UTF-8 with BOM, one row per variant | Spreadsheets or any other platform | Shopify and WooCommerce files contain enabled variants only (the generic CSV lists every variant with an `enabled` column). Archived products are left out unless you select them. ### Shopify columns | Column | Value | | --- | --- | | `Title`, `URL handle`, `Description` (HTML), `Tags` | From your product | | `Vendor` | Your brand name | | `Type` | The catalog product (e.g. *Classic Unisex Tee*) | | `Status`, `Published on online store` | `active` / `draft` from your product | | `Option1 name` / `Option1 value`, `Option2 …`, `Option3 …` | Variant options (e.g. Color, Size) | | `SKU` | **Your SKU** — do not change it in Shopify | | `Price` | Your retail price | | `Weight value (grams)`, `Requires shipping`, `Charge tax` | Shipping weight, `TRUE`, `TRUE` | | `Inventory tracker` | Empty (print on demand: never out of stock) | | `Fulfillment service` | `manual` (your server forwards the order — see the [Shopify recipe](https://swaglists.com/docs/shopify.md)) | | `Product image URL`, `Variant image URL` | Our rendered mockups | ### WooCommerce columns `Type` (`variable` / `variation` / `simple`), `SKU` (parent SKU on the variable row, **your SKU** on each variation), `Parent`, `Name`, `Published`, `Description`, `Regular price` (your retail price), `Attribute N name` / `value(s)` / `visible` / `global`, `Images` (mockups), `Categories`, `Tags`, `Tax status`, `In stock?` and `Weight (lbs)` or `Weight (kg)` (choose the unit your store uses). ### Generic CSV columns `product_id`, `product_title`, `product_status`, `product_external_id`, `catalog_product_id`, `variant_id`, `sku`, `catalog_variant_id`, `variant_title`, `color`, `size`, `options`, `enabled`, `retail_price`, `currency`, `variant_external_id`, `tags`, `image_url`, `created_at`. ## Keep your store in sync - **New variants or products** — export again and import with "overwrite existing products" (Shopify matches on `URL handle`; WooCommerce on `SKU`). - **Price changes** — wholesale price changes arrive as [`catalog.price_changed`](https://swaglists.com/docs/events.md#catalog.price_changed) events; your retail prices are yours to adjust. - **Changing a SKU** — change it here first (`PATCH /products/{id}/variants/{variant_id}`), then in your store. Until both match, orders for that variant fail with `400 resource_missing` on `items[i].sku`. - **Archiving** — archive the product here (`DELETE /products/{id}`) and unpublish it in your store. ## Import into Swaglists: bulk-create from a spreadsheet Have many designs? Create products with the API from a CSV of artwork URLs and SKUs. Example input: ```csv title="designs.csv" title,artwork_url,catalog_product_id,sku_prefix Sunset Vibes Tee,https://cdn.example.com/art/sunset.png,cp_01k8w1a2s3d4f5g6h7j8k9m0n1,SUNSET Mountain Air Tee,https://cdn.example.com/art/mountain.png,cp_01k8w1a2s3d4f5g6h7j8k9m0n1,MOUNTAIN ``` ```js title="Node.js" // bulk-create.mjs — node bulk-create.mjs designs.csv import { readFile } from "node:fs/promises"; const API = "https://swaglists.com/api/v1"; const headers = { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" }; const [header, ...rows] = (await readFile(process.argv[2], "utf8")).trim().split(/\r?\n/).map((l) => l.split(",")); const col = Object.fromEntries(header.map((h, i) => [h, i])); for (const row of rows) { const catalogProductId = row[col.catalog_product_id]; const catalog = await (await fetch(`${API}/catalog/products/${catalogProductId}`, { headers })).json(); const placement = catalog.placements[0].key; const res = await fetch(`${API}/products`, { method: "POST", headers: { ...headers, "Idempotency-Key": `bulk-${row[col.sku_prefix]}` }, body: JSON.stringify({ catalog_product_id: catalogProductId, title: row[col.title], designs: [{ placement, layers: [{ url: row[col.artwork_url] }] }], // we download the artwork variants: catalog.variants .filter((v) => v.available) .map((v) => ({ catalog_variant_id: v.id, sku: [row[col.sku_prefix], v.color, v.size].filter(Boolean).join("-").toUpperCase().replace(/[^A-Z0-9-]/g, "") })), }), }); const body = await res.json(); console.log(res.ok ? `✓ ${body.id} ${body.title} (${body.variants.length} variants)` : `✗ ${row[col.title]}: ${body.error.code} ${body.error.message}`); for (const w of body.warnings ?? []) console.log(` ⚠ ${w.code}: ${w.message}`); } ``` ```python title="Python" # bulk_create.py — python bulk_create.py designs.csv import csv, os, re, sys, requests API = "https://swaglists.com/api/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['API_KEY']}" with open(sys.argv[1], newline="") as f: for row in csv.DictReader(f): catalog = session.get(f"{API}/catalog/products/{row['catalog_product_id']}").json() variants = [ {"catalog_variant_id": v["id"], "sku": re.sub(r"[^A-Z0-9-]", "", "-".join(filter(None, [row["sku_prefix"], v["color"], v["size"]])).upper())} for v in catalog["variants"] if v["available"] ] res = session.post( f"{API}/products", headers={"Idempotency-Key": f"bulk-{row['sku_prefix']}"}, json={ "catalog_product_id": row["catalog_product_id"], "title": row["title"], "designs": [{"placement": catalog["placements"][0]["key"], "layers": [{"url": row["artwork_url"]}]}], "variants": variants, }, ) body = res.json() print(f"✓ {body['id']} {body['title']}" if res.ok else f"✗ {row['title']}: {body['error']['code']} {body['error']['message']}") ``` Then export the new products to your store as described above. Respect the [rate limit](https://swaglists.com/docs/rate-limits.md) for large batches (the scripts make two requests per product). --- # API reference > Every endpoint of the REST API with parameters, request and response fields, error codes and examples — generated from the OpenAPI document. Source: https://swaglists.com/docs/api-reference (markdown: https://swaglists.com/docs/api-reference.md) Base URL: `https://swaglists.com/api/v1` · Authentication: `Authorization: Bearer sk_test_…` (test) or `sk_live_…` (live) · Version `2026-10-01` · OpenAPI 3.1 document: [https://swaglists.com/api/v1/openapi.json](https://swaglists.com/api/v1/openapi.json) · Interactive version: [https://swaglists.com/docs/api-reference](https://swaglists.com/docs/api-reference). Conventions: JSON request and response bodies with snake_case fields (unknown request fields are rejected); amounts in integer cents with a lowercase `currency`; ISO-8601 UTC timestamps; cursor pagination (`limit`, `starting_after`, `ending_before`); send an `Idempotency-Key` header on every `POST`, `PATCH` and `DELETE`; errors as `{ "error": { "type", "code", "message", "param", "doc_url", "request_id" } }` ([error codes](https://swaglists.com/docs/errors.md)). Every response has `Request-Id` and `Api-Version` headers; authenticated responses carry [rate limit](https://swaglists.com/docs/rate-limits.md) headers. ## Endpoints | Method | Path | Summary | Scope | | --- | --- | --- | --- | | GET | `/account` | [Retrieve your account](#retrieve-account) | `account:read` | | GET | `/catalog/categories` | [List catalog categories](#list-catalog-categories) | `catalog:read` | | GET | `/catalog/products` | [List catalog products](#list-catalog-products) | `catalog:read` | | GET | `/catalog/products/{id_or_slug}` | [Retrieve a catalog product](#retrieve-catalog-product) | `catalog:read` | | GET | `/files` | [List files](#list-files) | `files:read` | | POST | `/files` | [Upload a file](#create-file) | `files:write` | | GET | `/files/{id}` | [Retrieve a file](#retrieve-file) | `files:read` | | DELETE | `/files/{id}` | [Delete a file](#delete-file) | `files:write` | | GET | `/products` | [List products](#list-products) | `products:read` | | POST | `/products` | [Create a product](#create-product) | `products:write` | | GET | `/products/{id}` | [Retrieve a product](#retrieve-product) | `products:read` | | PATCH | `/products/{id}` | [Update a product](#update-product) | `products:write` | | DELETE | `/products/{id}` | [Archive a product](#delete-product) | `products:write` | | GET | `/products/by_sku/{sku}` | [Find a product variant by SKU](#retrieve-product-variant-by-sku) | `products:read` | | GET | `/products/{id}/variants` | [List product variants](#list-product-variants) | `products:read` | | POST | `/products/{id}/variants` | [Add product variants](#add-product-variants) | `products:write` | | PATCH | `/products/{id}/variants/{variant_id}` | [Update a product variant](#update-product-variant) | `products:write` | | GET | `/product_variants/{id}` | [Retrieve a product variant](#retrieve-product-variant) | `products:read` | | POST | `/shipping/rates` | [Get shipping rates](#list-shipping-rates) | `orders:read` | | POST | `/orders/estimate` | [Estimate an order](#estimate-order) | `orders:read` | | GET | `/orders` | [List orders](#list-orders) | `orders:read` | | POST | `/orders` | [Create an order](#create-order) | `orders:write` | | GET | `/orders/{id}` | [Retrieve an order](#retrieve-order) | `orders:read` | | PATCH | `/orders/{id}` | [Update a draft order](#update-order) | `orders:write` | | POST | `/orders/{id}/confirm` | [Confirm a draft, or release an order waiting for credits](#confirm-order) | `orders:write` | | POST | `/orders/{id}/cancel` | [Cancel an order](#cancel-order) | `orders:write` | | GET | `/orders/{id}/events` | [List order events](#list-order-events) | `orders:read` | | GET | `/wallet` | [Retrieve the wallet](#retrieve-wallet) | `wallet:read` | | GET | `/wallet/transactions` | [List wallet transactions](#list-wallet-transactions) | `wallet:read` | | GET | `/topups` | [List top-ups](#list-topups) | `wallet:read` | | GET | `/topups/{id}` | [Retrieve a top-up](#retrieve-topup) | `wallet:read` | | GET | `/webhook_endpoints` | [List webhook endpoints](#list-webhook-endpoints) | `webhooks:read` | | POST | `/webhook_endpoints` | [Create a webhook endpoint](#create-webhook-endpoint) | `webhooks:write` | | GET | `/webhook_endpoints/{id}` | [Retrieve a webhook endpoint](#retrieve-webhook-endpoint) | `webhooks:read` | | PATCH | `/webhook_endpoints/{id}` | [Update a webhook endpoint](#update-webhook-endpoint) | `webhooks:write` | | DELETE | `/webhook_endpoints/{id}` | [Delete a webhook endpoint](#delete-webhook-endpoint) | `webhooks:write` | | POST | `/webhook_endpoints/{id}/rotate_secret` | [Rotate the signing secret](#rotate-webhook-endpoint-secret) | `webhooks:write` | | POST | `/webhook_endpoints/{id}/test` | [Send a test event](#test-webhook-endpoint) | `webhooks:write` | | GET | `/events` | [List events](#list-events) | `webhooks:read` | | GET | `/events/{id}` | [Retrieve an event](#retrieve-event) | `webhooks:read` | | POST | `/test/wallet/fund` | [Add test credits](#fund-test-wallet) | `*` | | POST | `/test/orders/{id}/advance` | [Advance a test order](#advance-test-order) | `orders:write` | ## Account Your account and the mode (live/test) of the API key. ### Retrieve your account `GET /account` Returns the account the API key belongs to, the key's mode (live/test) and a summary of the wallet of that mode. Requires scope `account:read`. **Scope:** `account:read` **Returns** `200` — The account. [Account](https://swaglists.com/docs/objects.md#account) | Field | Type | Description | | --- | --- | --- | | `object` | "account" | Type of the object: always `account`. | | `id` | string | Unique identifier (starts with `org_`). | | `name` | string | Your account (organization) name. | | `brand_name` | string \| null | Brand printed on packing slips and shown on the tracking page. | | `support_email` | string \| null | Support email shown to your customers (packing slips, tracking page, shipment emails). | | `website` | string \| null | Your website. | | `country` | string \| null | Country of your business (ISO 3166-1 alpha-2). | | `mode` | string | Mode of the API key used for this request. One of: `live`, `test`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `live_mode_enabled` | boolean | Whether live mode is enabled for the account (live keys work and live orders are produced). | | `settings` | object | Order settings that change how the API behaves (Settings → Orders in the dashboard). | | `wallet` | [Wallet](https://swaglists.com/docs/objects.md#wallet) | Wallet of the key's mode. | | `created_at` | string | When the object was created (ISO-8601, UTC). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/account \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "account", "id": "org_01k8x3q8m4h2c9t6v5r7w1y0za", "name": "Company A", "brand_name": "Company A Apparel", "support_email": "help@company-a.example", "website": "https://company-a.example", "country": "US", "mode": "live", "livemode": true, "live_mode_enabled": true, "settings": { "auto_confirm_orders": true, "insufficient_funds_behavior": "hold" }, "wallet": { "object": "wallet", "mode": "live", "livemode": true, "currency": "usd", "available": 482550, "held": 12480, "total": 495030, "low_balance_threshold": 10000 }, "created_at": "2026-10-21T14:32:05.000Z" } ``` ## Catalog Products you can customize and sell, with your wholesale prices, print areas and mockup templates. ### List catalog categories `GET /catalog/categories` All categories (with sub-categories via `parent_id`) and the number of active products in each. Requires scope `catalog:read`. **Scope:** `catalog:read` **Returns** `200` — Categories. [CatalogCategoryList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [CatalogCategory](https://swaglists.com/docs/objects.md#catalog_category) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/catalog/categories \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "object": "catalog_category", "id": "cat_01k8w0q1w2e3r4t5y6u7i8o9p0", "slug": "t-shirts", "name": "T-Shirts", "parent_id": null, "description": "Classic and premium tees.", "image_url": null, "sort_order": 0, "product_count": 12, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ], "has_more": false, "url": "/api/v1/catalog/categories" } ``` ### List catalog products `GET /catalog/products` Products you can customize and sell, featured first. Every variant carries your wholesale `price` (your price list applied, printed on the first placement); `price_from` is the lowest variant price. Requires scope `catalog:read`. **Scope:** `catalog:read` **Query parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Number of objects to return, 1–100 (default 20). Range 1–100. | | `starting_after` | string | no | Cursor for the next page: the id of the last object of the previous page. 1–255 characters. | | `ending_before` | string | no | Cursor for the previous page: the id of the first object of the current page. 1–255 characters. | | `q` | string | no | Search title, subtitle, brand and tags. 1–200 characters. | | `category` | string | no | Category slug or id (includes sub-categories). 1–100 characters. | | `technique` | string | no | Print technique, e.g. dtg, embroidery. 1–40 characters. | | `featured` | string | no | Only featured (true) or non-featured (false) products. One of: `true`, `false`. | **Returns** `200` — A page of catalog products. [CatalogProductList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [CatalogProduct](https://swaglists.com/docs/objects.md#catalog_product) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl "https://swaglists.com/api/v1/catalog/products?limit=3" \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "object": "catalog_product", "id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "slug": "classic-unisex-tee", "title": "Classic Unisex Tee", "subtitle": "100% combed cotton", "description": "A soft, everyday tee.", "category": { "object": "catalog_category", "id": "cat_01k8w0q1w2e3r4t5y6u7i8o9p0", "slug": "t-shirts", "name": "T-Shirts", "parent_id": null }, "tags": [ "bestseller" ], "images": [ { "url": "https://swaglists.com/f/file_01k8w1c2v4b6n8m0q2w4e6r8t1/1767225600.9b1c/tee.png", "alt": "Classic tee" } ], "features": [ "Pre-shrunk", "Side-seamed" ], "techniques": [ "dtg" ], "material": "100% cotton", "care_instructions": "Machine wash cold, inside out.", "size_guide": null, "options": [ { "name": "Color", "values": [ { "value": "Black", "label": "Black", "hex": "#111111" } ] }, { "name": "Size", "values": [ { "value": "M", "label": "M", "hex": null } ] } ], "production_days": { "min": 2, "max": 4 }, "featured": true, "placements": [ { "object": "placement", "key": "front", "label": "Front", "description": "Front print area", "technique": "dtg", "aspect_ratio": 0.833333, "width_in": 15, "height_in": 18, "recommended_width_px": 4500, "recommended_height_px": 5400, "min_dpi": 150, "required": false, "file_requirements": { "formats": [ "png", "jpeg", "webp", "tiff", "svg" ], "max_file_size_bytes": 104857600, "max_dimension_px": 30000, "recommended_width_px": 4500, "recommended_height_px": 5400, "recommended_dpi": 300, "min_dpi": 150, "color_profile": "sRGB", "transparent_background": true } } ], "variants": [ { "object": "catalog_variant", "id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "sku": "TEE-CLASSIC-BLK-M", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "color": "Black", "color_hex": "#111111", "size": "M", "image_url": null, "available": true, "price": 1299, "currency": "usd" } ], "mockup_templates": [ { "object": "mockup_template", "id": "cmt_01k8w1b3c5v7b9n1m3a5s7d9f1", "placement_key": "front", "color": "Black", "label": "Front", "image_url": "https://swaglists.com/f/file_01k8w1c2v4b6n8m0q2w4e6r8t0/1767225600.3f9a/tee-front.png", "image_width_px": 1600, "image_height_px": 2000, "print_area": { "x": 0.3, "y": 0.25, "width": 0.4, "height": 0.48 }, "blend_mode": "multiply", "is_primary": true } ], "price_range": { "min": 1299, "max": 1599, "currency": "usd" }, "price_from": 1299, "currency": "usd", "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ], "has_more": false, "url": "/api/v1/catalog/products" } ``` ### Retrieve a catalog product `GET /catalog/products/{id_or_slug}` Full product: options, variants with your price and availability, placements (print areas) with recommended pixel sizes and file requirements, and mockup templates. Requires scope `catalog:read`. **Scope:** `catalog:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id_or_slug` | string | yes | Catalog product id or slug. 1–300 characters. | **Returns** `200` — The catalog product. [CatalogProduct](https://swaglists.com/docs/objects.md#catalog_product) | Field | Type | Description | | --- | --- | --- | | `object` | "catalog_product" | Type of the object: always `catalog_product`. | | `id` | string | Unique identifier (starts with `cp_`). | | `slug` | string | URL-friendly identifier; can be used instead of the id in `GET /catalog/products/{id_or_slug}`. | | `title` | string | Product title. | | `subtitle` | string \| null | Short tagline. | | `description` | string \| null | Markdown. | | `category` | [CatalogCategoryRef](https://swaglists.com/docs/objects.md#catalog_category) | Category of the product. | | `tags` | array of string | Tags (e.g. `bestseller`). | | `images` | array of object | Product photos. | | `features` | array of string | Selling points. | | `techniques` | array of string | Print techniques used (e.g. `dtg`, `embroidery`, `sublimation`, `digital`). | | `material` | string \| null | Material. | | `care_instructions` | string \| null | Care instructions. | | `size_guide` | map of any \| null | Size chart data, when available. | | `options` | array of object | Option names and values (e.g. Color, Size) in display order. | | `production_days` | object | Business days to produce before shipping. | | `featured` | boolean | Featured products are listed first. | | `placements` | array of [Placement](https://swaglists.com/docs/objects.md#placement) | Print areas you can put artwork on. | | `variants` | array of [CatalogVariant](https://swaglists.com/docs/objects.md#catalog_variant) | Active variants with your wholesale price and availability. | | `mockup_templates` | array of [MockupTemplate](https://swaglists.com/docs/objects.md#mockup_template) | Templates to render previews. | | `price_range` | object \| null | Lowest and highest wholesale variant price (first placement). | | `price_from` | integer \| null | Lowest wholesale price over active variants (cents). | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). | | `updated_at` | string \| null | When the object was last changed (ISO-8601, UTC). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/catalog/products/cp_01k8w1a2s3d4f5g6h7j8k9m0n1 \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "catalog_product", "id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "slug": "classic-unisex-tee", "title": "Classic Unisex Tee", "subtitle": "100% combed cotton", "description": "A soft, everyday tee.", "category": { "object": "catalog_category", "id": "cat_01k8w0q1w2e3r4t5y6u7i8o9p0", "slug": "t-shirts", "name": "T-Shirts", "parent_id": null }, "tags": [ "bestseller" ], "images": [ { "url": "https://swaglists.com/f/file_01k8w1c2v4b6n8m0q2w4e6r8t1/1767225600.9b1c/tee.png", "alt": "Classic tee" } ], "features": [ "Pre-shrunk", "Side-seamed" ], "techniques": [ "dtg" ], "material": "100% cotton", "care_instructions": "Machine wash cold, inside out.", "size_guide": null, "options": [ { "name": "Color", "values": [ { "value": "Black", "label": "Black", "hex": "#111111" } ] }, { "name": "Size", "values": [ { "value": "M", "label": "M", "hex": null } ] } ], "production_days": { "min": 2, "max": 4 }, "featured": true, "placements": [ { "object": "placement", "key": "front", "label": "Front", "description": "Front print area", "technique": "dtg", "aspect_ratio": 0.833333, "width_in": 15, "height_in": 18, "recommended_width_px": 4500, "recommended_height_px": 5400, "min_dpi": 150, "required": false, "file_requirements": { "formats": [ "png", "jpeg", "webp", "tiff", "svg" ], "max_file_size_bytes": 104857600, "max_dimension_px": 30000, "recommended_width_px": 4500, "recommended_height_px": 5400, "recommended_dpi": 300, "min_dpi": 150, "color_profile": "sRGB", "transparent_background": true } } ], "variants": [ { "object": "catalog_variant", "id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "sku": "TEE-CLASSIC-BLK-M", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "color": "Black", "color_hex": "#111111", "size": "M", "image_url": null, "available": true, "price": 1299, "currency": "usd" } ], "mockup_templates": [ { "object": "mockup_template", "id": "cmt_01k8w1b3c5v7b9n1m3a5s7d9f1", "placement_key": "front", "color": "Black", "label": "Front", "image_url": "https://swaglists.com/f/file_01k8w1c2v4b6n8m0q2w4e6r8t0/1767225600.3f9a/tee-front.png", "image_width_px": 1600, "image_height_px": 2000, "print_area": { "x": 0.3, "y": 0.25, "width": 0.4, "height": 0.48 }, "blend_mode": "multiply", "is_primary": true } ], "price_range": { "min": 1299, "max": 1599, "currency": "usd" }, "price_from": 1299, "currency": "usd", "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ``` ## Files Upload artwork (multipart or by URL) to use in designs. ### List files `GET /files` Your uploaded files and generated mockups, newest first. Requires scope `files:read`. **Scope:** `files:read` **Query parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Number of objects to return, 1–100 (default 20). Range 1–100. | | `starting_after` | string | no | Cursor for the next page: the id of the last object of the previous page. 1–255 characters. | | `ending_before` | string | no | Cursor for the previous page: the id of the first object of the current page. 1–255 characters. | | `purpose` | string | no | Filter by purpose. One of: `artwork`, `logo`, `mockup`. | **Returns** `200` — A page of files. [FileList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [File](https://swaglists.com/docs/objects.md#file) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl "https://swaglists.com/api/v1/files?limit=3" \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "object": "file", "id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "filename": "logo-front.png", "mime_type": "image/png", "size_bytes": 482113, "width_px": 4500, "height_px": 5400, "dpi": 300, "status": "ready", "purpose": "artwork", "url": "https://swaglists.com/f/file_01k8x3y1q3w5e7r9t1y3u5i7o9/1767225600.ab12cd/logo-front.png", "created_at": "2026-10-21T14:32:05.000Z" } ], "has_more": false, "url": "/api/v1/files" } ``` ### Upload a file `POST /files` Upload artwork either as `multipart/form-data` (field `file`, optional `purpose`) or as JSON `{"url": "https://…"}` — we download the image once (public http(s) URLs only, size and time limited). Images are validated (PNG, JPEG, WebP, TIFF, GIF, SVG; SVGs are sanitized and rasterized). Requires scope `files:write`. **Scope:** `files:write` · **Idempotency:** send an `Idempotency-Key` header **Request body** (`application/json`) — CreateFileFromUrlRequest Either a JSON body with `url`, or a multipart upload with `file`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | yes | Public http(s) URL of the image; we download it once. Max 2048 characters. Must be an http(s) URL. | | `purpose` | string | no | Defaults to artwork. One of: `artwork`, `logo`. | | `filename` | string | no | 1–200 characters. | **Request body** (`multipart/form-data`) — UploadFileRequest Either a JSON body with `url`, or a multipart upload with `file`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `file` | file | yes | The image (PNG, JPEG, WebP, TIFF, GIF or SVG). | | `purpose` | string | no | Defaults to artwork. One of: `artwork`, `logo`. | **Returns** `201` — The stored file. [File](https://swaglists.com/docs/objects.md#file) | Field | Type | Description | | --- | --- | --- | | `object` | "file" | Type of the object: always `file`. | | `id` | string | Unique identifier (starts with `file_`). | | `filename` | string | File name. | | `mime_type` | string | Media type (images are stored as PNG, JPEG or WebP). | | `size_bytes` | integer | Size in bytes. | | `width_px` | integer \| null | Width in pixels. | | `height_px` | integer \| null | Height in pixels. | | `dpi` | number \| null | Resolution stored in the image metadata, when present (informational — print resolution depends on the placed size). | | `status` | string | ready to use, still processing (pending), failed processing, or deleted. One of: `pending`, `ready`, `failed`, `deleted`. | | `purpose` | string | artwork or logo (your uploads), or mockup (generated product mockups). One of: `artwork`, `logo`, `mockup`. | | `url` | string \| null | Signed download URL, valid for about 24 hours. | | `created_at` | string | When the object was created (ISO-8601, UTC). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`file_invalid`](https://swaglists.com/docs/errors.md#file_invalid), [`file_too_large`](https://swaglists.com/docs/errors.md#file_too_large), [`file_unsafe`](https://swaglists.com/docs/errors.md#file_unsafe), [`file_unsupported_type`](https://swaglists.com/docs/errors.md#file_unsupported_type), [`download_failed`](https://swaglists.com/docs/errors.md#download_failed), [`download_not_an_image`](https://swaglists.com/docs/errors.md#download_not_an_image), [`download_timeout`](https://swaglists.com/docs/errors.md#download_timeout) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/files \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -F "file=@artwork.png" \ -F "purpose=artwork" ``` **Example response** ```json { "object": "file", "id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "filename": "logo-front.png", "mime_type": "image/png", "size_bytes": 482113, "width_px": 4500, "height_px": 5400, "dpi": 300, "status": "ready", "purpose": "artwork", "url": "https://swaglists.com/f/file_01k8x3y1q3w5e7r9t1y3u5i7o9/1767225600.ab12cd/logo-front.png", "created_at": "2026-10-21T14:32:05.000Z" } ``` ### Retrieve a file `GET /files/{id}` Returns the file with a fresh signed download URL. Requires scope `files:read`. **Scope:** `files:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | File id. 1–300 characters. | **Returns** `200` — The file. [File](https://swaglists.com/docs/objects.md#file) | Field | Type | Description | | --- | --- | --- | | `object` | "file" | Type of the object: always `file`. | | `id` | string | Unique identifier (starts with `file_`). | | `filename` | string | File name. | | `mime_type` | string | Media type (images are stored as PNG, JPEG or WebP). | | `size_bytes` | integer | Size in bytes. | | `width_px` | integer \| null | Width in pixels. | | `height_px` | integer \| null | Height in pixels. | | `dpi` | number \| null | Resolution stored in the image metadata, when present (informational — print resolution depends on the placed size). | | `status` | string | ready to use, still processing (pending), failed processing, or deleted. One of: `pending`, `ready`, `failed`, `deleted`. | | `purpose` | string | artwork or logo (your uploads), or mockup (generated product mockups). One of: `artwork`, `logo`, `mockup`. | | `url` | string \| null | Signed download URL, valid for about 24 hours. | | `created_at` | string | When the object was created (ISO-8601, UTC). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/files/file_01k8x3y1q3w5e7r9t1y3u5i7o9 \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "file", "id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "filename": "logo-front.png", "mime_type": "image/png", "size_bytes": 482113, "width_px": 4500, "height_px": 5400, "dpi": 300, "status": "ready", "purpose": "artwork", "url": "https://swaglists.com/f/file_01k8x3y1q3w5e7r9t1y3u5i7o9/1767225600.ab12cd/logo-front.png", "created_at": "2026-10-21T14:32:05.000Z" } ``` ### Delete a file `DELETE /files/{id}` Deletes an uploaded file. Files used by a non-archived product cannot be deleted (409 file_in_use). Requires scope `files:write`. **Scope:** `files:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | File id. 1–300 characters. | **Returns** `200` — The deleted file (status deleted). [File](https://swaglists.com/docs/objects.md#file) | Field | Type | Description | | --- | --- | --- | | `object` | "file" | Type of the object: always `file`. | | `id` | string | Unique identifier (starts with `file_`). | | `filename` | string | File name. | | `mime_type` | string | Media type (images are stored as PNG, JPEG or WebP). | | `size_bytes` | integer | Size in bytes. | | `width_px` | integer \| null | Width in pixels. | | `height_px` | integer \| null | Height in pixels. | | `dpi` | number \| null | Resolution stored in the image metadata, when present (informational — print resolution depends on the placed size). | | `status` | string | ready to use, still processing (pending), failed processing, or deleted. One of: `pending`, `ready`, `failed`, `deleted`. | | `purpose` | string | artwork or logo (your uploads), or mockup (generated product mockups). One of: `artwork`, `logo`, `mockup`. | | `url` | string \| null | Signed download URL, valid for about 24 hours. | | `created_at` | string | When the object was created (ISO-8601, UTC). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`file_in_use`](https://swaglists.com/docs/errors.md#file_in_use) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X DELETE https://swaglists.com/api/v1/files/file_01k8x3y1q3w5e7r9t1y3u5i7o9 \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` **Example response** ```json { "object": "file", "id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "filename": "logo-front.png", "mime_type": "image/png", "size_bytes": 482113, "width_px": 4500, "height_px": 5400, "dpi": 300, "status": "ready", "purpose": "artwork", "url": "https://swaglists.com/f/file_01k8x3y1q3w5e7r9t1y3u5i7o9/1767225600.ab12cd/logo-front.png", "created_at": "2026-10-21T14:32:05.000Z" } ``` ## Products Your products: catalog product + designs + variants with your own SKUs. ### List products `GET /products` Your products, newest first. Archived products are excluded unless `status=archived` or `status=all`. Requires scope `products:read`. **Scope:** `products:read` **Query parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Number of objects to return, 1–100 (default 20). Range 1–100. | | `starting_after` | string | no | Cursor for the next page: the id of the last object of the previous page. 1–255 characters. | | `ending_before` | string | no | Cursor for the previous page: the id of the first object of the current page. 1–255 characters. | | `status` | string | no | One of: `draft`, `active`, `archived`, `all`. | | `q` | string | no | Title contains, or exact external_id / SKU / id. 1–200 characters. | | `catalog_product_id` | string | no | 1–100 characters. | **Returns** `200` — A page of products. [ProductList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [Product](https://swaglists.com/docs/objects.md#product) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl "https://swaglists.com/api/v1/products?limit=3" \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "object": "product", "id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "status": "active", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "external_id": "shopify-product-9921", "tags": [ "logo" ], "metadata": {}, "retail_price": 2900, "currency": "usd", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "variants": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "mockups": [ { "url": "https://swaglists.com/f/file_01k8x3y9a1s3d5f7g9h1j3k5l7/1782777600.c0ffee/mockup-front.png", "placement": "front", "color": "Black", "is_primary": true } ], "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ], "has_more": false, "url": "/api/v1/products" } ``` ### Create a product `POST /products` Creates a product from a catalog product and your designs (one entry per placement; each layer references an uploaded `file_id` or a `url` we download). Variants default to every active catalog variant, each with a generated SKU unless you pass your own. Mockups are rendered in the background (`product.updated` fires when ready). Requires scope `products:write`. **Scope:** `products:write` · **Idempotency:** send an `Idempotency-Key` header **Request body** (`application/json`) — CreateProductRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `catalog_product_id` | string | yes | 1–100 characters. | | `title` | string \| null | no | 1–200 characters. | | `description` | string \| null | no | Max 20000 characters. | | `designs` | array of DesignInput | yes | 1–20 items. | | `designs[].placement` | string | yes | Placement key from the catalog product (e.g. front, back). 1–64 characters. | | `designs[].layers` | array of DesignLayerInput | yes | 1–20 items. | | `designs[].layers[].file_id` | string | no | Id of an uploaded artwork file. 1–100 characters. | | `designs[].layers[].url` | string | no | Or a public image URL; we download it into a new artwork file. Max 2048 characters. Must be an http(s) URL. | | `designs[].layers[].position` | Position | no | Where the artwork sits inside the placement's print area. Fractions of the print area (0–1; values outside allow intentional bleed). Omit to center the artwork and fit it inside the area. | | `designs[].layers[].position.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `designs[].layers[].position.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `designs[].layers[].position.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `designs[].layers[].position.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `designs[].layers[].position.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `designs[].layers[].transform` | Position | no | Alias of `position` (as returned on products and orders). | | `designs[].layers[].transform.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `designs[].layers[].transform.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `designs[].layers[].transform.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `designs[].layers[].transform.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `designs[].layers[].transform.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `designs[].layers[].opacity` | number | no | 0–1 (default 1). Range 0–1. | | `variants` | "all" or array of ProductVariantInput | no | "all" (every active catalog variant) or an explicit list. | | `variants[].catalog_variant_id` | string | yes | 1–100 characters. | | `variants[].sku` | string \| null | no | Your SKU; generated when omitted. 1–64 characters. Pattern `^[\x21-\x7E](?:[\x20-\x7E]{0,62}[\x21-\x7E])?$`. | | `variants[].retail_price` | integer \| null | no | Range 0–100000000. | | `variants[].external_id` | string \| null | no | 1–255 characters. | | `retail_price` | integer \| null | no | Default retail price for variants (cents). Range 0–100000000. | | `external_id` | string \| null | no | 1–255 characters. | | `tags` | array of string | no | Up to 50 items. | | `metadata` | map of string | no | Up to 50 key/value pairs (keys ≤ 40 chars, string values ≤ 500 chars) for your own references. Never store sensitive data here. | | `status` | string | no | Default active. One of: `draft`, `active`. | **Returns** `201` — The created product (with `warnings`). [Product](https://swaglists.com/docs/objects.md#product) | Field | Type | Description | | --- | --- | --- | | `object` | "product" | Type of the object: always `product`. | | `id` | string | Unique identifier (starts with `prod_`). | | `title` | string | Product title. | | `description` | string \| null | Product description (plain text or markdown). | | `status` | string | draft, active, or archived (archived products can no longer be ordered). One of: `draft`, `active`, `archived`. | | `catalog_product_id` | string | Catalog product this product is made from. | | `external_id` | string \| null | Your id for the product (e.g. your store's product id). | | `tags` | array of string | Your tags. | | `metadata` | map of string | Your key/value pairs (string values). | | `retail_price` | integer \| null | Default retail price of the variants (informational, used in exports), in cents. | | `currency` | "usd" | Three-letter currency code, lowercase (`usd`). | | `designs` | array of [Design](https://swaglists.com/docs/objects.md#design) | Artwork per placement. | | `variants` | array of [ProductVariant](https://swaglists.com/docs/objects.md#product_variant) | Sellable variants with your SKUs. | | `mockups` | array of object | Generated preview images. Rendered in the background; `product.updated` fires when they are ready. | | `warnings` | array of [ProductWarning](https://swaglists.com/docs/objects.md#product_warning) | Only on create/update responses: non-blocking design issues. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `updated_at` | string | When the object was last changed (ISO-8601, UTC). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`catalog_product_unavailable`](https://swaglists.com/docs/errors.md#catalog_product_unavailable), [`designs_required`](https://swaglists.com/docs/errors.md#designs_required), [`duplicate_placement`](https://swaglists.com/docs/errors.md#duplicate_placement), [`unknown_placement`](https://swaglists.com/docs/errors.md#unknown_placement), [`placement_required`](https://swaglists.com/docs/errors.md#placement_required), [`file_not_artwork`](https://swaglists.com/docs/errors.md#file_not_artwork), [`file_not_ready`](https://swaglists.com/docs/errors.md#file_not_ready), [`file_not_image`](https://swaglists.com/docs/errors.md#file_not_image), [`design_not_visible`](https://swaglists.com/docs/errors.md#design_not_visible), [`artwork_resolution_too_low`](https://swaglists.com/docs/errors.md#artwork_resolution_too_low), [`no_layers`](https://swaglists.com/docs/errors.md#no_layers), [`too_many_layers`](https://swaglists.com/docs/errors.md#too_many_layers), [`invalid_transform`](https://swaglists.com/docs/errors.md#invalid_transform), [`no_variants`](https://swaglists.com/docs/errors.md#no_variants), [`duplicate_variant`](https://swaglists.com/docs/errors.md#duplicate_variant), [`duplicate_sku`](https://swaglists.com/docs/errors.md#duplicate_sku), [`variant_unavailable`](https://swaglists.com/docs/errors.md#variant_unavailable), [`download_failed`](https://swaglists.com/docs/errors.md#download_failed) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`sku_taken`](https://swaglists.com/docs/errors.md#sku_taken), [`external_id_taken`](https://swaglists.com/docs/errors.md#external_id_taken) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/products \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "title": "Company A Logo Tee", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "position": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 } } ] } ], "variants": "all", "retail_price": 2900, "external_id": "shopify-product-9921" }' ``` **Example response** ```json { "object": "product", "id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "status": "active", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "external_id": "shopify-product-9921", "tags": [ "logo" ], "metadata": {}, "retail_price": 2900, "currency": "usd", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "variants": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "mockups": [ { "url": "https://swaglists.com/f/file_01k8x3y9a1s3d5f7g9h1j3k5l7/1782777600.c0ffee/mockup-front.png", "placement": "front", "color": "Black", "is_primary": true } ], "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ``` ### Retrieve a product `GET /products/{id}` Requires scope `products:read`. **Scope:** `products:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Product id. 1–300 characters. | **Returns** `200` — The product. [Product](https://swaglists.com/docs/objects.md#product) | Field | Type | Description | | --- | --- | --- | | `object` | "product" | Type of the object: always `product`. | | `id` | string | Unique identifier (starts with `prod_`). | | `title` | string | Product title. | | `description` | string \| null | Product description (plain text or markdown). | | `status` | string | draft, active, or archived (archived products can no longer be ordered). One of: `draft`, `active`, `archived`. | | `catalog_product_id` | string | Catalog product this product is made from. | | `external_id` | string \| null | Your id for the product (e.g. your store's product id). | | `tags` | array of string | Your tags. | | `metadata` | map of string | Your key/value pairs (string values). | | `retail_price` | integer \| null | Default retail price of the variants (informational, used in exports), in cents. | | `currency` | "usd" | Three-letter currency code, lowercase (`usd`). | | `designs` | array of [Design](https://swaglists.com/docs/objects.md#design) | Artwork per placement. | | `variants` | array of [ProductVariant](https://swaglists.com/docs/objects.md#product_variant) | Sellable variants with your SKUs. | | `mockups` | array of object | Generated preview images. Rendered in the background; `product.updated` fires when they are ready. | | `warnings` | array of [ProductWarning](https://swaglists.com/docs/objects.md#product_warning) | Only on create/update responses: non-blocking design issues. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `updated_at` | string | When the object was last changed (ISO-8601, UTC). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/products/prod_01k8x3z7r1t3y5u7i9o1p3a5s7 \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "product", "id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "status": "active", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "external_id": "shopify-product-9921", "tags": [ "logo" ], "metadata": {}, "retail_price": 2900, "currency": "usd", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "variants": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "mockups": [ { "url": "https://swaglists.com/f/file_01k8x3y9a1s3d5f7g9h1j3k5l7/1782777600.c0ffee/mockup-front.png", "placement": "front", "color": "Black", "is_primary": true } ], "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ``` ### Update a product `PATCH /products/{id}` Updates details and/or replaces the designs (`designs`). Omitted fields are left unchanged. Requires scope `products:write`. **Scope:** `products:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Product id. 1–300 characters. | **Request body** (`application/json`) — UpdateProductRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | no | 1–200 characters. | | `description` | string \| null | no | Max 20000 characters. | | `status` | string | no | Setting a status restores an archived product. One of: `draft`, `active`. | | `tags` | array of string | no | Up to 50 items. | | `metadata` | map of string | no | Up to 50 key/value pairs (keys ≤ 40 chars, string values ≤ 500 chars) for your own references. Never store sensitive data here. | | `external_id` | string \| null | no | 1–255 characters. | | `retail_price` | integer \| null | no | Range 0–100000000. | | `designs` | array of DesignInput | no | Replaces all designs (mockups are regenerated). 1–20 items. | | `designs[].placement` | string | yes | Placement key from the catalog product (e.g. front, back). 1–64 characters. | | `designs[].layers` | array of DesignLayerInput | yes | 1–20 items. | | `designs[].layers[].file_id` | string | no | Id of an uploaded artwork file. 1–100 characters. | | `designs[].layers[].url` | string | no | Or a public image URL; we download it into a new artwork file. Max 2048 characters. Must be an http(s) URL. | | `designs[].layers[].position` | Position | no | Where the artwork sits inside the placement's print area. Fractions of the print area (0–1; values outside allow intentional bleed). Omit to center the artwork and fit it inside the area. | | `designs[].layers[].position.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `designs[].layers[].position.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `designs[].layers[].position.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `designs[].layers[].position.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `designs[].layers[].position.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `designs[].layers[].transform` | Position | no | Alias of `position` (as returned on products and orders). | | `designs[].layers[].transform.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `designs[].layers[].transform.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `designs[].layers[].transform.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `designs[].layers[].transform.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `designs[].layers[].transform.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `designs[].layers[].opacity` | number | no | 0–1 (default 1). Range 0–1. | **Returns** `200` — The updated product. [Product](https://swaglists.com/docs/objects.md#product) | Field | Type | Description | | --- | --- | --- | | `object` | "product" | Type of the object: always `product`. | | `id` | string | Unique identifier (starts with `prod_`). | | `title` | string | Product title. | | `description` | string \| null | Product description (plain text or markdown). | | `status` | string | draft, active, or archived (archived products can no longer be ordered). One of: `draft`, `active`, `archived`. | | `catalog_product_id` | string | Catalog product this product is made from. | | `external_id` | string \| null | Your id for the product (e.g. your store's product id). | | `tags` | array of string | Your tags. | | `metadata` | map of string | Your key/value pairs (string values). | | `retail_price` | integer \| null | Default retail price of the variants (informational, used in exports), in cents. | | `currency` | "usd" | Three-letter currency code, lowercase (`usd`). | | `designs` | array of [Design](https://swaglists.com/docs/objects.md#design) | Artwork per placement. | | `variants` | array of [ProductVariant](https://swaglists.com/docs/objects.md#product_variant) | Sellable variants with your SKUs. | | `mockups` | array of object | Generated preview images. Rendered in the background; `product.updated` fires when they are ready. | | `warnings` | array of [ProductWarning](https://swaglists.com/docs/objects.md#product_warning) | Only on create/update responses: non-blocking design issues. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `updated_at` | string | When the object was last changed (ISO-8601, UTC). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`duplicate_placement`](https://swaglists.com/docs/errors.md#duplicate_placement), [`unknown_placement`](https://swaglists.com/docs/errors.md#unknown_placement), [`placement_required`](https://swaglists.com/docs/errors.md#placement_required), [`file_not_artwork`](https://swaglists.com/docs/errors.md#file_not_artwork), [`file_not_ready`](https://swaglists.com/docs/errors.md#file_not_ready), [`file_not_image`](https://swaglists.com/docs/errors.md#file_not_image), [`design_not_visible`](https://swaglists.com/docs/errors.md#design_not_visible), [`artwork_resolution_too_low`](https://swaglists.com/docs/errors.md#artwork_resolution_too_low), [`download_failed`](https://swaglists.com/docs/errors.md#download_failed) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`product_archived`](https://swaglists.com/docs/errors.md#product_archived), [`external_id_taken`](https://swaglists.com/docs/errors.md#external_id_taken) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X PATCH https://swaglists.com/api/v1/products/prod_01k8x3z7r1t3y5u7i9o1p3a5s7 \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "title": "Company A Logo Tee (2026)", "retail_price": 3100 }' ``` **Example response** ```json { "object": "product", "id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "status": "active", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "external_id": "shopify-product-9921", "tags": [ "logo" ], "metadata": {}, "retail_price": 2900, "currency": "usd", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "variants": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "mockups": [ { "url": "https://swaglists.com/f/file_01k8x3y9a1s3d5f7g9h1j3k5l7/1782777600.c0ffee/mockup-front.png", "placement": "front", "color": "Black", "is_primary": true } ], "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ``` ### Archive a product `DELETE /products/{id}` Archives the product: its variants can no longer be ordered, SKUs stay reserved and existing orders are unaffected. Restore it by PATCHing a `status`. Requires scope `products:write`. **Scope:** `products:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Product id. 1–300 characters. | **Returns** `200` — The archived product. [Product](https://swaglists.com/docs/objects.md#product) | Field | Type | Description | | --- | --- | --- | | `object` | "product" | Type of the object: always `product`. | | `id` | string | Unique identifier (starts with `prod_`). | | `title` | string | Product title. | | `description` | string \| null | Product description (plain text or markdown). | | `status` | string | draft, active, or archived (archived products can no longer be ordered). One of: `draft`, `active`, `archived`. | | `catalog_product_id` | string | Catalog product this product is made from. | | `external_id` | string \| null | Your id for the product (e.g. your store's product id). | | `tags` | array of string | Your tags. | | `metadata` | map of string | Your key/value pairs (string values). | | `retail_price` | integer \| null | Default retail price of the variants (informational, used in exports), in cents. | | `currency` | "usd" | Three-letter currency code, lowercase (`usd`). | | `designs` | array of [Design](https://swaglists.com/docs/objects.md#design) | Artwork per placement. | | `variants` | array of [ProductVariant](https://swaglists.com/docs/objects.md#product_variant) | Sellable variants with your SKUs. | | `mockups` | array of object | Generated preview images. Rendered in the background; `product.updated` fires when they are ready. | | `warnings` | array of [ProductWarning](https://swaglists.com/docs/objects.md#product_warning) | Only on create/update responses: non-blocking design issues. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `updated_at` | string | When the object was last changed (ISO-8601, UTC). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X DELETE https://swaglists.com/api/v1/products/prod_01k8x3z7r1t3y5u7i9o1p3a5s7 \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` **Example response** ```json { "object": "product", "id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "status": "active", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "external_id": "shopify-product-9921", "tags": [ "logo" ], "metadata": {}, "retail_price": 2900, "currency": "usd", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "variants": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "mockups": [ { "url": "https://swaglists.com/f/file_01k8x3y9a1s3d5f7g9h1j3k5l7/1782777600.c0ffee/mockup-front.png", "placement": "front", "color": "Black", "is_primary": true } ], "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ``` ### Find a product variant by SKU `GET /products/by_sku/{sku}` Looks up one of your product variants by its SKU (URL-encode SKUs with special characters). Requires scope `products:read`. **Scope:** `products:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `sku` | string | yes | Your SKU. 1–300 characters. | **Returns** `200` — The variant. [ProductVariant](https://swaglists.com/docs/objects.md#product_variant) | Field | Type | Description | | --- | --- | --- | | `object` | "product_variant" | Type of the object: always `product_variant`. | | `id` | string | Unique identifier (starts with `pvar_`). | | `product_id` | string | Product the variant belongs to. | | `sku` | string | Your SKU (unique in your account). Order items can reference it. | | `catalog_variant_id` | string | Catalog variant that is produced for this SKU. | | `title` | string | Variant title (e.g. `Black / M`). | | `options` | map of string | Option values (e.g. `{"Color": "Black", "Size": "M"}`). | | `retail_price` | integer \| null | Your retail price (informational), cents. | | `price` | integer \| null | Your wholesale price per unit with this product's designs, cents. | | `currency` | "usd" | Three-letter currency code, lowercase (`usd`). | | `external_id` | string \| null | Your id for the variant (e.g. your store's variant id). | | `enabled` | boolean | Disabled variants cannot be ordered (their SKU stays reserved). | | `available` | boolean | Whether the variant can be ordered right now: enabled, product not archived, and the catalog variant offered and in stock. `false` while it is temporarily out of stock — mark it sold out in your store; `product.updated` fires when this changes. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/products/by_sku/CA-TEE-BLK-M \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ``` ### List product variants `GET /products/{id}/variants` All variants of the product (including disabled ones). Requires scope `products:read`. **Scope:** `products:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Product id. 1–300 characters. | **Returns** `200` — The variants. [ProductVariantList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [ProductVariant](https://swaglists.com/docs/objects.md#product_variant) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/products/prod_01k8x3z7r1t3y5u7i9o1p3a5s7/variants \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "has_more": false, "url": "/api/v1/products/prod_01k8x3z7r1t3y5u7i9o1p3a5s7/variants" } ``` ### Add product variants `POST /products/{id}/variants` Adds catalog variants to the product (or re-enables disabled ones). Pass `"all"` for every active catalog variant. Requires scope `products:write`. **Scope:** `products:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Product id. 1–300 characters. | **Request body** (`application/json`) — AddProductVariantsRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `variants` | "all" or array of ProductVariantInput | yes | "all" (every active catalog variant) or an explicit list. | | `variants[].catalog_variant_id` | string | yes | 1–100 characters. | | `variants[].sku` | string \| null | no | Your SKU; generated when omitted. 1–64 characters. Pattern `^[\x21-\x7E](?:[\x20-\x7E]{0,62}[\x21-\x7E])?$`. | | `variants[].retail_price` | integer \| null | no | Range 0–100000000. | | `variants[].external_id` | string \| null | no | 1–255 characters. | **Returns** `200` — The product with its variants. [Product](https://swaglists.com/docs/objects.md#product) | Field | Type | Description | | --- | --- | --- | | `object` | "product" | Type of the object: always `product`. | | `id` | string | Unique identifier (starts with `prod_`). | | `title` | string | Product title. | | `description` | string \| null | Product description (plain text or markdown). | | `status` | string | draft, active, or archived (archived products can no longer be ordered). One of: `draft`, `active`, `archived`. | | `catalog_product_id` | string | Catalog product this product is made from. | | `external_id` | string \| null | Your id for the product (e.g. your store's product id). | | `tags` | array of string | Your tags. | | `metadata` | map of string | Your key/value pairs (string values). | | `retail_price` | integer \| null | Default retail price of the variants (informational, used in exports), in cents. | | `currency` | "usd" | Three-letter currency code, lowercase (`usd`). | | `designs` | array of [Design](https://swaglists.com/docs/objects.md#design) | Artwork per placement. | | `variants` | array of [ProductVariant](https://swaglists.com/docs/objects.md#product_variant) | Sellable variants with your SKUs. | | `mockups` | array of object | Generated preview images. Rendered in the background; `product.updated` fires when they are ready. | | `warnings` | array of [ProductWarning](https://swaglists.com/docs/objects.md#product_warning) | Only on create/update responses: non-blocking design issues. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `updated_at` | string | When the object was last changed (ISO-8601, UTC). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`variant_unavailable`](https://swaglists.com/docs/errors.md#variant_unavailable), [`duplicate_variant`](https://swaglists.com/docs/errors.md#duplicate_variant), [`duplicate_sku`](https://swaglists.com/docs/errors.md#duplicate_sku) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`product_archived`](https://swaglists.com/docs/errors.md#product_archived), [`variant_exists`](https://swaglists.com/docs/errors.md#variant_exists), [`sku_taken`](https://swaglists.com/docs/errors.md#sku_taken) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/products/prod_01k8x3z7r1t3y5u7i9o1p3a5s7/variants \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "variants": [ { "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-L" } ] }' ``` **Example response** ```json { "object": "product", "id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "status": "active", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "external_id": "shopify-product-9921", "tags": [ "logo" ], "metadata": {}, "retail_price": 2900, "currency": "usd", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "variants": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "mockups": [ { "url": "https://swaglists.com/f/file_01k8x3y9a1s3d5f7g9h1j3k5l7/1782777600.c0ffee/mockup-front.png", "placement": "front", "color": "Black", "is_primary": true } ], "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ``` ### Update a product variant `PATCH /products/{id}/variants/{variant_id}` Changes the SKU (unique in your account), retail price, external id or enables/disables the variant. Requires scope `products:write`. **Scope:** `products:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Product id. 1–300 characters. | | `variant_id` | string | yes | Product variant id. 1–300 characters. | **Request body** (`application/json`) — UpdateProductVariantRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `sku` | string | no | 1–64 characters. Pattern `^[\x21-\x7E](?:[\x20-\x7E]{0,62}[\x21-\x7E])?$`. | | `retail_price` | integer \| null | no | Range 0–100000000. | | `enabled` | boolean | no | Disabled variants cannot be ordered; their SKU stays reserved. | | `external_id` | string \| null | no | 1–255 characters. | **Returns** `200` — The updated variant. [ProductVariant](https://swaglists.com/docs/objects.md#product_variant) | Field | Type | Description | | --- | --- | --- | | `object` | "product_variant" | Type of the object: always `product_variant`. | | `id` | string | Unique identifier (starts with `pvar_`). | | `product_id` | string | Product the variant belongs to. | | `sku` | string | Your SKU (unique in your account). Order items can reference it. | | `catalog_variant_id` | string | Catalog variant that is produced for this SKU. | | `title` | string | Variant title (e.g. `Black / M`). | | `options` | map of string | Option values (e.g. `{"Color": "Black", "Size": "M"}`). | | `retail_price` | integer \| null | Your retail price (informational), cents. | | `price` | integer \| null | Your wholesale price per unit with this product's designs, cents. | | `currency` | "usd" | Three-letter currency code, lowercase (`usd`). | | `external_id` | string \| null | Your id for the variant (e.g. your store's variant id). | | `enabled` | boolean | Disabled variants cannot be ordered (their SKU stays reserved). | | `available` | boolean | Whether the variant can be ordered right now: enabled, product not archived, and the catalog variant offered and in stock. `false` while it is temporarily out of stock — mark it sold out in your store; `product.updated` fires when this changes. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`variant_unavailable`](https://swaglists.com/docs/errors.md#variant_unavailable) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`product_archived`](https://swaglists.com/docs/errors.md#product_archived), [`sku_taken`](https://swaglists.com/docs/errors.md#sku_taken) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X PATCH https://swaglists.com/api/v1/products/prod_01k8x3z7r1t3y5u7i9o1p3a5s7/variants/pvar_01k8x3z7r4f6g8h0j2k4l6z8x0 \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "retail_price": 3100, "enabled": true }' ``` **Example response** ```json { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ``` ### Retrieve a product variant `GET /product_variants/{id}` Requires scope `products:read`. **Scope:** `products:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Product variant id. 1–300 characters. | **Returns** `200` — The variant. [ProductVariant](https://swaglists.com/docs/objects.md#product_variant) | Field | Type | Description | | --- | --- | --- | | `object` | "product_variant" | Type of the object: always `product_variant`. | | `id` | string | Unique identifier (starts with `pvar_`). | | `product_id` | string | Product the variant belongs to. | | `sku` | string | Your SKU (unique in your account). Order items can reference it. | | `catalog_variant_id` | string | Catalog variant that is produced for this SKU. | | `title` | string | Variant title (e.g. `Black / M`). | | `options` | map of string | Option values (e.g. `{"Color": "Black", "Size": "M"}`). | | `retail_price` | integer \| null | Your retail price (informational), cents. | | `price` | integer \| null | Your wholesale price per unit with this product's designs, cents. | | `currency` | "usd" | Three-letter currency code, lowercase (`usd`). | | `external_id` | string \| null | Your id for the variant (e.g. your store's variant id). | | `enabled` | boolean | Disabled variants cannot be ordered (their SKU stays reserved). | | `available` | boolean | Whether the variant can be ordered right now: enabled, product not archived, and the catalog variant offered and in stock. `false` while it is temporarily out of stock — mark it sold out in your store; `product.updated` fires when this changes. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/product_variants/pvar_01k8x3z7r4f6g8h0j2k4l6z8x0 \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ``` ## Shipping Shipping options and prices for a destination. ### Get shipping rates `POST /shipping/rates` Shipping options and prices for a destination and a set of items (identified exactly like order items). Use it in your checkout; the price of the method you pick is what the order will charge. Nothing is created. Requires scope `orders:read`. **Scope:** `orders:read` · **Idempotency:** send an `Idempotency-Key` header **Request body** (`application/json`) — ShippingRatesRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `recipient` | ShippingRatesRecipient | yes | — | | `recipient.country_code` | string | yes | ISO 3166-1 alpha-2 country code. Pattern `^[A-Za-z]{2}$`. | | `recipient.state_code` | string \| null | no | State, province or region — preferably the ISO 3166-2 subdivision code without the country prefix (e.g. CA, NY, ON, NSW; "US-CA" also works). Full names (e.g. "British Columbia", "New South Wales") are converted to the code for the US, Canada and Australia; for other countries the value is kept as sent. An empty string means not set. Max 64 characters. | | `recipient.postal_code` | string \| null | no | Max 20 characters. | | `items` | array of OrderItemInput | yes | 1–100 items. | | `items[].product_variant_id` | string | no | One of your product variants. 1–100 characters. | | `items[].sku` | string | no | Or your product variant SKU. 1–64 characters. | | `items[].catalog_variant_id` | string | no | Or a catalog variant with inline `designs` (on-the-fly personalization). 1–100 characters. | | `items[].designs` | array of DesignInput | no | Required with catalog_variant_id. 1–20 items. | | `items[].designs[].placement` | string | yes | Placement key from the catalog product (e.g. front, back). 1–64 characters. | | `items[].designs[].layers` | array of DesignLayerInput | yes | 1–20 items. | | `items[].designs[].layers[].file_id` | string | no | Id of an uploaded artwork file. 1–100 characters. | | `items[].designs[].layers[].url` | string | no | Or a public image URL; we download it into a new artwork file. Max 2048 characters. Must be an http(s) URL. | | `items[].designs[].layers[].position` | Position | no | Where the artwork sits inside the placement's print area. Fractions of the print area (0–1; values outside allow intentional bleed). Omit to center the artwork and fit it inside the area. | | `items[].designs[].layers[].position.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `items[].designs[].layers[].position.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `items[].designs[].layers[].position.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `items[].designs[].layers[].position.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `items[].designs[].layers[].position.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `items[].designs[].layers[].transform` | Position | no | Alias of `position` (as returned on products and orders). | | `items[].designs[].layers[].transform.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `items[].designs[].layers[].transform.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `items[].designs[].layers[].transform.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `items[].designs[].layers[].transform.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `items[].designs[].layers[].transform.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `items[].designs[].layers[].opacity` | number | no | 0–1 (default 1). Range 0–1. | | `items[].quantity` | integer | yes | Units of this item (at most 1000 per line). Range 1–1000. | | `items[].external_line_id` | string \| null | no | Your line item id. 1–128 characters. | | `items[].metadata` | map of string | no | Up to 50 key/value pairs (keys ≤ 40 chars, string values ≤ 500 chars) for your own references. Never store sensitive data here. | | `items[].retail_price` | integer \| null | no | What YOUR customer paid per unit (cents). Printed on international packing slips and declared for customs instead of our price. Default: the product variant's (or product's) retail price. Range 0–100000000. | **Returns** `200` — Available shipping methods. [ShippingRateList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [ShippingRate](https://swaglists.com/docs/objects.md#shipping_rate) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`variant_unavailable`](https://swaglists.com/docs/errors.md#variant_unavailable), [`variant_disabled`](https://swaglists.com/docs/errors.md#variant_disabled), [`product_archived`](https://swaglists.com/docs/errors.md#product_archived), [`product_has_no_designs`](https://swaglists.com/docs/errors.md#product_has_no_designs), [`placement_required`](https://swaglists.com/docs/errors.md#placement_required), [`unknown_placement`](https://swaglists.com/docs/errors.md#unknown_placement), [`file_not_artwork`](https://swaglists.com/docs/errors.md#file_not_artwork), [`file_not_ready`](https://swaglists.com/docs/errors.md#file_not_ready), [`file_not_image`](https://swaglists.com/docs/errors.md#file_not_image), [`design_not_visible`](https://swaglists.com/docs/errors.md#design_not_visible), [`artwork_resolution_too_low`](https://swaglists.com/docs/errors.md#artwork_resolution_too_low), [`item_unavailable`](https://swaglists.com/docs/errors.md#item_unavailable) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/shipping/rates \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "recipient": { "country_code": "US", "state_code": "CA", "postal_code": "94107" }, "items": [ { "sku": "CA-TEE-BLK-M", "quantity": 2 } ] }' ``` **Example response** ```json { "object": "list", "data": [ { "object": "shipping_rate", "method": "standard", "name": "Standard", "price": 475, "currency": "usd", "min_days": 3, "max_days": 5, "estimated_delivery_days": { "min": 5, "max": 9 } } ], "has_more": false, "url": "/api/v1/shipping/rates" } ``` ## Orders Create, estimate, confirm and cancel orders; track their status and shipments. ### Estimate an order `POST /orders/estimate` Prices an order exactly like POST /orders would (items, shipping, tax, delivery estimate, whether your wallet covers it) without creating anything. Requires scope `orders:read`. **Scope:** `orders:read` · **Idempotency:** send an `Idempotency-Key` header **Request body** (`application/json`) — CreateOrderRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `external_id` | string \| null | no | Your order id; unique per mode. Retrieve with GET /orders/ext:. Up to 128 printable ASCII characters. 1–128 characters. Pattern `^[\x20-\x7e]+$`. | | `recipient` | RecipientInput | yes | — | | `recipient.name` | string | yes | 1–100 characters. | | `recipient.company` | string \| null | no | Max 100 characters. | | `recipient.address1` | string | yes | 1–150 characters. | | `recipient.address2` | string \| null | no | Max 150 characters. | | `recipient.city` | string | yes | 1–100 characters. | | `recipient.state_code` | string \| null | no | State, province or region — preferably the ISO 3166-2 subdivision code without the country prefix (e.g. CA, NY, ON, NSW; "US-CA" also works). Full names (e.g. "British Columbia", "New South Wales") are converted to the code for the US, Canada and Australia; for other countries the value is kept as sent. An empty string means not set. Required for the US, Canada and Australia; for the US and Canada it must be a valid state/province code or name. Max 64 characters. | | `recipient.postal_code` | string \| null | no | Postal / ZIP code. Required unless the destination country has no postal codes (e.g. HK, AE). Max 20 characters. | | `recipient.country_code` | string | yes | ISO 3166-1 alpha-2 country code. Pattern `^[A-Za-z]{2}$`. | | `recipient.phone` | string \| null | no | Max 40 characters. | | `recipient.tax_id` | string \| null | no | Recipient tax id required by customs for some destinations, e.g. the CPF/CNPJ for Brazil. Write-only (never returned). Max 40 characters. | | `recipient.email` | string \| null | no | Used for delivery notifications (if you enable them). An empty string means no email. Max 254 characters. Email address. | | `shipping_method` | string | no | Default standard. One of: `economy`, `standard`, `express`, `priority`. | | `items` | array of OrderItemInput | yes | 1–100 items. | | `items[].product_variant_id` | string | no | One of your product variants. 1–100 characters. | | `items[].sku` | string | no | Or your product variant SKU. 1–64 characters. | | `items[].catalog_variant_id` | string | no | Or a catalog variant with inline `designs` (on-the-fly personalization). 1–100 characters. | | `items[].designs` | array of DesignInput | no | Required with catalog_variant_id. 1–20 items. | | `items[].designs[].placement` | string | yes | Placement key from the catalog product (e.g. front, back). 1–64 characters. | | `items[].designs[].layers` | array of DesignLayerInput | yes | 1–20 items. | | `items[].designs[].layers[].file_id` | string | no | Id of an uploaded artwork file. 1–100 characters. | | `items[].designs[].layers[].url` | string | no | Or a public image URL; we download it into a new artwork file. Max 2048 characters. Must be an http(s) URL. | | `items[].designs[].layers[].position` | Position | no | Where the artwork sits inside the placement's print area. Fractions of the print area (0–1; values outside allow intentional bleed). Omit to center the artwork and fit it inside the area. | | `items[].designs[].layers[].position.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `items[].designs[].layers[].position.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `items[].designs[].layers[].position.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `items[].designs[].layers[].position.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `items[].designs[].layers[].position.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `items[].designs[].layers[].transform` | Position | no | Alias of `position` (as returned on products and orders). | | `items[].designs[].layers[].transform.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `items[].designs[].layers[].transform.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `items[].designs[].layers[].transform.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `items[].designs[].layers[].transform.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `items[].designs[].layers[].transform.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `items[].designs[].layers[].opacity` | number | no | 0–1 (default 1). Range 0–1. | | `items[].quantity` | integer | yes | Units of this item (at most 1000 per line). Range 1–1000. | | `items[].external_line_id` | string \| null | no | Your line item id. 1–128 characters. | | `items[].metadata` | map of string | no | Up to 50 key/value pairs (keys ≤ 40 chars, string values ≤ 500 chars) for your own references. Never store sensitive data here. | | `items[].retail_price` | integer \| null | no | What YOUR customer paid per unit (cents). Printed on international packing slips and declared for customs instead of our price. Default: the product variant's (or product's) retail price. Range 0–100000000. | | `packing_slip` | PackingSlipInput | no | — | | `packing_slip.message` | string \| null | no | Max 500 characters. | | `packing_slip.brand_name` | string \| null | no | Overrides your account brand name on the packing slip. Max 100 characters. | | `packing_slip.logo_file_id` | string \| null | no | An uploaded logo/artwork file to print instead of your account logo. 1–100 characters. | | `packing_slip.customer_order_number` | string \| null | no | Max 64 characters. | | `gift_message` | string \| null | no | Max 500 characters. | | `customer_note` | string \| null | no | Max 1000 characters. | | `metadata` | map of string | no | Up to 50 key/value pairs (keys ≤ 40 chars, string values ≤ 500 chars) for your own references. Never store sensitive data here. | | `confirm` | boolean | no | true: reserve credits and start production now; false: create a draft. Default: your account's auto-confirm setting. | **Returns** `200` — The estimate. [OrderEstimate](https://swaglists.com/docs/objects.md#order_estimate) | Field | Type | Description | | --- | --- | --- | | `object` | "order_estimate" | Type of the object: always `order_estimate`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `shipping_method` | string | Shipping method the estimate was priced with. One of: `economy`, `standard`, `express`, `priority`. | | `items` | array of object | Priced items, in the order you sent them. | | `subtotal` | integer | Items total in cents. | | `shipping` | integer | Shipping price in cents. | | `tax` | integer | Tax in cents. | | `total` | integer | Exactly what POST /orders would charge. | | `available_shipping_methods` | array of [ShippingRate](https://swaglists.com/docs/objects.md#shipping_rate) | Every shipping method you could choose, with its price. | | `estimated_delivery_days` | object | Business days from order to delivery, including production. | | `wallet_available` | integer | Available credits (cents). | | `sufficient_funds` | boolean | Whether your available credits cover `total`. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`variant_unavailable`](https://swaglists.com/docs/errors.md#variant_unavailable), [`variant_disabled`](https://swaglists.com/docs/errors.md#variant_disabled), [`product_archived`](https://swaglists.com/docs/errors.md#product_archived), [`product_has_no_designs`](https://swaglists.com/docs/errors.md#product_has_no_designs), [`placement_required`](https://swaglists.com/docs/errors.md#placement_required), [`unknown_placement`](https://swaglists.com/docs/errors.md#unknown_placement), [`file_not_artwork`](https://swaglists.com/docs/errors.md#file_not_artwork), [`file_not_ready`](https://swaglists.com/docs/errors.md#file_not_ready), [`file_not_image`](https://swaglists.com/docs/errors.md#file_not_image), [`design_not_visible`](https://swaglists.com/docs/errors.md#design_not_visible), [`artwork_resolution_too_low`](https://swaglists.com/docs/errors.md#artwork_resolution_too_low), [`item_unavailable`](https://swaglists.com/docs/errors.md#item_unavailable), [`shipping_method_unavailable`](https://swaglists.com/docs/errors.md#shipping_method_unavailable), [`variant_unpriceable`](https://swaglists.com/docs/errors.md#variant_unpriceable) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/orders/estimate \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "external_id": "1042", "recipient": { "name": "Jane Doe", "address1": "123 Market St", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "email": "jane@example.com" }, "shipping_method": "standard", "items": [ { "sku": "CA-TEE-BLK-M", "quantity": 2, "external_line_id": "line-1" } ], "packing_slip": { "message": "Thanks for shopping with Company A!", "customer_order_number": "#1042" }, "confirm": true }' ``` **Example response** ```json { "object": "order_estimate", "livemode": true, "currency": "usd", "shipping_method": "standard", "items": [ { "index": 0, "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "unit_price": 1299, "total_price": 2598 } ], "subtotal": 2598, "shipping": 700, "tax": 0, "total": 3298, "available_shipping_methods": [ { "object": "shipping_rate", "method": "standard", "name": "Standard", "price": 475, "currency": "usd", "min_days": 3, "max_days": 5, "estimated_delivery_days": { "min": 5, "max": 9 } } ], "estimated_delivery_days": { "min": 5, "max": 9 }, "wallet_available": 482550, "sufficient_funds": true } ``` ### List orders `GET /orders` Orders of the key's mode, newest first. Requires scope `orders:read`. **Scope:** `orders:read` **Query parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Number of objects to return, 1–100 (default 20). Range 1–100. | | `starting_after` | string | no | Cursor for the next page: the id of the last object of the previous page. 1–255 characters. | | `ending_before` | string | no | Cursor for the previous page: the id of the first object of the current page. 1–255 characters. | | `status` | string or array of string | no | Filter by status; comma-separated or repeated (draft, on_hold, processing, in_production, partially_shipped, shipped, delivered, canceled, failed). | | `external_id` | string | no | Exact match on your order id. 1–128 characters. | | `q` | string | no | Search order number, external id, recipient name or email. 1–200 characters. | | `created[gte]` | string | no | Created at or after this time. Unix seconds or milliseconds, an ISO-8601 date (2026-10-01 = midnight UTC) or an ISO-8601 date-time (with Z or an offset; without an offset it is UTC). Years 2000–2100. | | `created[lte]` | string | no | Created at or before this time. Unix seconds or milliseconds, an ISO-8601 date (2026-10-01 = midnight UTC) or an ISO-8601 date-time (with Z or an offset; without an offset it is UTC). Years 2000–2100. | **Returns** `200` — A page of orders. [OrderList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [Order](https://swaglists.com/docs/objects.md#order) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl "https://swaglists.com/api/v1/orders?limit=3" \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } ], "has_more": false, "url": "/api/v1/orders" } ``` ### Create an order `POST /orders` Creates an order for your end customer. Items reference your product variants (`product_variant_id` or `sku`) or a catalog variant with inline `designs`. With `confirm` (default: your account setting) the order total is reserved from your wallet and production starts; without enough credits the order waits `on_hold` (hold_reason insufficient_funds) — or, if your account rejects such orders, you get 402 insufficient_funds and nothing is created. `external_id` is unique per mode. Always send an Idempotency-Key. Requires scope `orders:write`. **Scope:** `orders:write` · **Idempotency:** send an `Idempotency-Key` header **Request body** (`application/json`) — CreateOrderRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `external_id` | string \| null | no | Your order id; unique per mode. Retrieve with GET /orders/ext:. Up to 128 printable ASCII characters. 1–128 characters. Pattern `^[\x20-\x7e]+$`. | | `recipient` | RecipientInput | yes | — | | `recipient.name` | string | yes | 1–100 characters. | | `recipient.company` | string \| null | no | Max 100 characters. | | `recipient.address1` | string | yes | 1–150 characters. | | `recipient.address2` | string \| null | no | Max 150 characters. | | `recipient.city` | string | yes | 1–100 characters. | | `recipient.state_code` | string \| null | no | State, province or region — preferably the ISO 3166-2 subdivision code without the country prefix (e.g. CA, NY, ON, NSW; "US-CA" also works). Full names (e.g. "British Columbia", "New South Wales") are converted to the code for the US, Canada and Australia; for other countries the value is kept as sent. An empty string means not set. Required for the US, Canada and Australia; for the US and Canada it must be a valid state/province code or name. Max 64 characters. | | `recipient.postal_code` | string \| null | no | Postal / ZIP code. Required unless the destination country has no postal codes (e.g. HK, AE). Max 20 characters. | | `recipient.country_code` | string | yes | ISO 3166-1 alpha-2 country code. Pattern `^[A-Za-z]{2}$`. | | `recipient.phone` | string \| null | no | Max 40 characters. | | `recipient.tax_id` | string \| null | no | Recipient tax id required by customs for some destinations, e.g. the CPF/CNPJ for Brazil. Write-only (never returned). Max 40 characters. | | `recipient.email` | string \| null | no | Used for delivery notifications (if you enable them). An empty string means no email. Max 254 characters. Email address. | | `shipping_method` | string | no | Default standard. One of: `economy`, `standard`, `express`, `priority`. | | `items` | array of OrderItemInput | yes | 1–100 items. | | `items[].product_variant_id` | string | no | One of your product variants. 1–100 characters. | | `items[].sku` | string | no | Or your product variant SKU. 1–64 characters. | | `items[].catalog_variant_id` | string | no | Or a catalog variant with inline `designs` (on-the-fly personalization). 1–100 characters. | | `items[].designs` | array of DesignInput | no | Required with catalog_variant_id. 1–20 items. | | `items[].designs[].placement` | string | yes | Placement key from the catalog product (e.g. front, back). 1–64 characters. | | `items[].designs[].layers` | array of DesignLayerInput | yes | 1–20 items. | | `items[].designs[].layers[].file_id` | string | no | Id of an uploaded artwork file. 1–100 characters. | | `items[].designs[].layers[].url` | string | no | Or a public image URL; we download it into a new artwork file. Max 2048 characters. Must be an http(s) URL. | | `items[].designs[].layers[].position` | Position | no | Where the artwork sits inside the placement's print area. Fractions of the print area (0–1; values outside allow intentional bleed). Omit to center the artwork and fit it inside the area. | | `items[].designs[].layers[].position.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `items[].designs[].layers[].position.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `items[].designs[].layers[].position.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `items[].designs[].layers[].position.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `items[].designs[].layers[].position.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `items[].designs[].layers[].transform` | Position | no | Alias of `position` (as returned on products and orders). | | `items[].designs[].layers[].transform.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `items[].designs[].layers[].transform.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `items[].designs[].layers[].transform.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `items[].designs[].layers[].transform.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `items[].designs[].layers[].transform.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `items[].designs[].layers[].opacity` | number | no | 0–1 (default 1). Range 0–1. | | `items[].quantity` | integer | yes | Units of this item (at most 1000 per line). Range 1–1000. | | `items[].external_line_id` | string \| null | no | Your line item id. 1–128 characters. | | `items[].metadata` | map of string | no | Up to 50 key/value pairs (keys ≤ 40 chars, string values ≤ 500 chars) for your own references. Never store sensitive data here. | | `items[].retail_price` | integer \| null | no | What YOUR customer paid per unit (cents). Printed on international packing slips and declared for customs instead of our price. Default: the product variant's (or product's) retail price. Range 0–100000000. | | `packing_slip` | PackingSlipInput | no | — | | `packing_slip.message` | string \| null | no | Max 500 characters. | | `packing_slip.brand_name` | string \| null | no | Overrides your account brand name on the packing slip. Max 100 characters. | | `packing_slip.logo_file_id` | string \| null | no | An uploaded logo/artwork file to print instead of your account logo. 1–100 characters. | | `packing_slip.customer_order_number` | string \| null | no | Max 64 characters. | | `gift_message` | string \| null | no | Max 500 characters. | | `customer_note` | string \| null | no | Max 1000 characters. | | `metadata` | map of string | no | Up to 50 key/value pairs (keys ≤ 40 chars, string values ≤ 500 chars) for your own references. Never store sensitive data here. | | `confirm` | boolean | no | true: reserve credits and start production now; false: create a draft. Default: your account's auto-confirm setting. | **Returns** `201` — The created order. [Order](https://swaglists.com/docs/objects.md#order) | Field | Type | Description | | --- | --- | --- | | `object` | "order" | Type of the object: always `order`. | | `id` | string | Unique identifier (starts with `ord_`). | | `number` | integer | Sequential human-friendly order number. | | `external_id` | string \| null | Your order id (unique per mode). Look the order up with `GET /orders/ext:`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `status` | string | draft → processing → in_production → (partially_)shipped → delivered; on_hold (see hold_reason), canceled, failed. One of: `draft`, `on_hold`, `processing`, `in_production`, `partially_shipped`, `shipped`, `delivered`, `canceled`, `failed`. | | `hold_reason` | string \| null | Why the order is on hold; only set while `status` is `on_hold`. One of: `insufficient_funds`, `manual_review`, `artwork_issue`, `address_issue`, `fulfillment_issue`. | | `funds_status` | string | State of the credits for this order: `none` (draft), `awaiting` (on hold for credits), `held` (reserved), `partially_captured`/`captured` (spent), `released` (returned unspent), `partially_refunded`/`refunded`. One of: `none`, `awaiting`, `held`, `partially_captured`, `captured`, `released`, `partially_refunded`, `refunded`. | | `source` | string | Where the order was created. One of: `api`, `dashboard`, `import`, `admin`. | | `recipient` | [Recipient](https://swaglists.com/docs/objects.md#recipient) | Shipping address of the end customer. | | `shipping_method` | string | Shipping speed of the order. One of: `economy`, `standard`, `express`, `priority`. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `subtotal` | integer | Items total in cents. | | `shipping` | integer | Shipping price in cents. | | `tax` | integer | Tax in cents (0 unless tax applies to the destination). | | `discount` | integer | Discount in cents. | | `total` | integer | Credits charged for this order (cents). | | `refunded` | integer | Credits refunded to your wallet for this order, in cents. | | `items` | array of [OrderItem](https://swaglists.com/docs/objects.md#order_item) | Line items of the order. | | `shipments` | array of [Shipment](https://swaglists.com/docs/objects.md#shipment) | Parcels shipped so far (an order can ship in several parcels). | | `tracking_page_url` | string \| null | Your branded tracking page for the end customer. null while the order is a draft (the page exists once it is confirmed). | | `packing_slip` | object \| null | Packing slip overrides of this order (null when none were sent). | | `gift_message` | string \| null | Gift message included with the parcel. | | `customer_note` | string \| null | Note from your customer. | | `metadata` | map of string | Your key/value pairs (string values). | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). | | `updated_at` | string \| null | When the object was last changed (ISO-8601, UTC). | | `placed_at` | string \| null | When the order was confirmed (credits reserved, or the order put on hold for funds). | | `in_production_at` | string \| null | When production started. | | `shipped_at` | string \| null | When the order shipped. | | `delivered_at` | string \| null | When every shipment was delivered. | | `canceled_at` | string \| null | When the order was canceled. | | `cancel_reason` | string \| null | Reason given when the order was canceled. | | `failed_at` | string \| null | When the order failed. | | `failure_reason` | string \| null | Why the order failed (reserved credits were released). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`variant_unavailable`](https://swaglists.com/docs/errors.md#variant_unavailable), [`variant_disabled`](https://swaglists.com/docs/errors.md#variant_disabled), [`product_archived`](https://swaglists.com/docs/errors.md#product_archived), [`product_has_no_designs`](https://swaglists.com/docs/errors.md#product_has_no_designs), [`placement_required`](https://swaglists.com/docs/errors.md#placement_required), [`unknown_placement`](https://swaglists.com/docs/errors.md#unknown_placement), [`file_not_artwork`](https://swaglists.com/docs/errors.md#file_not_artwork), [`file_not_ready`](https://swaglists.com/docs/errors.md#file_not_ready), [`file_not_image`](https://swaglists.com/docs/errors.md#file_not_image), [`design_not_visible`](https://swaglists.com/docs/errors.md#design_not_visible), [`artwork_resolution_too_low`](https://swaglists.com/docs/errors.md#artwork_resolution_too_low), [`file_download_failed`](https://swaglists.com/docs/errors.md#file_download_failed), [`download_failed`](https://swaglists.com/docs/errors.md#download_failed), [`download_not_an_image`](https://swaglists.com/docs/errors.md#download_not_an_image), [`download_timeout`](https://swaglists.com/docs/errors.md#download_timeout), [`file_too_large`](https://swaglists.com/docs/errors.md#file_too_large), [`file_invalid`](https://swaglists.com/docs/errors.md#file_invalid), [`file_unsupported_type`](https://swaglists.com/docs/errors.md#file_unsupported_type), [`file_unsafe`](https://swaglists.com/docs/errors.md#file_unsafe), [`item_unavailable`](https://swaglists.com/docs/errors.md#item_unavailable), [`shipping_method_unavailable`](https://swaglists.com/docs/errors.md#shipping_method_unavailable), [`variant_unpriceable`](https://swaglists.com/docs/errors.md#variant_unpriceable) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 402 | [`insufficient_funds`](https://swaglists.com/docs/errors.md#insufficient_funds) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`duplicate_external_id`](https://swaglists.com/docs/errors.md#duplicate_external_id) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/orders \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "external_id": "1042", "recipient": { "name": "Jane Doe", "address1": "123 Market St", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "email": "jane@example.com" }, "shipping_method": "standard", "items": [ { "sku": "CA-TEE-BLK-M", "quantity": 2, "external_line_id": "line-1" } ], "packing_slip": { "message": "Thanks for shopping with Company A!", "customer_order_number": "#1042" }, "confirm": true }' ``` **Example response** ```json { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } ``` ### Retrieve an order `GET /orders/{id}` Look up by id or by your own id with the "ext:" prefix (GET /orders/ext:1042). Requires scope `orders:read`. **Scope:** `orders:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Order id, or "ext:" + your external_id (e.g. ext:1042). 1–300 characters. | **Returns** `200` — The order. [Order](https://swaglists.com/docs/objects.md#order) | Field | Type | Description | | --- | --- | --- | | `object` | "order" | Type of the object: always `order`. | | `id` | string | Unique identifier (starts with `ord_`). | | `number` | integer | Sequential human-friendly order number. | | `external_id` | string \| null | Your order id (unique per mode). Look the order up with `GET /orders/ext:`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `status` | string | draft → processing → in_production → (partially_)shipped → delivered; on_hold (see hold_reason), canceled, failed. One of: `draft`, `on_hold`, `processing`, `in_production`, `partially_shipped`, `shipped`, `delivered`, `canceled`, `failed`. | | `hold_reason` | string \| null | Why the order is on hold; only set while `status` is `on_hold`. One of: `insufficient_funds`, `manual_review`, `artwork_issue`, `address_issue`, `fulfillment_issue`. | | `funds_status` | string | State of the credits for this order: `none` (draft), `awaiting` (on hold for credits), `held` (reserved), `partially_captured`/`captured` (spent), `released` (returned unspent), `partially_refunded`/`refunded`. One of: `none`, `awaiting`, `held`, `partially_captured`, `captured`, `released`, `partially_refunded`, `refunded`. | | `source` | string | Where the order was created. One of: `api`, `dashboard`, `import`, `admin`. | | `recipient` | [Recipient](https://swaglists.com/docs/objects.md#recipient) | Shipping address of the end customer. | | `shipping_method` | string | Shipping speed of the order. One of: `economy`, `standard`, `express`, `priority`. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `subtotal` | integer | Items total in cents. | | `shipping` | integer | Shipping price in cents. | | `tax` | integer | Tax in cents (0 unless tax applies to the destination). | | `discount` | integer | Discount in cents. | | `total` | integer | Credits charged for this order (cents). | | `refunded` | integer | Credits refunded to your wallet for this order, in cents. | | `items` | array of [OrderItem](https://swaglists.com/docs/objects.md#order_item) | Line items of the order. | | `shipments` | array of [Shipment](https://swaglists.com/docs/objects.md#shipment) | Parcels shipped so far (an order can ship in several parcels). | | `tracking_page_url` | string \| null | Your branded tracking page for the end customer. null while the order is a draft (the page exists once it is confirmed). | | `packing_slip` | object \| null | Packing slip overrides of this order (null when none were sent). | | `gift_message` | string \| null | Gift message included with the parcel. | | `customer_note` | string \| null | Note from your customer. | | `metadata` | map of string | Your key/value pairs (string values). | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). | | `updated_at` | string \| null | When the object was last changed (ISO-8601, UTC). | | `placed_at` | string \| null | When the order was confirmed (credits reserved, or the order put on hold for funds). | | `in_production_at` | string \| null | When production started. | | `shipped_at` | string \| null | When the order shipped. | | `delivered_at` | string \| null | When every shipment was delivered. | | `canceled_at` | string \| null | When the order was canceled. | | `cancel_reason` | string \| null | Reason given when the order was canceled. | | `failed_at` | string \| null | When the order failed. | | `failure_reason` | string \| null | Why the order failed (reserved credits were released). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } ``` ### Update a draft order `PATCH /orders/{id}` Changes a draft order (recipient, items, shipping method, …). Confirmed orders cannot be changed (409). Requires scope `orders:write`. **Scope:** `orders:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Order id, or "ext:" + your external_id (e.g. ext:1042). 1–300 characters. | **Request body** (`application/json`) — UpdateOrderRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `external_id` | string \| null | no | Your order id; unique per mode. Retrieve with GET /orders/ext:. Up to 128 printable ASCII characters. 1–128 characters. Pattern `^[\x20-\x7e]+$`. | | `recipient` | RecipientInput | no | — | | `recipient.name` | string | yes | 1–100 characters. | | `recipient.company` | string \| null | no | Max 100 characters. | | `recipient.address1` | string | yes | 1–150 characters. | | `recipient.address2` | string \| null | no | Max 150 characters. | | `recipient.city` | string | yes | 1–100 characters. | | `recipient.state_code` | string \| null | no | State, province or region — preferably the ISO 3166-2 subdivision code without the country prefix (e.g. CA, NY, ON, NSW; "US-CA" also works). Full names (e.g. "British Columbia", "New South Wales") are converted to the code for the US, Canada and Australia; for other countries the value is kept as sent. An empty string means not set. Required for the US, Canada and Australia; for the US and Canada it must be a valid state/province code or name. Max 64 characters. | | `recipient.postal_code` | string \| null | no | Postal / ZIP code. Required unless the destination country has no postal codes (e.g. HK, AE). Max 20 characters. | | `recipient.country_code` | string | yes | ISO 3166-1 alpha-2 country code. Pattern `^[A-Za-z]{2}$`. | | `recipient.phone` | string \| null | no | Max 40 characters. | | `recipient.tax_id` | string \| null | no | Recipient tax id required by customs for some destinations, e.g. the CPF/CNPJ for Brazil. Write-only (never returned). Max 40 characters. | | `recipient.email` | string \| null | no | Used for delivery notifications (if you enable them). An empty string means no email. Max 254 characters. Email address. | | `shipping_method` | string | no | One of: `economy`, `standard`, `express`, `priority`. | | `items` | array of OrderItemInput | no | Replaces all items. 1–100 items. | | `items[].product_variant_id` | string | no | One of your product variants. 1–100 characters. | | `items[].sku` | string | no | Or your product variant SKU. 1–64 characters. | | `items[].catalog_variant_id` | string | no | Or a catalog variant with inline `designs` (on-the-fly personalization). 1–100 characters. | | `items[].designs` | array of DesignInput | no | Required with catalog_variant_id. 1–20 items. | | `items[].designs[].placement` | string | yes | Placement key from the catalog product (e.g. front, back). 1–64 characters. | | `items[].designs[].layers` | array of DesignLayerInput | yes | 1–20 items. | | `items[].designs[].layers[].file_id` | string | no | Id of an uploaded artwork file. 1–100 characters. | | `items[].designs[].layers[].url` | string | no | Or a public image URL; we download it into a new artwork file. Max 2048 characters. Must be an http(s) URL. | | `items[].designs[].layers[].position` | Position | no | Where the artwork sits inside the placement's print area. Fractions of the print area (0–1; values outside allow intentional bleed). Omit to center the artwork and fit it inside the area. | | `items[].designs[].layers[].position.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `items[].designs[].layers[].position.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `items[].designs[].layers[].position.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `items[].designs[].layers[].position.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `items[].designs[].layers[].position.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `items[].designs[].layers[].transform` | Position | no | Alias of `position` (as returned on products and orders). | | `items[].designs[].layers[].transform.x` | number | yes | Left edge of the artwork box as a fraction of the print area width. Range -10–10. | | `items[].designs[].layers[].transform.y` | number | yes | Top edge of the artwork box as a fraction of the print area height. Range -10–10. | | `items[].designs[].layers[].transform.width` | number | yes | Artwork width as a fraction of the print area width. Range >0–10. | | `items[].designs[].layers[].transform.height` | number | yes | Artwork height as a fraction of the print area height. Range >0–10. | | `items[].designs[].layers[].transform.rotation` | number | no | Clockwise rotation in degrees around the artwork center. Range -3600–3600. Default `0`. | | `items[].designs[].layers[].opacity` | number | no | 0–1 (default 1). Range 0–1. | | `items[].quantity` | integer | yes | Units of this item (at most 1000 per line). Range 1–1000. | | `items[].external_line_id` | string \| null | no | Your line item id. 1–128 characters. | | `items[].metadata` | map of string | no | Up to 50 key/value pairs (keys ≤ 40 chars, string values ≤ 500 chars) for your own references. Never store sensitive data here. | | `items[].retail_price` | integer \| null | no | What YOUR customer paid per unit (cents). Printed on international packing slips and declared for customs instead of our price. Default: the product variant's (or product's) retail price. Range 0–100000000. | | `packing_slip` | PackingSlipInput | no | — | | `packing_slip.message` | string \| null | no | Max 500 characters. | | `packing_slip.brand_name` | string \| null | no | Overrides your account brand name on the packing slip. Max 100 characters. | | `packing_slip.logo_file_id` | string \| null | no | An uploaded logo/artwork file to print instead of your account logo. 1–100 characters. | | `packing_slip.customer_order_number` | string \| null | no | Max 64 characters. | | `gift_message` | string \| null | no | Max 500 characters. | | `customer_note` | string \| null | no | Max 1000 characters. | | `metadata` | map of string | no | Up to 50 key/value pairs (keys ≤ 40 chars, string values ≤ 500 chars) for your own references. Never store sensitive data here. | **Returns** `200` — The updated order. [Order](https://swaglists.com/docs/objects.md#order) | Field | Type | Description | | --- | --- | --- | | `object` | "order" | Type of the object: always `order`. | | `id` | string | Unique identifier (starts with `ord_`). | | `number` | integer | Sequential human-friendly order number. | | `external_id` | string \| null | Your order id (unique per mode). Look the order up with `GET /orders/ext:`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `status` | string | draft → processing → in_production → (partially_)shipped → delivered; on_hold (see hold_reason), canceled, failed. One of: `draft`, `on_hold`, `processing`, `in_production`, `partially_shipped`, `shipped`, `delivered`, `canceled`, `failed`. | | `hold_reason` | string \| null | Why the order is on hold; only set while `status` is `on_hold`. One of: `insufficient_funds`, `manual_review`, `artwork_issue`, `address_issue`, `fulfillment_issue`. | | `funds_status` | string | State of the credits for this order: `none` (draft), `awaiting` (on hold for credits), `held` (reserved), `partially_captured`/`captured` (spent), `released` (returned unspent), `partially_refunded`/`refunded`. One of: `none`, `awaiting`, `held`, `partially_captured`, `captured`, `released`, `partially_refunded`, `refunded`. | | `source` | string | Where the order was created. One of: `api`, `dashboard`, `import`, `admin`. | | `recipient` | [Recipient](https://swaglists.com/docs/objects.md#recipient) | Shipping address of the end customer. | | `shipping_method` | string | Shipping speed of the order. One of: `economy`, `standard`, `express`, `priority`. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `subtotal` | integer | Items total in cents. | | `shipping` | integer | Shipping price in cents. | | `tax` | integer | Tax in cents (0 unless tax applies to the destination). | | `discount` | integer | Discount in cents. | | `total` | integer | Credits charged for this order (cents). | | `refunded` | integer | Credits refunded to your wallet for this order, in cents. | | `items` | array of [OrderItem](https://swaglists.com/docs/objects.md#order_item) | Line items of the order. | | `shipments` | array of [Shipment](https://swaglists.com/docs/objects.md#shipment) | Parcels shipped so far (an order can ship in several parcels). | | `tracking_page_url` | string \| null | Your branded tracking page for the end customer. null while the order is a draft (the page exists once it is confirmed). | | `packing_slip` | object \| null | Packing slip overrides of this order (null when none were sent). | | `gift_message` | string \| null | Gift message included with the parcel. | | `customer_note` | string \| null | Note from your customer. | | `metadata` | map of string | Your key/value pairs (string values). | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). | | `updated_at` | string \| null | When the object was last changed (ISO-8601, UTC). | | `placed_at` | string \| null | When the order was confirmed (credits reserved, or the order put on hold for funds). | | `in_production_at` | string \| null | When production started. | | `shipped_at` | string \| null | When the order shipped. | | `delivered_at` | string \| null | When every shipment was delivered. | | `canceled_at` | string \| null | When the order was canceled. | | `cancel_reason` | string \| null | Reason given when the order was canceled. | | `failed_at` | string \| null | When the order failed. | | `failure_reason` | string \| null | Why the order failed (reserved credits were released). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`variant_unavailable`](https://swaglists.com/docs/errors.md#variant_unavailable), [`variant_disabled`](https://swaglists.com/docs/errors.md#variant_disabled), [`product_archived`](https://swaglists.com/docs/errors.md#product_archived), [`product_has_no_designs`](https://swaglists.com/docs/errors.md#product_has_no_designs), [`placement_required`](https://swaglists.com/docs/errors.md#placement_required), [`unknown_placement`](https://swaglists.com/docs/errors.md#unknown_placement), [`file_not_artwork`](https://swaglists.com/docs/errors.md#file_not_artwork), [`file_not_ready`](https://swaglists.com/docs/errors.md#file_not_ready), [`file_not_image`](https://swaglists.com/docs/errors.md#file_not_image), [`design_not_visible`](https://swaglists.com/docs/errors.md#design_not_visible), [`artwork_resolution_too_low`](https://swaglists.com/docs/errors.md#artwork_resolution_too_low), [`file_download_failed`](https://swaglists.com/docs/errors.md#file_download_failed), [`download_failed`](https://swaglists.com/docs/errors.md#download_failed), [`download_not_an_image`](https://swaglists.com/docs/errors.md#download_not_an_image), [`download_timeout`](https://swaglists.com/docs/errors.md#download_timeout), [`file_too_large`](https://swaglists.com/docs/errors.md#file_too_large), [`file_invalid`](https://swaglists.com/docs/errors.md#file_invalid), [`file_unsupported_type`](https://swaglists.com/docs/errors.md#file_unsupported_type), [`file_unsafe`](https://swaglists.com/docs/errors.md#file_unsafe), [`item_unavailable`](https://swaglists.com/docs/errors.md#item_unavailable), [`shipping_method_unavailable`](https://swaglists.com/docs/errors.md#shipping_method_unavailable), [`variant_unpriceable`](https://swaglists.com/docs/errors.md#variant_unpriceable) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`order_not_editable`](https://swaglists.com/docs/errors.md#order_not_editable), [`duplicate_external_id`](https://swaglists.com/docs/errors.md#duplicate_external_id) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X PATCH https://swaglists.com/api/v1/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "shipping_method": "express" }' ``` **Example response** ```json { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } ``` ### Confirm a draft, or release an order waiting for credits `POST /orders/{id}/confirm` Draft: reserves the order total from your wallet and starts production (or puts the order on hold for funds). Order on hold for insufficient credits (`hold_reason: insufficient_funds`): reserves its total now and releases it to production — answers 402 `insufficient_funds` (order unchanged) if your available credits still don't cover it. Use this to release orders when your account uses manual release (`insufficient_funds_behavior: hold_manual`). Nothing is ever sent to production without its credits reserved. Requires scope `orders:write`. **Scope:** `orders:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Order id, or "ext:" + your external_id (e.g. ext:1042). 1–300 characters. | **Returns** `200` — The confirmed order. [Order](https://swaglists.com/docs/objects.md#order) | Field | Type | Description | | --- | --- | --- | | `object` | "order" | Type of the object: always `order`. | | `id` | string | Unique identifier (starts with `ord_`). | | `number` | integer | Sequential human-friendly order number. | | `external_id` | string \| null | Your order id (unique per mode). Look the order up with `GET /orders/ext:`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `status` | string | draft → processing → in_production → (partially_)shipped → delivered; on_hold (see hold_reason), canceled, failed. One of: `draft`, `on_hold`, `processing`, `in_production`, `partially_shipped`, `shipped`, `delivered`, `canceled`, `failed`. | | `hold_reason` | string \| null | Why the order is on hold; only set while `status` is `on_hold`. One of: `insufficient_funds`, `manual_review`, `artwork_issue`, `address_issue`, `fulfillment_issue`. | | `funds_status` | string | State of the credits for this order: `none` (draft), `awaiting` (on hold for credits), `held` (reserved), `partially_captured`/`captured` (spent), `released` (returned unspent), `partially_refunded`/`refunded`. One of: `none`, `awaiting`, `held`, `partially_captured`, `captured`, `released`, `partially_refunded`, `refunded`. | | `source` | string | Where the order was created. One of: `api`, `dashboard`, `import`, `admin`. | | `recipient` | [Recipient](https://swaglists.com/docs/objects.md#recipient) | Shipping address of the end customer. | | `shipping_method` | string | Shipping speed of the order. One of: `economy`, `standard`, `express`, `priority`. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `subtotal` | integer | Items total in cents. | | `shipping` | integer | Shipping price in cents. | | `tax` | integer | Tax in cents (0 unless tax applies to the destination). | | `discount` | integer | Discount in cents. | | `total` | integer | Credits charged for this order (cents). | | `refunded` | integer | Credits refunded to your wallet for this order, in cents. | | `items` | array of [OrderItem](https://swaglists.com/docs/objects.md#order_item) | Line items of the order. | | `shipments` | array of [Shipment](https://swaglists.com/docs/objects.md#shipment) | Parcels shipped so far (an order can ship in several parcels). | | `tracking_page_url` | string \| null | Your branded tracking page for the end customer. null while the order is a draft (the page exists once it is confirmed). | | `packing_slip` | object \| null | Packing slip overrides of this order (null when none were sent). | | `gift_message` | string \| null | Gift message included with the parcel. | | `customer_note` | string \| null | Note from your customer. | | `metadata` | map of string | Your key/value pairs (string values). | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). | | `updated_at` | string \| null | When the object was last changed (ISO-8601, UTC). | | `placed_at` | string \| null | When the order was confirmed (credits reserved, or the order put on hold for funds). | | `in_production_at` | string \| null | When production started. | | `shipped_at` | string \| null | When the order shipped. | | `delivered_at` | string \| null | When every shipment was delivered. | | `canceled_at` | string \| null | When the order was canceled. | | `cancel_reason` | string \| null | Reason given when the order was canceled. | | `failed_at` | string \| null | When the order failed. | | `failure_reason` | string \| null | Why the order failed (reserved credits were released). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`item_unavailable`](https://swaglists.com/docs/errors.md#item_unavailable) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 402 | [`insufficient_funds`](https://swaglists.com/docs/errors.md#insufficient_funds) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`order_not_draft`](https://swaglists.com/docs/errors.md#order_not_draft), [`order_not_confirmable`](https://swaglists.com/docs/errors.md#order_not_confirmable) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa/confirm \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` **Example response** ```json { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } ``` ### Cancel an order `POST /orders/{id}/cancel` Cancels an order that has not gone into production yet. Reserved credits are released and captured credits refunded to your wallet. Orders in production or shipped cannot be canceled (409). Requires scope `orders:write`. **Scope:** `orders:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Order id, or "ext:" + your external_id (e.g. ext:1042). 1–300 characters. | **Request body** (`application/json`, optional) — CancelOrderRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `reason` | string \| null | no | 1–500 characters. | **Returns** `200` — The canceled order. [Order](https://swaglists.com/docs/objects.md#order) | Field | Type | Description | | --- | --- | --- | | `object` | "order" | Type of the object: always `order`. | | `id` | string | Unique identifier (starts with `ord_`). | | `number` | integer | Sequential human-friendly order number. | | `external_id` | string \| null | Your order id (unique per mode). Look the order up with `GET /orders/ext:`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `status` | string | draft → processing → in_production → (partially_)shipped → delivered; on_hold (see hold_reason), canceled, failed. One of: `draft`, `on_hold`, `processing`, `in_production`, `partially_shipped`, `shipped`, `delivered`, `canceled`, `failed`. | | `hold_reason` | string \| null | Why the order is on hold; only set while `status` is `on_hold`. One of: `insufficient_funds`, `manual_review`, `artwork_issue`, `address_issue`, `fulfillment_issue`. | | `funds_status` | string | State of the credits for this order: `none` (draft), `awaiting` (on hold for credits), `held` (reserved), `partially_captured`/`captured` (spent), `released` (returned unspent), `partially_refunded`/`refunded`. One of: `none`, `awaiting`, `held`, `partially_captured`, `captured`, `released`, `partially_refunded`, `refunded`. | | `source` | string | Where the order was created. One of: `api`, `dashboard`, `import`, `admin`. | | `recipient` | [Recipient](https://swaglists.com/docs/objects.md#recipient) | Shipping address of the end customer. | | `shipping_method` | string | Shipping speed of the order. One of: `economy`, `standard`, `express`, `priority`. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `subtotal` | integer | Items total in cents. | | `shipping` | integer | Shipping price in cents. | | `tax` | integer | Tax in cents (0 unless tax applies to the destination). | | `discount` | integer | Discount in cents. | | `total` | integer | Credits charged for this order (cents). | | `refunded` | integer | Credits refunded to your wallet for this order, in cents. | | `items` | array of [OrderItem](https://swaglists.com/docs/objects.md#order_item) | Line items of the order. | | `shipments` | array of [Shipment](https://swaglists.com/docs/objects.md#shipment) | Parcels shipped so far (an order can ship in several parcels). | | `tracking_page_url` | string \| null | Your branded tracking page for the end customer. null while the order is a draft (the page exists once it is confirmed). | | `packing_slip` | object \| null | Packing slip overrides of this order (null when none were sent). | | `gift_message` | string \| null | Gift message included with the parcel. | | `customer_note` | string \| null | Note from your customer. | | `metadata` | map of string | Your key/value pairs (string values). | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). | | `updated_at` | string \| null | When the object was last changed (ISO-8601, UTC). | | `placed_at` | string \| null | When the order was confirmed (credits reserved, or the order put on hold for funds). | | `in_production_at` | string \| null | When production started. | | `shipped_at` | string \| null | When the order shipped. | | `delivered_at` | string \| null | When every shipment was delivered. | | `canceled_at` | string \| null | When the order was canceled. | | `cancel_reason` | string \| null | Reason given when the order was canceled. | | `failed_at` | string \| null | When the order failed. | | `failure_reason` | string \| null | Why the order failed (reserved credits were released). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`order_not_cancelable`](https://swaglists.com/docs/errors.md#order_not_cancelable) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa/cancel \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "reason": "Customer changed their mind" }' ``` **Example response** ```json { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } ``` ### List order events `GET /orders/{id}/events` The order timeline (created, confirmed, in production, shipped, …), oldest first. Requires scope `orders:read`. **Scope:** `orders:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Order id, or "ext:" + your external_id (e.g. ext:1042). 1–300 characters. | **Returns** `200` — Order events. [OrderEventList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [OrderEvent](https://swaglists.com/docs/objects.md#order_event) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa/events \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "object": "order_event", "id": "oev_01k8x4b2qc5v7b9n1m3k5j7h9g", "type": "status_changed", "message": "Order is in production", "from_status": "processing", "to_status": "in_production", "created_at": "2026-10-21T14:32:05.000Z" } ], "has_more": false, "url": "/api/v1/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa/events" } ``` ## Wallet Prepaid credits: balance, transactions and top-ups. ### Retrieve the wallet `GET /wallet` Credit balance of the key's mode (test mode has its own wallet with free test credits). Requires scope `wallet:read`. **Scope:** `wallet:read` **Returns** `200` — The wallet. [Wallet](https://swaglists.com/docs/objects.md#wallet) | Field | Type | Description | | --- | --- | --- | | `object` | "wallet" | Type of the object: always `wallet`. | | `mode` | string | `live` or `test`. One of: `live`, `test`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `available` | integer | Spendable credits in cents. May be negative after a payment dispute. | | `held` | integer | Credits reserved for in-flight orders, in cents. | | `total` | integer | available + held, in cents. | | `low_balance_threshold` | integer \| null | Low-balance notification threshold in cents. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/wallet \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "wallet", "mode": "live", "livemode": true, "currency": "usd", "available": 482550, "held": 12480, "total": 495030, "low_balance_threshold": 10000 } ``` ### List wallet transactions `GET /wallet/transactions` Every balance change (top-ups, bonuses, holds, captures, refunds, adjustments), newest first. Requires scope `wallet:read`. **Scope:** `wallet:read` **Query parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Number of objects to return, 1–100 (default 20). Range 1–100. | | `starting_after` | string | no | Cursor for the next page: the id of the last object of the previous page. 1–255 characters. | | `ending_before` | string | no | Cursor for the previous page: the id of the first object of the current page. 1–255 characters. | | `type` | string | no | Filter by transaction type. One of: `topup`, `bonus`, `hold`, `release`, `capture`, `charge`, `refund`, `adjustment`, `reversal`, `test_credit`. | | `order_id` | string | no | Only transactions of this order. 1–100 characters. | **Returns** `200` — A page of transactions. [TransactionList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [Transaction](https://swaglists.com/docs/objects.md#transaction) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl "https://swaglists.com/api/v1/wallet/transactions?limit=3" \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "object": "transaction", "id": "txn_01k8x4b2q8w0e2r4t6y8u0i2o4", "type": "hold", "amount": -2598, "held_delta": 2598, "balance_after": 479952, "currency": "usd", "description": "Credits reserved for order ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "topup_id": null, "created_at": "2026-10-21T14:32:05.000Z" } ], "has_more": false, "url": "/api/v1/wallet/transactions" } ``` ### List top-ups `GET /topups` Credit purchases (made in the dashboard), newest first. Top-ups are live-mode only; test keys get an empty list. Requires scope `wallet:read`. **Scope:** `wallet:read` **Query parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Number of objects to return, 1–100 (default 20). Range 1–100. | | `starting_after` | string | no | Cursor for the next page: the id of the last object of the previous page. 1–255 characters. | | `ending_before` | string | no | Cursor for the previous page: the id of the first object of the current page. 1–255 characters. | **Returns** `200` — A page of top-ups. [TopupList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [Topup](https://swaglists.com/docs/objects.md#topup) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl "https://swaglists.com/api/v1/topups?limit=3" \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "object": "topup", "id": "top_01k8x1q3w5e7r9t1y3u5i7o9p1", "status": "succeeded", "amount": 500000, "bonus": 25000, "bonus_percent": 5, "credits": 525000, "currency": "usd", "payment_method_type": "us_bank_account", "receipt_url": null, "created_at": "2026-10-21T14:32:05.000Z", "succeeded_at": "2026-10-21T14:32:05.000Z" } ], "has_more": false, "url": "/api/v1/topups" } ``` ### Retrieve a top-up `GET /topups/{id}` Requires scope `wallet:read`. **Scope:** `wallet:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Top-up id. 1–300 characters. | **Returns** `200` — The top-up. [Topup](https://swaglists.com/docs/objects.md#topup) | Field | Type | Description | | --- | --- | --- | | `object` | "topup" | Type of the object: always `topup`. | | `id` | string | Unique identifier (starts with `top_`). | | `status` | string | Payment status of the top-up. One of: `pending`, `processing`, `succeeded`, `failed`, `canceled`, `refunded`, `partially_refunded`, `disputed`. | | `amount` | integer | Amount paid (cents). | | `bonus` | integer | Bonus credits granted (cents). | | `bonus_percent` | number | Bonus percentage, e.g. 5 for 5%. | | `credits` | integer | Credits added = amount + bonus (cents). | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `payment_method_type` | string \| null | How it was paid (e.g. `card`, `us_bank_account`). | | `receipt_url` | string \| null | Payment receipt, when available. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `succeeded_at` | string \| null | When the credits were added. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/topups/top_01k8x1q3w5e7r9t1y3u5i7o9p1 \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "topup", "id": "top_01k8x1q3w5e7r9t1y3u5i7o9p1", "status": "succeeded", "amount": 500000, "bonus": 25000, "bonus_percent": 5, "credits": 525000, "currency": "usd", "payment_method_type": "us_bank_account", "receipt_url": null, "created_at": "2026-10-21T14:32:05.000Z", "succeeded_at": "2026-10-21T14:32:05.000Z" } ``` ## Webhooks Endpoints we notify about events (Standard Webhooks signatures). ### List webhook endpoints `GET /webhook_endpoints` Endpoints of the key's mode, newest first. Requires scope `webhooks:read`. **Scope:** `webhooks:read` **Query parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Number of objects to return, 1–100 (default 20). Range 1–100. | | `starting_after` | string | no | Cursor for the next page: the id of the last object of the previous page. 1–255 characters. | | `ending_before` | string | no | Cursor for the previous page: the id of the first object of the current page. 1–255 characters. | | `status` | string | no | Filter by status. One of: `enabled`, `disabled`. | **Returns** `200` — A page of endpoints. [WebhookEndpointList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [WebhookEndpoint](https://swaglists.com/docs/objects.md#webhook_endpoint) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl "https://swaglists.com/api/v1/webhook_endpoints?limit=3" \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "object": "webhook_endpoint", "id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "url": "https://company-a.example/webhooks/print", "description": "Production order updates", "enabled_events": [ "order.shipped", "order.delivered" ], "status": "enabled", "disabled_reason": null, "mode": "live", "livemode": true, "created_at": "2026-10-21T14:32:05.000Z" } ], "has_more": false, "url": "/api/v1/webhook_endpoints" } ``` ### Create a webhook endpoint `POST /webhook_endpoints` Registers a URL for events of the key's mode. Live endpoints must use HTTPS on a public host. The response contains the signing `secret` (whsec_…) — this is the only time it is shown. Requires scope `webhooks:write`. **Scope:** `webhooks:write` · **Idempotency:** send an `Idempotency-Key` header **Request body** (`application/json`) — CreateWebhookEndpointRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | yes | HTTPS URL (test mode also allows http). 1–2048 characters. | | `enabled_events` | array of string | no | Event types to receive, or ["*"] for all. Available: order.created, order.updated, order.confirmed, order.on_hold, order.processing, order.in_production, order.partially_shipped, order.shipped, order.delivered, order.canceled, order.failed, order.refunded, shipment.created, shipment.updated, shipment.delivered, product.created, product.updated, product.deleted, catalog.price_changed, wallet.credited, wallet.low_balance, topup.succeeded, topup.failed, file.processed. 1–200 items. | | `description` | string \| null | no | Max 500 characters. | **Returns** `201` — The endpoint, including its secret. [WebhookEndpoint](https://swaglists.com/docs/objects.md#webhook_endpoint) | Field | Type | Description | | --- | --- | --- | | `object` | "webhook_endpoint" | Type of the object: always `webhook_endpoint`. | | `id` | string | Unique identifier (starts with `we_`). | | `url` | string | URL we POST events to. | | `description` | string \| null | Your description. | | `enabled_events` | array of string | Event types delivered to this endpoint; ["*"] = all. | | `status` | string | `enabled` endpoints receive events. One of: `enabled`, `disabled`. | | `disabled_reason` | string \| null | Why the endpoint was disabled: `disabled_by_user`, `failing_deliveries` (every delivery failed for 5 days) or `endpoint_gone` (it answered HTTP 410). | | `mode` | string | `live` or `test`. One of: `live`, `test`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `secret` | string | Signing secret (whsec_…). Only returned when the endpoint is created and when the secret is rotated — store it safely. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid), [`webhook_endpoint_limit_reached`](https://swaglists.com/docs/errors.md#webhook_endpoint_limit_reached) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/webhook_endpoints \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "url": "https://company-a.example/webhooks/print", "enabled_events": [ "order.shipped", "order.delivered" ], "description": "Production order updates" }' ``` **Example response** ```json { "object": "webhook_endpoint", "id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "url": "https://company-a.example/webhooks/print", "description": "Production order updates", "enabled_events": [ "order.shipped", "order.delivered" ], "status": "enabled", "disabled_reason": null, "mode": "live", "livemode": true, "created_at": "2026-10-21T14:32:05.000Z" } ``` ### Retrieve a webhook endpoint `GET /webhook_endpoints/{id}` Requires scope `webhooks:read`. **Scope:** `webhooks:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Webhook endpoint id. 1–300 characters. | **Returns** `200` — The endpoint (without its secret). [WebhookEndpoint](https://swaglists.com/docs/objects.md#webhook_endpoint) | Field | Type | Description | | --- | --- | --- | | `object` | "webhook_endpoint" | Type of the object: always `webhook_endpoint`. | | `id` | string | Unique identifier (starts with `we_`). | | `url` | string | URL we POST events to. | | `description` | string \| null | Your description. | | `enabled_events` | array of string | Event types delivered to this endpoint; ["*"] = all. | | `status` | string | `enabled` endpoints receive events. One of: `enabled`, `disabled`. | | `disabled_reason` | string \| null | Why the endpoint was disabled: `disabled_by_user`, `failing_deliveries` (every delivery failed for 5 days) or `endpoint_gone` (it answered HTTP 410). | | `mode` | string | `live` or `test`. One of: `live`, `test`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `secret` | string | Signing secret (whsec_…). Only returned when the endpoint is created and when the secret is rotated — store it safely. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/webhook_endpoints/we_01k8x2m4n6b8v0c2x4z6a8s0d2 \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "webhook_endpoint", "id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "url": "https://company-a.example/webhooks/print", "description": "Production order updates", "enabled_events": [ "order.shipped", "order.delivered" ], "status": "enabled", "disabled_reason": null, "mode": "live", "livemode": true, "created_at": "2026-10-21T14:32:05.000Z" } ``` ### Update a webhook endpoint `PATCH /webhook_endpoints/{id}` Change the URL, subscribed events or description, or disable/re-enable the endpoint. Requires scope `webhooks:write`. **Scope:** `webhooks:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Webhook endpoint id. 1–300 characters. | **Request body** (`application/json`) — UpdateWebhookEndpointRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | no | 1–2048 characters. | | `enabled_events` | array of string | no | Event types to receive, or ["*"] for all. Available: order.created, order.updated, order.confirmed, order.on_hold, order.processing, order.in_production, order.partially_shipped, order.shipped, order.delivered, order.canceled, order.failed, order.refunded, shipment.created, shipment.updated, shipment.delivered, product.created, product.updated, product.deleted, catalog.price_changed, wallet.credited, wallet.low_balance, topup.succeeded, topup.failed, file.processed. 1–200 items. | | `description` | string \| null | no | Max 500 characters. | | `status` | string | no | Disable to pause deliveries; re-enable to resume. One of: `enabled`, `disabled`. | **Returns** `200` — The updated endpoint. [WebhookEndpoint](https://swaglists.com/docs/objects.md#webhook_endpoint) | Field | Type | Description | | --- | --- | --- | | `object` | "webhook_endpoint" | Type of the object: always `webhook_endpoint`. | | `id` | string | Unique identifier (starts with `we_`). | | `url` | string | URL we POST events to. | | `description` | string \| null | Your description. | | `enabled_events` | array of string | Event types delivered to this endpoint; ["*"] = all. | | `status` | string | `enabled` endpoints receive events. One of: `enabled`, `disabled`. | | `disabled_reason` | string \| null | Why the endpoint was disabled: `disabled_by_user`, `failing_deliveries` (every delivery failed for 5 days) or `endpoint_gone` (it answered HTTP 410). | | `mode` | string | `live` or `test`. One of: `live`, `test`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `secret` | string | Signing secret (whsec_…). Only returned when the endpoint is created and when the secret is rotated — store it safely. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X PATCH https://swaglists.com/api/v1/webhook_endpoints/we_01k8x2m4n6b8v0c2x4z6a8s0d2 \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "enabled_events": [ "*" ], "status": "enabled" }' ``` **Example response** ```json { "object": "webhook_endpoint", "id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "url": "https://company-a.example/webhooks/print", "description": "Production order updates", "enabled_events": [ "order.shipped", "order.delivered" ], "status": "enabled", "disabled_reason": null, "mode": "live", "livemode": true, "created_at": "2026-10-21T14:32:05.000Z" } ``` ### Delete a webhook endpoint `DELETE /webhook_endpoints/{id}` Permanently deletes the endpoint and its delivery history (events remain listable). Requires scope `webhooks:write`. **Scope:** `webhooks:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Webhook endpoint id. 1–300 characters. | **Returns** `200` — Deletion confirmation. DeletedWebhookEndpoint | Field | Type | Description | | --- | --- | --- | | `id` | string | Unique identifier (starts with `we_`). | | `object` | "webhook_endpoint" | Type of the object: always `webhook_endpoint`. | | `deleted` | boolean | — | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X DELETE https://swaglists.com/api/v1/webhook_endpoints/we_01k8x2m4n6b8v0c2x4z6a8s0d2 \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` **Example response** ```json { "id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "object": "webhook_endpoint", "deleted": true } ``` ### Rotate the signing secret `POST /webhook_endpoints/{id}/rotate_secret` Issues a new signing secret (returned once). With `expire_old_in_seconds`, deliveries are signed with both secrets until the old one expires so you can roll without downtime. Requires scope `webhooks:write`. **Scope:** `webhooks:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Webhook endpoint id. 1–300 characters. | **Request body** (`application/json`, optional) — RotateWebhookSecretRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `expire_old_in_seconds` | integer | no | Keep signing with the old secret as well for this many seconds (0–86400, default 0). Range 0–86400. | **Returns** `200` — The endpoint with its new secret. [WebhookEndpoint](https://swaglists.com/docs/objects.md#webhook_endpoint) | Field | Type | Description | | --- | --- | --- | | `object` | "webhook_endpoint" | Type of the object: always `webhook_endpoint`. | | `id` | string | Unique identifier (starts with `we_`). | | `url` | string | URL we POST events to. | | `description` | string \| null | Your description. | | `enabled_events` | array of string | Event types delivered to this endpoint; ["*"] = all. | | `status` | string | `enabled` endpoints receive events. One of: `enabled`, `disabled`. | | `disabled_reason` | string \| null | Why the endpoint was disabled: `disabled_by_user`, `failing_deliveries` (every delivery failed for 5 days) or `endpoint_gone` (it answered HTTP 410). | | `mode` | string | `live` or `test`. One of: `live`, `test`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `secret` | string | Signing secret (whsec_…). Only returned when the endpoint is created and when the secret is rotated — store it safely. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/webhook_endpoints/we_01k8x2m4n6b8v0c2x4z6a8s0d2/rotate_secret \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "expire_old_in_seconds": 3600 }' ``` **Example response** ```json { "object": "webhook_endpoint", "id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "url": "https://company-a.example/webhooks/print", "description": "Production order updates", "enabled_events": [ "order.shipped", "order.delivered" ], "status": "enabled", "disabled_reason": null, "mode": "live", "livemode": true, "created_at": "2026-10-21T14:32:05.000Z" } ``` ### Send a test event `POST /webhook_endpoints/{id}/test` Sends a `webhook_endpoint.test` event (data.object = the endpoint) to this endpoint only — use it to verify your signature check. Requires scope `webhooks:write`. **Scope:** `webhooks:write` · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Webhook endpoint id. 1–300 characters. | **Returns** `200` — The test event and its delivery. WebhookTestResult | Field | Type | Description | | --- | --- | --- | | `object` | "webhook_test" | Type of the object: always `webhook_test`. | | `event` | [Event](https://swaglists.com/docs/objects.md#event) | Something that happened in your account. Also the exact body POSTed to webhook endpoints. | | `delivery` | [WebhookDelivery](https://swaglists.com/docs/objects.md#webhook_delivery) | — | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`webhook_endpoint_disabled`](https://swaglists.com/docs/errors.md#webhook_endpoint_disabled) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/webhook_endpoints/we_01k8x2m4n6b8v0c2x4z6a8s0d2/test \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` **Example response** ```json { "object": "webhook_test", "event": { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "webhook_endpoint.test", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": true, "data": { "object": { "object": "webhook_endpoint", "id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "url": "https://company-a.example/webhooks/print", "description": "Production order updates", "enabled_events": [ "order.shipped", "order.delivered" ], "status": "enabled", "disabled_reason": null, "mode": "live", "livemode": true, "created_at": "2026-10-21T14:32:05.000Z" } } }, "delivery": { "object": "webhook_delivery", "id": "whd_01k8x4b2r3e5w7q9a1s3d5f7g9", "event_id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "endpoint_id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "status": "pending", "attempts": 0, "response_status": null, "next_attempt_at": "2026-10-21T14:32:05.000Z", "last_attempt_at": null, "created_at": "2026-10-21T14:32:05.000Z" } } ``` ## Events Everything that happened in your account; the same objects delivered to webhooks. ### List events `GET /events` Events of the key's mode from the last 30 days, newest first — the same objects delivered to webhook endpoints. Filter by `type` (exact, or a prefix wildcard such as `order.*`). Requires scope `webhooks:read`. **Scope:** `webhooks:read` **Query parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | Number of objects to return, 1–100 (default 20). Range 1–100. | | `starting_after` | string | no | Cursor for the next page: the id of the last object of the previous page. 1–255 characters. | | `ending_before` | string | no | Cursor for the previous page: the id of the first object of the current page. 1–255 characters. | | `type` | string | no | Event type or wildcard, e.g. "order.shipped" or "order.*". 1–100 characters. | | `related_object_id` | string | no | Only events about this object (e.g. an order id). 1–100 characters. | | `created[gte]` | string | no | Created at or after this time. Unix seconds or milliseconds, an ISO-8601 date (2026-10-01 = midnight UTC) or an ISO-8601 date-time (with Z or an offset; without an offset it is UTC). Years 2000–2100. | | `created[lte]` | string | no | Created at or before this time. Unix seconds or milliseconds, an ISO-8601 date (2026-10-01 = midnight UTC) or an ISO-8601 date-time (with Z or an offset; without an offset it is UTC). Years 2000–2100. | **Returns** `200` — A page of events. [EventList](https://swaglists.com/docs/objects.md#list) | Field | Type | Description | | --- | --- | --- | | `object` | "list" | Always `list`. | | `data` | array of [Event](https://swaglists.com/docs/objects.md#event) | The objects of this page. | | `has_more` | boolean | Whether more items exist beyond this page (in the paging direction). | | `url` | string | Path of this list endpoint. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl "https://swaglists.com/api/v1/events?limit=3" \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "object": "list", "data": [ { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.shipped", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": true, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } } } ], "has_more": false, "url": "/api/v1/events" } ``` ### Retrieve an event `GET /events/{id}` Requires scope `webhooks:read`. **Scope:** `webhooks:read` **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Event id. 1–300 characters. | **Returns** `200` — The event. [Event](https://swaglists.com/docs/objects.md#event) | Field | Type | Description | | --- | --- | --- | | `id` | string | Event id. Also sent as the `webhook-id` header and stable across retries — deduplicate on it. | | `object` | "event" | Type of the object: always `event`. | | `type` | string | Event type (e.g. `order.shipped`). | | `api_version` | string | API version the payload was rendered with. | | `created_at` | string | When the event happened (ISO-8601, UTC). | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `data` | map of any | `object` is the resource as of the event (same shape as the API returns); `previous_attributes` lists changed fields on *.updated events. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl https://swaglists.com/api/v1/events/evt_01k8x4b2r1t3y5u7i9o1p3a5s7 \ -H "Authorization: Bearer $API_KEY" ``` **Example response** ```json { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.shipped", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": true, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } } } ``` ## Testing Test-mode helpers: add test credits, advance simulated production. ### Add test credits `POST /test/wallet/fund` Test mode only: adds free test credits to the test wallet (orders waiting for funds resume automatically). Requires a full-access test key. **Scope:** full access (`*`) · **Test mode only** · **Idempotency:** send an `Idempotency-Key` header **Request body** (`application/json`, optional) — FundTestWalletRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `amount` | integer | no | Test credits to add, in cents (default: the platform's test grant, e.g. 100000 = $1,000). Minimum >0. | **Returns** `200` — The test wallet after the grant. [Wallet](https://swaglists.com/docs/objects.md#wallet) | Field | Type | Description | | --- | --- | --- | | `object` | "wallet" | Type of the object: always `wallet`. | | `mode` | string | `live` or `test`. One of: `live`, `test`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `available` | integer | Spendable credits in cents. May be negative after a payment dispute. | | `held` | integer | Credits reserved for in-flight orders, in cents. | | `total` | integer | available + held, in cents. | | `low_balance_threshold` | integer \| null | Low-balance notification threshold in cents. | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended), [`test_mode_only`](https://swaglists.com/docs/errors.md#test_mode_only) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/test/wallet/fund \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "amount": 100000 }' ``` **Example response** ```json { "object": "wallet", "mode": "live", "livemode": true, "currency": "usd", "available": 482550, "held": 12480, "total": 495030, "low_balance_threshold": 10000 } ``` ### Advance a test order `POST /test/orders/{id}/advance` Test mode only: moves the simulated production of a test order one step forward (in production → shipped with tracking → delivered), emitting the same events and webhooks as real orders. Call repeatedly to reach `delivered`. With `outcome` it simulates a problem instead: a production failure, a partial shipment, a delivery exception or a return to sender. Later status checks of the simulator agree with every simulated step. Requires scope `orders:write`. **Scope:** `orders:write` · **Test mode only** · **Idempotency:** send an `Idempotency-Key` header **Path parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Order id or "ext:" + external_id. 1–300 characters. | **Request body** (`application/json`, optional) — AdvanceTestOrderRequest | Field | Type | Required | Description | | --- | --- | --- | --- | | `outcome` | string | no | What the simulated factory does. `next` (default): the next lifecycle step (`processing` → `in_production` → `shipped` → `delivered`, or resume an `on_hold` order). `fail_production`: production fails before shipping — the items are not produced and their test credits are returned; like a real partner failure, the order goes `on_hold` (`hold_reason: fulfillment_issue`) for our team to follow up. `partial_shipment`: about half of the units ship now (`order.partially_shipped`), the rest on the normal schedule (an order with at least two units). `delivery_exception`: the carrier reports a problem with a package in transit (`shipment.updated`, status `exception`). `return_to_sender`: a package in transit goes back to the sender (`shipment.updated`, status `returned`). An outcome that doesn't fit the order's current state returns 409 `order_not_advanceable`. One of: `next`, `fail_production`, `delivery_exception`, `return_to_sender`, `partial_shipment`. | | `expected_status` | string | no | The order status your client last saw. When the order has moved on since (the simulator advances on its own), nothing happens and the call returns 409 `order_status_changed` with the current status, so a retried or stale call never advances an order twice. One of: `draft`, `on_hold`, `processing`, `in_production`, `partially_shipped`, `shipped`, `delivered`, `canceled`, `failed`. | **Returns** `200` — The order after advancing. [Order](https://swaglists.com/docs/objects.md#order) | Field | Type | Description | | --- | --- | --- | | `object` | "order" | Type of the object: always `order`. | | `id` | string | Unique identifier (starts with `ord_`). | | `number` | integer | Sequential human-friendly order number. | | `external_id` | string \| null | Your order id (unique per mode). Look the order up with `GET /orders/ext:`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `status` | string | draft → processing → in_production → (partially_)shipped → delivered; on_hold (see hold_reason), canceled, failed. One of: `draft`, `on_hold`, `processing`, `in_production`, `partially_shipped`, `shipped`, `delivered`, `canceled`, `failed`. | | `hold_reason` | string \| null | Why the order is on hold; only set while `status` is `on_hold`. One of: `insufficient_funds`, `manual_review`, `artwork_issue`, `address_issue`, `fulfillment_issue`. | | `funds_status` | string | State of the credits for this order: `none` (draft), `awaiting` (on hold for credits), `held` (reserved), `partially_captured`/`captured` (spent), `released` (returned unspent), `partially_refunded`/`refunded`. One of: `none`, `awaiting`, `held`, `partially_captured`, `captured`, `released`, `partially_refunded`, `refunded`. | | `source` | string | Where the order was created. One of: `api`, `dashboard`, `import`, `admin`. | | `recipient` | [Recipient](https://swaglists.com/docs/objects.md#recipient) | Shipping address of the end customer. | | `shipping_method` | string | Shipping speed of the order. One of: `economy`, `standard`, `express`, `priority`. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `subtotal` | integer | Items total in cents. | | `shipping` | integer | Shipping price in cents. | | `tax` | integer | Tax in cents (0 unless tax applies to the destination). | | `discount` | integer | Discount in cents. | | `total` | integer | Credits charged for this order (cents). | | `refunded` | integer | Credits refunded to your wallet for this order, in cents. | | `items` | array of [OrderItem](https://swaglists.com/docs/objects.md#order_item) | Line items of the order. | | `shipments` | array of [Shipment](https://swaglists.com/docs/objects.md#shipment) | Parcels shipped so far (an order can ship in several parcels). | | `tracking_page_url` | string \| null | Your branded tracking page for the end customer. null while the order is a draft (the page exists once it is confirmed). | | `packing_slip` | object \| null | Packing slip overrides of this order (null when none were sent). | | `gift_message` | string \| null | Gift message included with the parcel. | | `customer_note` | string \| null | Note from your customer. | | `metadata` | map of string | Your key/value pairs (string values). | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). | | `updated_at` | string \| null | When the object was last changed (ISO-8601, UTC). | | `placed_at` | string \| null | When the order was confirmed (credits reserved, or the order put on hold for funds). | | `in_production_at` | string \| null | When production started. | | `shipped_at` | string \| null | When the order shipped. | | `delivered_at` | string \| null | When every shipment was delivered. | | `canceled_at` | string \| null | When the order was canceled. | | `cancel_reason` | string \| null | Reason given when the order was canceled. | | `failed_at` | string \| null | When the order failed. | | `failure_reason` | string \| null | Why the order failed (reserved credits were released). | **Errors** | Status | Codes | | --- | --- | | 400 | [`parameter_invalid`](https://swaglists.com/docs/errors.md#parameter_invalid), [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing), [`invalid_json`](https://swaglists.com/docs/errors.md#invalid_json), [`idempotency_key_invalid`](https://swaglists.com/docs/errors.md#idempotency_key_invalid) | | 401 | [`missing_api_key`](https://swaglists.com/docs/errors.md#missing_api_key), [`invalid_api_key`](https://swaglists.com/docs/errors.md#invalid_api_key) | | 403 | [`insufficient_scope`](https://swaglists.com/docs/errors.md#insufficient_scope), [`live_mode_disabled`](https://swaglists.com/docs/errors.md#live_mode_disabled), [`ip_not_allowed`](https://swaglists.com/docs/errors.md#ip_not_allowed), [`account_suspended`](https://swaglists.com/docs/errors.md#account_suspended), [`test_mode_only`](https://swaglists.com/docs/errors.md#test_mode_only) | | 404 | [`resource_missing`](https://swaglists.com/docs/errors.md#resource_missing) | | 409 | [`idempotency_key_reused`](https://swaglists.com/docs/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://swaglists.com/docs/errors.md#idempotency_in_progress), [`order_not_advanceable`](https://swaglists.com/docs/errors.md#order_not_advanceable), [`order_status_changed`](https://swaglists.com/docs/errors.md#order_status_changed) | | 413 | [`request_too_large`](https://swaglists.com/docs/errors.md#request_too_large) | | 415 | [`unsupported_media_type`](https://swaglists.com/docs/errors.md#unsupported_media_type) | | 429 | [`rate_limited`](https://swaglists.com/docs/errors.md#rate_limited) | | 500 | [`internal_error`](https://swaglists.com/docs/errors.md#internal_error) | **Example request** ```bash curl -X POST https://swaglists.com/api/v1/test/orders/ord_01k8x4b2q9w7e3r5t1y8u6i0pa/advance \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "outcome": "next", "expected_status": "processing" }' ``` **Example response** ```json { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } ``` --- # Error codes > Every error code the API returns — what it means, how to fix it and whether to retry. Source: https://swaglists.com/docs/errors (markdown: https://swaglists.com/docs/errors.md) Every error response has the body `{ "error": { "type", "code", "message", "param", "doc_url", "request_id" } }`; `doc_url` links to the code on this page. Branch on `code` (stable), not on `message`. How to handle errors in code: [Errors guide](https://swaglists.com/docs/error-handling.md). ## Error types | Type | HTTP status | Meaning | | --- | --- | --- | | `invalid_request_error` | 400, 409, 413, 415 | The request is invalid: a parameter is missing or wrong, or the action is not allowed in the object's current state. | | `authentication_error` | 401 | No API key, or the key is not valid. | | `permission_error` | 403 | The key is valid but not allowed to make this request (scope, mode, IP allowlist, account status). | | `not_found_error` | 404 | The object in the URL (or the URL itself) does not exist in the key's mode. | | `conflict_error` | 409 | The request conflicts with existing data (duplicate external id, SKU already used, …). | | `idempotency_error` | 409 | An Idempotency-Key was reused with different parameters, or the first request with that key is still running. | | `rate_limit_error` | 429 | Too many requests. Wait for the number of seconds in the Retry-After header. | | `insufficient_funds_error` | 402 | Your wallet does not have enough available credits (only when your account rejects instead of holding such orders). | | `api_error` | 500 | Something went wrong on our side. Retry with the same Idempotency-Key; contact support with the request id if it persists. | ## All codes | Code | Type | Status | | --- | --- | --- | | [`missing_api_key`](#missing_api_key) | `authentication_error` | 401 | | [`invalid_api_key`](#invalid_api_key) | `authentication_error` | 401 | | [`insufficient_scope`](#insufficient_scope) | `permission_error` | 403 | | [`account_suspended`](#account_suspended) | `permission_error` | 403 | | [`account_closed`](#account_closed) | `permission_error` | 403 | | [`organization_suspended`](#organization_suspended) | `permission_error` | 403 | | [`organization_inactive`](#organization_inactive) | `permission_error` | 403 | | [`live_mode_disabled`](#live_mode_disabled) | `permission_error` | 403 | | [`ip_not_allowed`](#ip_not_allowed) | `permission_error` | 403 | | [`test_mode_only`](#test_mode_only) | `permission_error` | 403 | | [`forbidden`](#forbidden) | `permission_error` | 403 | | [`parameter_invalid`](#parameter_invalid) | `invalid_request_error` | 400 | | [`parameter_missing`](#parameter_missing) | `invalid_request_error` | 400 | | [`resource_missing`](#resource_missing) | `not_found_error` | 404, 400 | | [`route_not_found`](#route_not_found) | `not_found_error` | 404 | | [`invalid_json`](#invalid_json) | `invalid_request_error` | 400 | | [`invalid_request`](#invalid_request) | `invalid_request_error` | 400 | | [`unsupported_media_type`](#unsupported_media_type) | `invalid_request_error` | 415 | | [`request_too_large`](#request_too_large) | `invalid_request_error` | 413 | | [`idempotency_key_invalid`](#idempotency_key_invalid) | `invalid_request_error` | 400 | | [`idempotency_key_reused`](#idempotency_key_reused) | `idempotency_error` | 409 | | [`idempotency_in_progress`](#idempotency_in_progress) | `idempotency_error` | 409 | | [`idempotency_conflict`](#idempotency_conflict) | `idempotency_error` | 409 | | [`conflict`](#conflict) | `conflict_error` | 409 | | [`invalid_state`](#invalid_state) | `invalid_request_error` | 409 | | [`rate_limited`](#rate_limited) | `rate_limit_error` | 429 | | [`file_invalid`](#file_invalid) | `invalid_request_error` | 400 | | [`file_too_large`](#file_too_large) | `invalid_request_error` | 400 | | [`file_unsafe`](#file_unsafe) | `invalid_request_error` | 400 | | [`file_unsupported_type`](#file_unsupported_type) | `invalid_request_error` | 400 | | [`download_failed`](#download_failed) | `invalid_request_error` | 400 | | [`download_not_an_image`](#download_not_an_image) | `invalid_request_error` | 400 | | [`download_timeout`](#download_timeout) | `invalid_request_error` | 400 | | [`file_download_failed`](#file_download_failed) | `invalid_request_error` | 400 | | [`artwork_download_budget_exceeded`](#artwork_download_budget_exceeded) | `invalid_request_error` | 400 | | [`file_in_use`](#file_in_use) | `conflict_error` | 409 | | [`file_not_artwork`](#file_not_artwork) | `invalid_request_error` | 400 | | [`file_not_ready`](#file_not_ready) | `invalid_request_error` | 400 | | [`file_not_image`](#file_not_image) | `invalid_request_error` | 400 | | [`designs_required`](#designs_required) | `invalid_request_error` | 400 | | [`unknown_placement`](#unknown_placement) | `invalid_request_error` | 400 | | [`placement_invalid`](#placement_invalid) | `invalid_request_error` | 400 | | [`duplicate_placement`](#duplicate_placement) | `invalid_request_error` | 400 | | [`placement_required`](#placement_required) | `invalid_request_error` | 400 | | [`placement_unavailable`](#placement_unavailable) | `invalid_request_error` | 400 | | [`no_layers`](#no_layers) | `invalid_request_error` | 400 | | [`too_many_layers`](#too_many_layers) | `invalid_request_error` | 400 | | [`invalid_transform`](#invalid_transform) | `invalid_request_error` | 400 | | [`invalid_opacity`](#invalid_opacity) | `invalid_request_error` | 400 | | [`design_not_visible`](#design_not_visible) | `invalid_request_error` | 400 | | [`artwork_resolution_too_low`](#artwork_resolution_too_low) | `invalid_request_error` | 400 | | [`catalog_product_unavailable`](#catalog_product_unavailable) | `invalid_request_error` | 400 | | [`catalog_variant_inactive`](#catalog_variant_inactive) | `invalid_request_error` | 400 | | [`variant_unavailable`](#variant_unavailable) | `invalid_request_error` | 400 | | [`variant_disabled`](#variant_disabled) | `invalid_request_error` | 400 | | [`product_archived`](#product_archived) | `invalid_request_error` | 400, 409 | | [`product_has_no_designs`](#product_has_no_designs) | `invalid_request_error` | 400 | | [`sku_mismatch`](#sku_mismatch) | `invalid_request_error` | 400 | | [`item_reference_required`](#item_reference_required) | `invalid_request_error` | 400 | | [`no_variants`](#no_variants) | `invalid_request_error` | 400 | | [`duplicate_variant`](#duplicate_variant) | `invalid_request_error` | 400 | | [`duplicate_sku`](#duplicate_sku) | `invalid_request_error` | 400 | | [`sku_taken`](#sku_taken) | `conflict_error` | 409 | | [`external_id_taken`](#external_id_taken) | `conflict_error` | 409 | | [`variant_exists`](#variant_exists) | `conflict_error` | 409 | | [`duplicate_external_id`](#duplicate_external_id) | `conflict_error` | 409 | | [`order_not_editable`](#order_not_editable) | `invalid_request_error` | 409 | | [`order_not_draft`](#order_not_draft) | `invalid_request_error` | 409 | | [`order_not_confirmable`](#order_not_confirmable) | `invalid_request_error` | 409 | | [`order_not_cancelable`](#order_not_cancelable) | `invalid_request_error` | 409 | | [`item_unavailable`](#item_unavailable) | `invalid_request_error` | 400 | | [`quantity_too_large`](#quantity_too_large) | `invalid_request_error` | 400 | | [`insufficient_funds`](#insufficient_funds) | `insufficient_funds_error` | 402 | | [`recipient_field_too_long`](#recipient_field_too_long) | `invalid_request_error` | 400 | | [`recipient_phone_required`](#recipient_phone_required) | `invalid_request_error` | 400 | | [`recipient_email_required`](#recipient_email_required) | `invalid_request_error` | 400 | | [`recipient_tax_id_required`](#recipient_tax_id_required) | `invalid_request_error` | 400 | | [`recipient_state_required`](#recipient_state_required) | `invalid_request_error` | 400 | | [`po_box_not_supported`](#po_box_not_supported) | `invalid_request_error` | 400 | | [`military_address_not_supported`](#military_address_not_supported) | `invalid_request_error` | 400 | | [`recipient_local_script_required`](#recipient_local_script_required) | `invalid_request_error` | 400 | | [`customization_unknown`](#customization_unknown) | `invalid_request_error` | 400 | | [`customization_invalid`](#customization_invalid) | `invalid_request_error` | 400 | | [`customization_required`](#customization_required) | `invalid_request_error` | 400 | | [`customization_not_applicable`](#customization_not_applicable) | `invalid_request_error` | 400 | | [`customization_value_hidden`](#customization_value_hidden) | `invalid_request_error` | 400 | | [`customization_unsupported`](#customization_unsupported) | `invalid_request_error` | 400 | | [`shipping_method_unavailable`](#shipping_method_unavailable) | `invalid_request_error` | 400 | | [`variant_unpriceable`](#variant_unpriceable) | `invalid_request_error` | 400 | | [`variant_product_mismatch`](#variant_product_mismatch) | `invalid_request_error` | 400 | | [`webhook_endpoint_limit_reached`](#webhook_endpoint_limit_reached) | `invalid_request_error` | 400 | | [`webhook_endpoint_disabled`](#webhook_endpoint_disabled) | `invalid_request_error` | 409 | | [`order_not_advanceable`](#order_not_advanceable) | `invalid_request_error` | 409 | | [`order_status_changed`](#order_status_changed) | `conflict_error` | 409 | | [`internal_error`](#internal_error) | `api_error` | 500, 502, 503 | ## Authentication & permissions Problems with the API key or what it is allowed to do. ### `missing_api_key` **No API key was sent.** HTTP 401 · type `authentication_error` **What it means:** The request has no `Authorization` header (or it is empty). **How to fix:** Send your secret key as a Bearer token: `Authorization: Bearer sk_test_…`. If you use a proxy or serverless platform, make sure it forwards the `Authorization` header. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `invalid_api_key` **The API key is not valid.** HTTP 401 · type `authentication_error` **What it means:** The key is unknown, revoked or expired, or the `Authorization` header is malformed. The message says which. **How to fix:** Copy the key again from **Developers → API keys** (the full secret is shown only once — create or roll a key if you lost it). Check that you did not mix up test (`sk_test_`) and live (`sk_live_`) keys and that there are no extra spaces or quotes. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `insufficient_scope` **The key lacks a required scope.** HTTP 403 · type `permission_error` **What it means:** The key was created with restricted scopes and the endpoint needs another one. `required_scope` names it. **How to fix:** Create a key that includes the scope (or full access `*`) in **Developers → API keys**. A `:write` scope includes the matching `:read` scope. **Retry:** After fixing the request. The rejected request was not stored, so the same Idempotency-Key can be reused ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `required_scope` ### `account_suspended` **Your account is suspended.** HTTP 403 · type `permission_error` **What it means:** API access is paused for this account. **How to fix:** Contact support to restore access. Nothing you send will be processed until then. **Retry:** No — retrying the same request fails again. ### `account_closed` **Your account is closed.** HTTP 403 · type `permission_error` **What it means:** The account that owns the key was closed. **How to fix:** Contact support if you believe this is a mistake. **Retry:** No — retrying the same request fails again. ### `organization_suspended` **Orders cannot be placed while the account is suspended.** HTTP 403 · type `permission_error` **What it means:** The account was suspended, so new orders are refused. **How to fix:** Contact support to restore the account. **Retry:** No — retrying the same request fails again. ### `organization_inactive` **Orders cannot be placed for a closed account.** HTTP 403 · type `permission_error` **What it means:** The account is no longer active. **How to fix:** Contact support. **Retry:** No — retrying the same request fails again. ### `live_mode_disabled` **Live mode is not enabled yet.** HTTP 403 · type `permission_error` **What it means:** A live key was used (or a live order was placed) but live mode is not enabled for the account. **How to fix:** Keep building with a test key (`sk_test_…`). To go live, verify your email address and follow the [going live checklist](https://swaglists.com/docs/going-live.md); contact support if live mode still is not available. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `ip_not_allowed` **Request IP is not on the key's allowlist.** HTTP 403 · type `permission_error` **What it means:** The key has an IP allowlist and the request came from another address. **How to fix:** Add your server's public egress IP (or CIDR range) to the key in **Developers → API keys**, or remove the allowlist. Behind NAT, a load balancer or a serverless platform the egress IP can differ from the server's own address. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `test_mode_only` **Endpoint only works with test keys.** HTTP 403 · type `permission_error` **What it means:** Test helpers (`/test/…`) cannot be called with a live key. **How to fix:** Use a test key (`sk_test_…`). **Retry:** After fixing the request. The rejected request was not stored, so the same Idempotency-Key can be reused ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `forbidden` **Not allowed.** HTTP 403 · type `permission_error` **What it means:** The action is not allowed for your role or in the current context (for example a dashboard member without the needed permission). **How to fix:** Ask an owner or admin of your account to grant a role with the permission, or to perform the action. **Retry:** No — retrying the same request fails again. ## Request validation The request itself is malformed or a parameter is wrong. Fix the request before retrying. ### `parameter_invalid` **A parameter is missing or invalid.** HTTP 400 · type `invalid_request_error` **What it means:** A parameter has the wrong type or format, is out of range, is missing, or is not a known parameter (unknown parameters are rejected). `param` names it, e.g. `items[0].quantity`, and `message` explains the rule. **How to fix:** Correct the parameter named in `param`. Compare your request with the [API reference](https://swaglists.com/docs/api-reference.md) — request bodies are strict JSON objects with snake_case fields. **Retry:** After fixing the request. The rejected request was not stored, so the same Idempotency-Key can be reused ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). Services may report this as `validation_failed` internally; the API always returns `parameter_invalid`. ### `parameter_missing` **A required parameter is missing.** HTTP 400 · type `invalid_request_error` **What it means:** A parameter required by this operation was not sent. `param` names it. **How to fix:** Add the parameter named in `param`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `resource_missing` **The object does not exist.** HTTP 404 / 400 · type `not_found_error` **What it means:** 404: the object in the URL does not exist. 400 (type `invalid_request_error`): an object referenced in the body — a `sku`, `product_variant_id`, `file_id`, `catalog_variant_id`, … — does not exist; `param` points at it. **How to fix:** Check the id. Objects belong to one mode: test objects are invisible to live keys and vice versa. Orders can also be looked up with `ext:`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `route_not_found` **Unknown URL.** HTTP 404 · type `not_found_error` **What it means:** No endpoint exists for this method and path. **How to fix:** Check the path, the HTTP method and the `/api/v1` prefix against the [API reference](https://swaglists.com/docs/api-reference.md). **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `invalid_json` **The body is not valid JSON.** HTTP 400 · type `invalid_request_error` **What it means:** The request body could not be parsed as JSON. **How to fix:** Send a JSON object with `Content-Type: application/json`. In shells, wrap the body in single quotes so variables and quotes are not mangled. **Retry:** After fixing the request. The rejected request was not stored, so the same Idempotency-Key can be reused ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `invalid_request` **The body could not be parsed.** HTTP 400 · type `invalid_request_error` **What it means:** The request body could not be read (for example a broken multipart form). **How to fix:** Send JSON, or for `POST /files` a well-formed `multipart/form-data` body with a `file` field. **Retry:** After fixing the request. The rejected request was not stored, so the same Idempotency-Key can be reused ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `unsupported_media_type` **Unsupported Content-Type.** HTTP 415 · type `invalid_request_error` **What it means:** The body uses a content type the endpoint does not accept. **How to fix:** Use `Content-Type: application/json`. Only `POST /files` also accepts `multipart/form-data`. **Retry:** After fixing the request. The rejected request was not stored, so the same Idempotency-Key can be reused ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `request_too_large` **The body is too large.** HTTP 413 · type `invalid_request_error` **What it means:** JSON bodies are limited to 2 MB; file uploads to the maximum file size plus 1 MB of form overhead. **How to fix:** Split large batches into several requests. For big artwork, upload by URL (`POST /files` with `{"url": …}`) or reduce the file size. **Retry:** After fixing the request. The rejected request was not stored, so the same Idempotency-Key can be reused ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `idempotency_key_invalid` **The Idempotency-Key header is invalid.** HTTP 400 · type `invalid_request_error` **What it means:** The header is empty, longer than 255 characters, or contains spaces or non-printable characters. **How to fix:** Use a UUID or another printable ASCII string of at most 255 characters. **Retry:** After fixing the request. The rejected request was not stored, so the same Idempotency-Key can be reused ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ## Idempotency, concurrency & rate limits Retries and concurrent requests. ### `idempotency_key_reused` **Idempotency-Key reused with different parameters.** HTTP 409 · type `idempotency_error` **What it means:** The key was already used in the last 24 hours with a different method, path or body. **How to fix:** Use a new key for every distinct request; reuse a key only to retry exactly the same request. **Retry:** No — retrying the same request fails again. ### `idempotency_in_progress` **The first request with this key is still running.** HTTP 409 · type `idempotency_error` **What it means:** A request with the same Idempotency-Key has not finished yet. **How to fix:** Wait for the `Retry-After` seconds (1) and retry with the same key and body; you will get the original response. **Retry:** Yes — retry with the same Idempotency-Key. ### `idempotency_conflict` **Operation already recorded with different values.** HTTP 409 · type `idempotency_error` **What it means:** An internal operation key was reused for a different operation (for example two different amounts for the same refund). **How to fix:** Do not retry with changed values. Contact support with the request id if you did not expect this. **Retry:** No — retrying the same request fails again. ### `conflict` **Conflict with the current state.** HTTP 409 · type `conflict_error` **What it means:** The object changed concurrently or the request conflicts with existing data. **How to fix:** Fetch the object again and retry the request if it still applies. **Retry:** Yes, later — back off and retry. ### `invalid_state` **Not allowed in the current state.** HTTP 409 · type `invalid_request_error` **What it means:** The action is not possible in the object's current state. **How to fix:** Fetch the object to check its status before retrying. **Retry:** No — retrying the same request fails again. ### `rate_limited` **Too many requests.** HTTP 429 · type `rate_limit_error` **What it means:** The key exceeded its request rate (a token bucket refilled continuously over a minute). **How to fix:** Wait for the number of seconds in `Retry-After`, then retry; add exponential backoff with jitter. See [rate limits](https://swaglists.com/docs/rate-limits.md). **Retry:** Yes, later — back off and retry. ## Files & artwork Uploading artwork and using files in designs. ### `file_invalid` **The file could not be read.** HTTP 400 · type `invalid_request_error` **What it means:** The file is empty, corrupt or truncated, or its dimensions cannot be read (for SVG: unreadable or with an absurd declared size). **How to fix:** Re-export the artwork as PNG (recommended) or JPEG and upload it again. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `file_too_large` **The file is too large.** HTTP 400 · type `invalid_request_error` **What it means:** The file exceeds the maximum upload size (100 MB by default), 30,000 px on a side, or 250 megapixels (width × height) in total — images are checked before they are decoded. **How to fix:** Reduce the file size or dimensions. 300 DPI at the print size is all you need — see the placement's `recommended_width_px`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `file_unsafe` **The SVG contains unsafe content.** HTTP 400 · type `invalid_request_error` **What it means:** SVG files must not contain scripts, event handlers or references to external resources. **How to fix:** Export a plain SVG (outlined text, embedded images) or a PNG. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `file_unsupported_type` **Unsupported file type.** HTTP 400 · type `invalid_request_error` **What it means:** Only PNG, JPEG, WebP, TIFF, GIF (first frame) and SVG images are accepted. **How to fix:** Convert the artwork to PNG. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `download_failed` **The file could not be downloaded.** HTTP 400 · type `invalid_request_error` **What it means:** We could not download the `url` (connection error, non-2xx response, more than 5 redirects or a redirect without a location). A connection reset or an HTTP 429/502/503/504 answer is retried once before this error. **How to fix:** Make sure the URL is public (no login, no signed URL that expired), returns the image itself with HTTP 200, and is reachable over http(s) from the internet. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `download_not_an_image` **The URL returned a web page, not an image.** HTTP 400 · type `invalid_request_error` **What it means:** The `url` answered with an HTML page (a share page, a login page or an error page) instead of the image file itself. Share links of file hosts usually point at such a page. **How to fix:** Use a direct link to the image file (for example the host's "download" or "raw" link), or upload the file with `multipart/form-data`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `download_timeout` **Downloading the file timed out.** HTTP 400 · type `invalid_request_error` **What it means:** The `url` did not deliver the file in time. **How to fix:** Host the file somewhere faster (a CDN or object storage) or upload it with `multipart/form-data`. **Retry:** Yes, later — back off and retry. ### `file_download_failed` **Artwork URL of an order item could not be downloaded.** HTTP 400 · type `invalid_request_error` **What it means:** A `url` layer of an inline order design was refused (not an http(s) URL, a URL with credentials, or a host that is not public or cannot be resolved) or could not be downloaded. Problems with a more specific cause are reported with that code instead: `download_failed`, `download_not_an_image`, `download_timeout`, `file_too_large`, `file_invalid`, `file_unsupported_type`, `file_unsafe`. `param` points at the layer. **How to fix:** Check that the URL is public and returns a supported image, or upload the file first and use its `file_id`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `artwork_download_budget_exceeded` **The order's artwork took too long to download.** HTTP 400 · type `invalid_request_error` **What it means:** An order downloads all of its `url` layers within one time budget (45 seconds by default; a few at a time). The budget ran out before every URL was downloaded, so nothing was created. URLs downloaded so far are kept as artwork files and reused when you send the order again, so a retry is cheaper. **How to fix:** Upload large or slow artwork first with `POST /files` and reference it with `file_id`, host the artwork on a fast CDN, or split the order. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `file_in_use` **The file is used by a product.** HTTP 409 · type `conflict_error` **What it means:** Files used by a product that is not archived cannot be deleted. **How to fix:** Archive the product (`DELETE /products/{id}`) or replace its artwork first, then delete the file. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `file_not_artwork` **The file cannot be used as artwork.** HTTP 400 · type `invalid_request_error` **What it means:** Only files uploaded as `artwork` or `logo` can be placed in designs (not mockups or other generated files). **How to fix:** Upload the image with `POST /files` and use the returned `file_id`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `file_not_ready` **The file is not ready.** HTTP 400 · type `invalid_request_error` **What it means:** The file is still processing or failed processing. **How to fix:** Use files whose `status` is `ready`. Upload again if it failed. **Retry:** Yes, later — back off and retry. ### `file_not_image` **The file is not an image.** HTTP 400 · type `invalid_request_error` **What it means:** Designs need image files with known pixel dimensions. **How to fix:** Upload a PNG, JPEG, WebP, TIFF, GIF or SVG image. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ## Designs Placing artwork on print areas (see [Designs & transforms](https://swaglists.com/docs/designs.md)). ### `designs_required` **No designs.** HTTP 400 · type `invalid_request_error` **What it means:** A product needs at least one design. **How to fix:** Send `designs: [{ "placement": "front", "layers": [{ "file_id": "file_…" }] }]`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `unknown_placement` **Unknown placement.** HTTP 400 · type `invalid_request_error` **What it means:** The `placement` key does not exist on this catalog product. The message lists the available keys. **How to fix:** Use a `placements[].key` from `GET /catalog/products/{id}`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `placement_invalid` **Placement not available on this product.** HTTP 400 · type `invalid_request_error` **What it means:** A price calculation was asked for a placement key (`placement_keys`) the catalog product does not have. Order requests, estimates and shipping-rate quotes report an unknown design placement as `unknown_placement` instead. **How to fix:** Use a `placements[].key` from `GET /catalog/products/{id}`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `duplicate_placement` **Placement listed twice.** HTTP 400 · type `invalid_request_error` **What it means:** Each placement may appear only once in `designs`. **How to fix:** Merge the layers into one design entry per placement (up to 20 layers each). **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `placement_required` **A required placement has no artwork.** HTTP 400 · type `invalid_request_error` **What it means:** The catalog product requires artwork on a placement (`placements[].required: true`) that your designs do not cover. **How to fix:** Add a design for the placement named in the message. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `placement_unavailable` **No production option prints this placement.** HTTP 400 · type `invalid_request_error` **What it means:** Reported in `errors` of an `item_unavailable` error: no production option of the item's variant can print on this placement for the destination, so the item can't be made with a design there. `param` names the design (`items[i].designs[j].placement`). **How to fix:** Remove the design from this placement (or move the artwork to another placement), or choose another variant. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `no_layers` **A design has no layers.** HTTP 400 · type `invalid_request_error` **What it means:** Every design entry needs at least one layer. **How to fix:** Add a layer with a `file_id` or `url`, or remove the design entry. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `too_many_layers` **Too many layers.** HTTP 400 · type `invalid_request_error` **What it means:** A placement can have at most 20 layers — for an order item that appends layers to a saved product's design, the product's layers count too. **How to fix:** Flatten artwork into fewer images. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `invalid_transform` **Invalid position.** HTTP 400 · type `invalid_request_error` **What it means:** A layer's `position` has non-numeric values, a zero/negative size, or values beyond ±10. **How to fix:** Use fractions of the print area (`0`–`1`, small overshoots allowed for bleed) — see [Designs & transforms](https://swaglists.com/docs/designs.md). **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `invalid_opacity` **Invalid opacity.** HTTP 400 · type `invalid_request_error` **What it means:** `opacity` must be a number between 0 and 1. **How to fix:** Send a value from `0` to `1` (default `1`). **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `design_not_visible` **No artwork inside the print area.** HTTP 400 · type `invalid_request_error` **What it means:** Every layer of a placement lies completely outside the printable area, so nothing would be printed. **How to fix:** Move the layers inside the area (fractions between 0 and 1), or omit `position` to center the artwork. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `artwork_resolution_too_low` **Artwork resolution too low to print.** HTTP 400 · type `invalid_request_error` **What it means:** At the size you placed it, the artwork would print below the minimum resolution (75 DPI by default). The message shows the effective DPI. **How to fix:** Use a larger image or place it smaller. Aim for 300 DPI: `recommended_width_px` of the placement covers the full area at 300 DPI. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ## Products & SKUs Creating products, variants and SKUs, and referencing them in orders. ### `catalog_product_unavailable` **Catalog product no longer available.** HTTP 400 · type `invalid_request_error` **What it means:** The catalog product exists but is not offered anymore. **How to fix:** Pick another product from `GET /catalog/products`. **Retry:** No — retrying the same request fails again. ### `catalog_variant_inactive` **Catalog variant no longer available.** HTTP 400 · type `invalid_request_error` **What it means:** The catalog variant was retired. **How to fix:** Pick another variant from `GET /catalog/products/{id}`. **Retry:** No — retrying the same request fails again. ### `variant_unavailable` **Variant not available.** HTTP 400 · type `invalid_request_error` **What it means:** The variant (or its catalog product) is no longer available, so it cannot be added or ordered. **How to fix:** Choose another variant; disable this one in your store. Listen to `catalog.price_changed` and check `available` on variants. **Retry:** No — retrying the same request fails again. ### `variant_disabled` **Product variant is disabled.** HTTP 400 · type `invalid_request_error` **What it means:** You disabled this product variant, so it cannot be ordered. **How to fix:** Re-enable it with `PATCH /products/{id}/variants/{variant_id}` `{"enabled": true}`, or order another variant. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `product_archived` **Product is archived.** HTTP 400 / 409 · type `invalid_request_error` **What it means:** Archived products cannot be ordered (400) or edited (409). **How to fix:** Restore the product by setting a status: `PATCH /products/{id}` `{"status": "active"}`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `product_has_no_designs` **Product has no artwork.** HTTP 400 · type `invalid_request_error` **What it means:** The product has no designs, so there is nothing to print. **How to fix:** Add designs with `PATCH /products/{id}` `{"designs": […]}`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `sku_mismatch` **SKU does not match the variant.** HTTP 400 · type `invalid_request_error` **What it means:** Both a product variant id and a SKU were given and they refer to different variants. **How to fix:** Identify each item with exactly one of `product_variant_id`, `sku` or `catalog_variant_id`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `item_reference_required` **Item has no product reference.** HTTP 400 · type `invalid_request_error` **What it means:** An item does not say which product to make. **How to fix:** Set `sku`, `product_variant_id`, or `catalog_variant_id` + `designs` on every item. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `no_variants` **No available variants.** HTTP 400 · type `invalid_request_error` **What it means:** The catalog product currently has no variant you can add. **How to fix:** Pick another catalog product or try again later. **Retry:** No — retrying the same request fails again. ### `duplicate_variant` **Variant listed twice.** HTTP 400 · type `invalid_request_error` **What it means:** The same `catalog_variant_id` appears more than once in `variants`. **How to fix:** List each catalog variant once. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `duplicate_sku` **SKU used twice in the request.** HTTP 400 · type `invalid_request_error` **What it means:** Two variants in the same request have the same SKU. **How to fix:** Give every variant a unique SKU (or omit `sku` to have one generated). **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `sku_taken` **SKU already in use.** HTTP 409 · type `conflict_error` **What it means:** Another product variant in your account already has this SKU. SKUs are unique per account (disabled and archived variants keep theirs). **How to fix:** Use a different SKU, or change the SKU of the other variant first. Find it with `GET /products/by_sku/{sku}`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `external_id_taken` **Product external_id already in use.** HTTP 409 · type `conflict_error` **What it means:** Another product already uses this `external_id`. **How to fix:** Use a unique `external_id` per product (for example your store's product id). **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `variant_exists` **Variant already on the product.** HTTP 409 · type `conflict_error` **What it means:** The catalog variant is already part of this product. **How to fix:** Update the existing variant with `PATCH /products/{id}/variants/{variant_id}` instead. **Retry:** No — retrying the same request fails again. ## Orders Creating, changing and canceling orders (see [Orders](https://swaglists.com/docs/orders.md)). ### `duplicate_external_id` **An order with this external_id exists.** HTTP 409 · type `conflict_error` **What it means:** `external_id` is unique per mode, and an order already has it. `existing_id` is that order's id. **How to fix:** Do not create the order again — fetch it with `GET /orders/{existing_id}` (or `GET /orders/ext:`). This usually means your integration already forwarded the order. **Retry:** No — retrying the same request fails again. **Extra fields:** `existing_id` ### `order_not_editable` **Only draft orders can be changed.** HTTP 409 · type `invalid_request_error` **What it means:** The order was already confirmed. **How to fix:** Cancel the order (if it has not gone into production) and create a corrected one. **Retry:** No — retrying the same request fails again. ### `order_not_draft` **The order is not a draft.** HTTP 409 · type `invalid_request_error` **What it means:** Only draft orders can be confirmed; this one is already confirmed (or canceled). **How to fix:** Fetch the order to see its status. Confirming twice is not needed. **Retry:** No — retrying the same request fails again. ### `order_not_confirmable` **The draft cannot be confirmed.** HTTP 409 · type `invalid_request_error` **What it means:** The draft has no items, or it was changed (items, recipient or shipping method) while it was being confirmed. **How to fix:** Add items with `PATCH /orders/{id}` first. After a concurrent change, fetch the draft again and confirm it then. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `order_not_cancelable` **The order can no longer be canceled.** HTTP 409 · type `invalid_request_error` **What it means:** Production already started, the order is being sent to production right now, or it already shipped, failed or was delivered. The message says which. **How to fix:** If the message says to try again in a minute, retry later. Once production has started the order cannot be canceled. **Retry:** Yes, later — back off and retry. ### `item_unavailable` **Item cannot be produced for this destination.** HTTP 400 · type `invalid_request_error` **What it means:** No production option can make this item for this order right now (`param` names it): nothing ships it to the recipient's country, it is out of stock, or several requirements of the order rule out every option. Problems you can fix yourself are reported with their own codes instead (recipient fields, shipping method, quantity, customizations); `errors` may name a placement that can't be printed (`placement_unavailable`). **How to fix:** Remove the item, choose another variant, or contact support for availability. **Retry:** No — retrying the same request fails again. ### `quantity_too_large` **Quantity too large for this item.** HTTP 400 · type `invalid_request_error` **What it means:** The production options of this item accept at most `max_quantity` units of it in one order (`param` = `items[i].quantity`). Nothing was created. **How to fix:** Lower the quantity to `max_quantity` or less, or split the units over several orders. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `max_quantity` ### `insufficient_funds` **Not enough credits.** HTTP 402 · type `insufficient_funds_error` **What it means:** Your account is set to reject (not hold) orders your wallet cannot cover, and available credits are too low. `required_amount` and `available_amount` are in cents. Nothing was created. **How to fix:** Add credits (or enable auto-recharge), then send the order again with a **new** Idempotency-Key (for example `order-1042:2`). The 402 is stored under the original key for 24 hours, so a retry with that key returns this error again even after you added credits; `external_id` still prevents a duplicate order. Or switch **Settings → Orders → When credits run out** to *hold* so orders wait and resume automatically, with nothing to retry. In test mode call `POST /test/wallet/fund`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `required_amount`, `available_amount`, `currency` ## Recipient & destination Checked when an order is created, estimated or confirmed, against what the production options of its items can accept: nothing is created or reserved, and `param` names the recipient field. Messages never repeat the recipient's data. ### `recipient_field_too_long` **A recipient field is too long.** HTTP 400 · type `invalid_request_error` **What it means:** The field named by `param` (for example `recipient.city` or `recipient.address1`) is longer than every production option of an item accepts. `max_length` is the largest length (characters, as people count them: an emoji or an accented letter is one) that at least one option accepts; `errors` lists other fields that option needs shorter. **How to fix:** Shorten the field to `max_length` characters or fewer (for street addresses, move part of `address1` to `address2`) and send the order again. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `max_length` ### `recipient_phone_required` **A recipient phone number is required.** HTTP 400 · type `invalid_request_error` **What it means:** The carriers available for this destination and shipping method need a phone number to deliver. When another shipping method would deliver without one, `available_shipping_methods` lists those methods. **How to fix:** Add `recipient.phone`, or choose one of `available_shipping_methods`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `available_shipping_methods` ### `recipient_email_required` **A recipient email address is required.** HTTP 400 · type `invalid_request_error` **What it means:** The carriers available for this destination and shipping method need an email address for delivery notifications. When another shipping method would deliver without one, `available_shipping_methods` lists those methods. **How to fix:** Add `recipient.email`, or choose one of `available_shipping_methods`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `available_shipping_methods` ### `recipient_tax_id_required` **A recipient tax id is required.** HTTP 400 · type `invalid_request_error` **What it means:** Customs of the destination require the recipient's tax id (for Brazil the CPF or CNPJ). **How to fix:** Add `recipient.tax_id`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `recipient_state_required` **A state is required.** HTTP 400 · type `invalid_request_error` **What it means:** Shipments to this country need a state or province (`recipient.state_code`). **How to fix:** Add `recipient.state_code` (ISO 3166-2 subdivision code, e.g. `CA` for California). **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `po_box_not_supported` **PO boxes are not supported for this order.** HTTP 400 · type `invalid_request_error` **What it means:** An address line is a PO box (`param` names it), and the items can't be delivered to PO boxes with this shipping method (couriers deliver to street addresses only). `available_shipping_methods` lists the methods that do deliver to it, when there are any. **How to fix:** Use a street address, or one of `available_shipping_methods` (usually `standard`). **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `available_shipping_methods` ### `military_address_not_supported` **Military addresses are not supported.** HTTP 400 · type `invalid_request_error` **What it means:** The address is an APO / FPO / DPO military address (state `AA`, `AE` or `AP`, or city APO / FPO / DPO), which the production options of an item can't deliver to. **How to fix:** Use a civilian address, or remove the item. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `recipient_local_script_required` **The address must be in the local script.** HTTP 400 · type `invalid_request_error` **What it means:** For this destination (for example Japan, China, Korea or Russia) the address lines and city must be written in the local script; the field named by `param` has too many Latin letters. **How to fix:** Send the address as the recipient would write it locally (e.g. 東京都渋谷区…). **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ## Customizations Options of a catalog product beyond its variants (a frame color, thread colors, a name…). An order item gets the values saved on its product, else each option's default; `param` is `items[i].customizations.` (just `items[i]` for an option that is managed only in the dashboard) and every problem of the request is listed in `errors`. ### `customization_unknown` **Unknown customization.** HTTP 400 · type `invalid_request_error` **What it means:** The key is not a customization of this product. `allowed_values` lists the valid keys (empty when the product has none). **How to fix:** Use the keys of the catalog product's customizations. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `allowed_values` ### `customization_invalid` **Invalid customization value.** HTTP 400 · type `invalid_request_error` **What it means:** The value has the wrong type (types are never converted: the string `"true"` is not a boolean, the string `"3"` is not a number) or is outside the option's range, length, pattern or palette; the message says which. For a choice, `allowed_values` lists the accepted values (`[true, false]` for yes/no options). A product's saved value that is no longer valid is reported the same way. **How to fix:** Send a value of the option's type and range (or one of `allowed_values`), or update the product's saved value. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `allowed_values` ### `customization_required` **A customization value is required.** HTTP 400 · type `invalid_request_error` **What it means:** The option is required and has no default, and neither the item nor the product sets it. `allowed_values` lists the choices of choice and yes/no options. **How to fix:** Send a value for the key named by `param`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `allowed_values` ### `customization_not_applicable` **The option needs a design on its placement.** HTTP 400 · type `invalid_request_error` **What it means:** The option belongs to one placement (for example embroidery thread colors of the left chest) and the item has no design there. **How to fix:** Add a design on that placement, or leave the option out. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `customization_value_hidden` **The value is no longer offered.** HTTP 400 · type `invalid_request_error` **What it means:** The value was retired for new orders. Products that already use it keep it, but it can't be chosen again. `allowed_values` lists the current choices. **How to fix:** Choose one of `allowed_values`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `allowed_values` ### `customization_unsupported` **Option not available for this destination.** HTTP 400 · type `invalid_request_error` **What it means:** No production option can make the item with this customization value for this destination (for example a frame color one factory doesn't stock). With the default value the item could be made. **How to fix:** Choose another value, leave the option at its default, or ship to another destination. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ## Shipping & pricing Prices and shipping methods for a destination. ### `shipping_method_unavailable` **Shipping method not available.** HTTP 400 · type `invalid_request_error` **What it means:** The `shipping_method` is not offered for these items and this destination (or nothing can ship there). The message and `available_shipping_methods` list the methods that are available. **How to fix:** Call `POST /shipping/rates` and use one of the returned methods (or one of `available_shipping_methods`); `standard` is the most widely available. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). **Extra fields:** `available_shipping_methods` ### `variant_unpriceable` **Variant cannot be priced right now.** HTTP 400 · type `invalid_request_error` **What it means:** The variant has no available production source at the moment. **How to fix:** Try another variant or try again later. **Retry:** Yes, later — back off and retry. ### `variant_product_mismatch` **Variant does not belong to the product.** HTTP 400 · type `invalid_request_error` **What it means:** A catalog variant was combined with a different catalog product. **How to fix:** Use variant ids from the same `GET /catalog/products/{id}` response. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ## Webhooks Managing webhook endpoints. ### `webhook_endpoint_limit_reached` **Too many webhook endpoints.** HTTP 400 · type `invalid_request_error` **What it means:** Each mode can have up to 16 endpoints by default. **How to fix:** Delete unused endpoints, or subscribe one endpoint to several event types. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `webhook_endpoint_disabled` **The endpoint is disabled.** HTTP 409 · type `invalid_request_error` **What it means:** Test events and replays cannot be sent to a disabled endpoint. **How to fix:** Re-enable it with `PATCH /webhook_endpoints/{id}` `{"status": "enabled"}`. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ## Test mode Test helpers (see [Test mode](https://swaglists.com/docs/test-mode.md)). ### `order_not_advanceable` **The test order cannot advance.** HTTP 409 · type `invalid_request_error` **What it means:** The order is a draft, already finished (delivered, canceled, failed), or on hold for a reason that needs action (for example waiting for credits), or the requested `outcome` doesn't fit its current state (for example a partial shipment of a single unit, or a delivery problem before anything shipped). **How to fix:** Confirm drafts first; add test credits with `POST /test/wallet/fund` for orders waiting for funds. Fetch the order and pick an `outcome` that fits its status. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ### `order_status_changed` **The test order changed status first.** HTTP 409 · type `conflict_error` **What it means:** The simulated step was requested for an order status that is no longer current: the simulator (or another request) moved the order on in the meantime. Nothing was changed. **How to fix:** Fetch the order again, check its new status, and simulate the next step from there if it is still needed. **Retry:** After fixing the cause, with a **new** Idempotency-Key: the original key can replay this error for 24 hours, even after the fix ([details](https://swaglists.com/docs/idempotency.md#idempotency-key)). ## Server errors Problems on our side. ### `internal_error` **Internal error.** HTTP 500 / 502 / 503 · type `api_error` **What it means:** Something went wrong on our side. No details are returned; the error is logged with the request id. **How to fix:** Retry with exponential backoff and the same Idempotency-Key (the request is never half-applied). If it keeps happening, contact support with the `request_id`. **Retry:** Yes — retry with the same Idempotency-Key. ## Warnings Product create and update responses can include non-blocking `warnings` (`code`, `message`, `param`, `placement`, `effective_dpi`). The request succeeded; review them before selling the product. ### `dpi_low` **Artwork may print slightly blurry.** At the placed size the artwork is below the recommended resolution (150 DPI by default, or the placement's `min_dpi` when higher). `effective_dpi` shows the value. **How to fix:** Use a larger image or place it smaller. The product is still created. ### `layer_outside_area` **Layer will not be printed.** A layer is completely outside the print area (or has opacity 0), so it will not appear on the product. **How to fix:** Move the layer inside the area or remove it. --- # Events reference > Every event type, when it fires, what data.object contains, and a complete example payload for each. Source: https://swaglists.com/docs/events (markdown: https://swaglists.com/docs/events.md) Events are delivered to your [webhook endpoints](https://swaglists.com/docs/webhooks.md) and listed by `GET /events` (last 30 days). Every event has the same envelope: | Field | Description | | --- | --- | | `id` | Event id (`evt_…`). Also the `webhook-id` header — deduplicate on it. | | `object` | Always `event`. | | `type` | Event type, below. | | `api_version` | API version the payload was rendered with (`2026-10-01`). | | `created_at` | When it happened (ISO-8601, UTC). | | `livemode` | `true` for live events, `false` for test events. | | `data.object` | The resource as of the event — the same shape the API returns. | | `data.previous_attributes` | Old values of changed fields (status changes and `*.updated` events; absent otherwise). | Subscribe to `["*"]` to receive every type, including types added in the future (ignore the ones you do not handle). Order-status events are sent for every status change, and also include the full order with its `shipments`. ## Every lifecycle step, exactly once Each step of an order's life — `order.in_production`, `order.partially_shipped`, `order.shipped`, `order.delivered` — and of each parcel — `shipment.created`, `shipment.delivered`, and `shipment.updated` per tracking change — is sent **exactly once**, also when production moves faster than our status updates and one update covers several steps. The steps are then created together, in lifecycle order (for example `order.in_production`, `shipment.created`, `order.shipped`, `shipment.delivered`, `order.delivered`), and: - `data.object` is the order or shipment **as of the end of that update** — a back-filled `order.shipped` may already show `"status": "delivered"`; - `data.previous_attributes.status` is the previous **step** (`processing` → `in_production` → `shipped`), not the status the order had before the update; - the order events among them share one `data.object.updated_at`, which is later than that of any earlier event about the order. Re-sent or repeated status updates never produce a step twice (only an order that our team reopens to produce missing items again is announced as `order.delivered` once more when those arrive). See [Webhooks](https://swaglists.com/docs/webhooks.md#lifecycle-steps) for delivery order and examples. ## All event types | Event | When it fires | `data.object` | | --- | --- | --- | | [`order.created`](https://swaglists.com/docs/events.md#order.created) | An order was created through the API or the dashboard — as a draft (`confirm: false`) or confirmed. A confirmed order also sends `order.processing` or `order.on_hold`. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.updated`](https://swaglists.com/docs/events.md#order.updated) | A draft's details changed (recipient, items, shipping method, metadata, external id), or something about the order changed that has no more specific event. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.confirmed`](https://swaglists.com/docs/events.md#order.confirmed) | A draft order was confirmed (`POST /orders/{id}/confirm`). Followed by `order.processing` or `order.on_hold`. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.on_hold`](https://swaglists.com/docs/events.md#order.on_hold) | The order is waiting: not enough credits (`hold_reason: insufficient_funds`), or held for review, artwork, address or a production issue. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.processing`](https://swaglists.com/docs/events.md#order.processing) | Credits were reserved and the order was sent to production — right after confirmation, or when a held order is released (e.g. after you added credits). | [order](https://swaglists.com/docs/objects.md#order) | | [`order.in_production`](https://swaglists.com/docs/events.md#order.in_production) | Production started. From now on the order can no longer be canceled. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.partially_shipped`](https://swaglists.com/docs/events.md#order.partially_shipped) | Some items shipped; more parcels follow. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.shipped`](https://swaglists.com/docs/events.md#order.shipped) | Every item shipped. `shipments` has the carrier, tracking number and tracking URL of each parcel. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.delivered`](https://swaglists.com/docs/events.md#order.delivered) | Every shipment was delivered. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.canceled`](https://swaglists.com/docs/events.md#order.canceled) | The order was canceled (by you or by support). Reserved credits were released and spent credits refunded to your wallet. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.failed`](https://swaglists.com/docs/events.md#order.failed) | The order could not be produced. Reserved credits were released; no further events follow. | [order](https://swaglists.com/docs/objects.md#order) | | [`order.refunded`](https://swaglists.com/docs/events.md#order.refunded) | Credits were refunded to your wallet for this order (full or partial), for example when support accepts a claim for a damaged or misprinted item. | [order](https://swaglists.com/docs/objects.md#order) | | [`shipment.created`](https://swaglists.com/docs/events.md#shipment.created) | A parcel was handed to the carrier and has tracking information. Sent together with `order.partially_shipped` or `order.shipped`. | [shipment](https://swaglists.com/docs/objects.md#shipment) | | [`shipment.updated`](https://swaglists.com/docs/events.md#shipment.updated) | A parcel's tracking status changed (e.g. `in_transit` → `out_for_delivery`, `exception`, `returned`). | [shipment](https://swaglists.com/docs/objects.md#shipment) | | [`shipment.delivered`](https://swaglists.com/docs/events.md#shipment.delivered) | A parcel was delivered. | [shipment](https://swaglists.com/docs/objects.md#shipment) | | [`product.created`](https://swaglists.com/docs/events.md#product.created) | A product was created (API or designer). Mockups are rendered afterwards and announced with `product.updated`. | [product](https://swaglists.com/docs/objects.md#product) | | [`product.updated`](https://swaglists.com/docs/events.md#product.updated) | A product changed: details, designs, variants, or its mockups finished rendering. | [product](https://swaglists.com/docs/objects.md#product) | | [`product.deleted`](https://swaglists.com/docs/events.md#product.deleted) | A product was archived (`DELETE /products/{id}`). Its SKUs stay reserved; restoring it sends `product.updated`. | [product](https://swaglists.com/docs/objects.md#product) | | [`catalog.price_changed`](https://swaglists.com/docs/events.md#catalog.price_changed) | The wholesale price of catalog variants used by your products changed (our costs or your price list changed). Re-fetch prices and adjust your store if needed. | [catalog_price_change](https://swaglists.com/docs/objects.md#catalog_price_change) | | [`wallet.credited`](https://swaglists.com/docs/events.md#wallet.credited) | Credits were added to the wallet: a top-up and its bonus (one event each), a refund for an order, an adjustment by support, or test credits. | [transaction](https://swaglists.com/docs/objects.md#transaction) | | [`wallet.low_balance`](https://swaglists.com/docs/events.md#wallet.low_balance) | Available credits fell below your low-balance threshold (sent once until the balance recovers). | [wallet](https://swaglists.com/docs/objects.md#wallet) | | [`topup.succeeded`](https://swaglists.com/docs/events.md#topup.succeeded) | A credit purchase was paid and the credits (plus any bonus) were added. | [topup](https://swaglists.com/docs/objects.md#topup) | | [`topup.failed`](https://swaglists.com/docs/events.md#topup.failed) | A credit purchase failed (e.g. the card was declined or a bank debit was returned). | [topup](https://swaglists.com/docs/objects.md#topup) | | [`file.processed`](https://swaglists.com/docs/events.md#file.processed) | An uploaded file finished processing (`status` is `ready`, or `failed`). `url` is null in the event — fetch the file for a download URL. | [file](https://swaglists.com/docs/objects.md#file) | | [`webhook_endpoint.test`](https://swaglists.com/docs/events.md#webhook_endpoint.test) | Sent on request with `POST /webhook_endpoints/{id}/test` (or the dashboard) to that endpoint only. Use it to check your signature verification. Not listed by `GET /events` unless you filter for it. | [webhook_endpoint](https://swaglists.com/docs/objects.md#webhook_endpoint) | ## Orders ### `order.created` An order was created through the API or the dashboard — as a draft (`confirm: false`) or confirmed. A confirmed order also sends `order.processing` or `order.on_hold`. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.created", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "processing", "hold_reason": null, "funds_status": "held", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 0, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "pending", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": null, "shipped_at": null, "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } } } ```
### `order.updated` A draft's details changed (recipient, items, shipping method, metadata, external id), or something about the order changed that has no more specific event. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order) · **`data.previous_attributes`:** Changed fields with their old values (e.g. `shipping_method`, `recipient`, `metadata`).
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.updated", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "draft", "hold_reason": null, "funds_status": "none", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "express", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 0, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "draft", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": null, "in_production_at": null, "shipped_at": null, "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null }, "previous_attributes": { "shipping_method": "standard" } } } ```
### `order.confirmed` A draft order was confirmed (`POST /orders/{id}/confirm`). Followed by `order.processing` or `order.on_hold`. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.confirmed", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "processing", "hold_reason": null, "funds_status": "held", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 0, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "pending", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": null, "shipped_at": null, "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } } } ```
### `order.on_hold` The order is waiting: not enough credits (`hold_reason: insufficient_funds`), or held for review, artwork, address or a production issue. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order) · **`data.previous_attributes`:** `status` when the order was already confirmed before.
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.on_hold", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "on_hold", "hold_reason": "insufficient_funds", "funds_status": "awaiting", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 0, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "pending", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": null, "shipped_at": null, "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } } } ```
### `order.processing` Credits were reserved and the order was sent to production — right after confirmation, or when a held order is released (e.g. after you added credits). **`data.object`:** [order](https://swaglists.com/docs/objects.md#order) · **`data.previous_attributes`:** `status` (`on_hold`) when the order was released from a hold.
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.processing", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "processing", "hold_reason": null, "funds_status": "held", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 0, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "pending", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": null, "shipped_at": null, "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null }, "previous_attributes": { "status": "on_hold" } } } ```
### `order.in_production` Production started. From now on the order can no longer be canceled. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order) · **`data.previous_attributes`:** `status` before the change.
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.in_production", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "in_production", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 0, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "in_production", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": null, "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null }, "previous_attributes": { "status": "processing" } } } ```
### `order.partially_shipped` Some items shipped; more parcels follow. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order) · **`data.previous_attributes`:** `status` before the change.
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.partially_shipped", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "partially_shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null }, "previous_attributes": { "status": "in_production" } } } ```
### `order.shipped` Every item shipped. `shipments` has the carrier, tracking number and tracking URL of each parcel. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order) · **`data.previous_attributes`:** `status` before the change.
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.shipped", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null }, "previous_attributes": { "status": "in_production" } } } ```
### `order.delivered` Every shipment was delivered. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order) · **`data.previous_attributes`:** `status` before the change.
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.delivered", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "delivered", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "delivered", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": "2026-10-26T15:20:00.000Z", "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null }, "previous_attributes": { "status": "shipped" } } } ```
### `order.canceled` The order was canceled (by you or by support). Reserved credits were released and spent credits refunded to your wallet. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order) · **`data.previous_attributes`:** `status` before the cancellation.
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.canceled", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "canceled", "hold_reason": null, "funds_status": "released", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 0, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "canceled", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": null, "shipped_at": null, "delivered_at": null, "canceled_at": "2026-10-21T15:01:12.000Z", "cancel_reason": "Customer changed their mind", "failed_at": null, "failure_reason": null }, "previous_attributes": { "status": "processing" } } } ```
### `order.failed` The order could not be produced. Reserved credits were released; no further events follow. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order) · **`data.previous_attributes`:** `status` before the change.
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.failed", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "failed", "hold_reason": null, "funds_status": "released", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 0, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "failed", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": null, "shipped_at": null, "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": "2026-10-21T15:40:00.000Z", "failure_reason": "This order could not be produced. Reserved credits were released." }, "previous_attributes": { "status": "processing" } } } ```
### `order.refunded` Credits were refunded to your wallet for this order (full or partial), for example when support accepts a claim for a damaged or misprinted item. **`data.object`:** [order](https://swaglists.com/docs/objects.md#order) · **`data.previous_attributes`:** `refunded` before the refund.
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.refunded", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "delivered", "hold_reason": null, "funds_status": "partially_refunded", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 1299, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "delivered", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": "2026-10-26T15:20:00.000Z", "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null }, "previous_attributes": { "refunded": 0 } } } ```
## Shipments ### `shipment.created` A parcel was handed to the carrier and has tracking information. Sent together with `order.partially_shipped` or `order.shipped`. **`data.object`:** [shipment](https://swaglists.com/docs/objects.md#shipment)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "shipment.created", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } } } ```
### `shipment.updated` A parcel's tracking status changed (e.g. `in_transit` → `out_for_delivery`, `exception`, `returned`). **`data.object`:** [shipment](https://swaglists.com/docs/objects.md#shipment)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "shipment.updated", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "out_for_delivery", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } } } ```
### `shipment.delivered` A parcel was delivered. **`data.object`:** [shipment](https://swaglists.com/docs/objects.md#shipment)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "shipment.delivered", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "delivered", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": "2026-10-26T15:20:00.000Z", "created_at": "2026-10-23T09:12:44.000Z" } } } ```
## Products ### `product.created` A product was created (API or designer). Mockups are rendered afterwards and announced with `product.updated`. **`data.object`:** [product](https://swaglists.com/docs/objects.md#product)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "product.created", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "product", "id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "status": "active", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "external_id": "shopify-product-9921", "tags": [ "logo" ], "metadata": {}, "retail_price": 2900, "currency": "usd", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "variants": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "mockups": [], "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } } } ```
### `product.updated` A product changed: details, designs, variants, or its mockups finished rendering. **`data.object`:** [product](https://swaglists.com/docs/objects.md#product) · **`data.previous_attributes`:** Changed fields with their old values, when known (e.g. `title`, `designs`).
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "product.updated", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "product", "id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "status": "active", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "external_id": "shopify-product-9921", "tags": [ "logo" ], "metadata": {}, "retail_price": 2900, "currency": "usd", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "variants": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "mockups": [ { "url": "https://swaglists.com/f/file_01k8x3y9a1s3d5f7g9h1j3k5l7/1782777600.c0ffee/mockup-front.png", "placement": "front", "color": "Black", "is_primary": true } ], "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" }, "previous_attributes": { "title": "Company A Tee" } } } ```
### `product.deleted` A product was archived (`DELETE /products/{id}`). Its SKUs stay reserved; restoring it sends `product.updated`. **`data.object`:** [product](https://swaglists.com/docs/objects.md#product)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "product.deleted", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "product", "id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "status": "archived", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "external_id": "shopify-product-9921", "tags": [ "logo" ], "metadata": {}, "retail_price": 2900, "currency": "usd", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "variants": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "mockups": [ { "url": "https://swaglists.com/f/file_01k8x3y9a1s3d5f7g9h1j3k5l7/1782777600.c0ffee/mockup-front.png", "placement": "front", "color": "Black", "is_primary": true } ], "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } } } ```
## Catalog prices ### `catalog.price_changed` The wholesale price of catalog variants used by your products changed (our costs or your price list changed). Re-fetch prices and adjust your store if needed. **`data.object`:** [catalog_price_change](https://swaglists.com/docs/objects.md#catalog_price_change)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "catalog.price_changed", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "catalog_price_change", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_ids": [ "cv_01k8w1a2s7d9f1g3h5j7k9m1n3" ], "reason": "cost_increase" } } } ```
## Wallet ### `wallet.credited` Credits were added to the wallet: a top-up and its bonus (one event each), a refund for an order, an adjustment by support, or test credits. **`data.object`:** [transaction](https://swaglists.com/docs/objects.md#transaction)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "wallet.credited", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "transaction", "id": "txn_01k8x4b2q8w0e2r4t6y8u0i2o4", "type": "topup", "amount": 500000, "held_delta": 0, "balance_after": 982550, "currency": "usd", "description": "Credit purchase", "order_id": null, "topup_id": "top_01k8x1q3w5e7r9t1y3u5i7o9p1", "created_at": "2026-10-21T14:32:05.000Z" } } } ```
### `wallet.low_balance` Available credits fell below your low-balance threshold (sent once until the balance recovers). **`data.object`:** [wallet](https://swaglists.com/docs/objects.md#wallet)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "wallet.low_balance", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "wallet", "mode": "live", "livemode": true, "currency": "usd", "available": 8200, "held": 12480, "total": 20680, "low_balance_threshold": 10000 } } } ```
## Top-ups ### `topup.succeeded` A credit purchase was paid and the credits (plus any bonus) were added. **`data.object`:** [topup](https://swaglists.com/docs/objects.md#topup)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "topup.succeeded", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "topup", "id": "top_01k8x1q3w5e7r9t1y3u5i7o9p1", "status": "succeeded", "amount": 500000, "bonus": 25000, "bonus_percent": 5, "credits": 525000, "currency": "usd", "payment_method_type": "us_bank_account", "receipt_url": null, "created_at": "2026-10-21T14:32:05.000Z", "succeeded_at": "2026-10-21T14:32:05.000Z" } } } ```
### `topup.failed` A credit purchase failed (e.g. the card was declined or a bank debit was returned). **`data.object`:** [topup](https://swaglists.com/docs/objects.md#topup)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "topup.failed", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "topup", "id": "top_01k8x1q3w5e7r9t1y3u5i7o9p1", "status": "failed", "amount": 500000, "bonus": 0, "bonus_percent": 5, "credits": 525000, "currency": "usd", "payment_method_type": "us_bank_account", "receipt_url": null, "created_at": "2026-10-21T14:32:05.000Z", "succeeded_at": null } } } ```
## Files ### `file.processed` An uploaded file finished processing (`status` is `ready`, or `failed`). `url` is null in the event — fetch the file for a download URL. **`data.object`:** [file](https://swaglists.com/docs/objects.md#file)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "file.processed", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "file", "id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "filename": "logo-front.png", "mime_type": "image/png", "size_bytes": 482113, "width_px": 4500, "height_px": 5400, "dpi": 300, "status": "ready", "purpose": "artwork", "url": null, "created_at": "2026-10-21T14:32:05.000Z" } } } ```
## Testing ### `webhook_endpoint.test` Sent on request with `POST /webhook_endpoints/{id}/test` (or the dashboard) to that endpoint only. Use it to check your signature verification. Not listed by `GET /events` unless you filter for it. **`data.object`:** [webhook_endpoint](https://swaglists.com/docs/objects.md#webhook_endpoint)
Example payload ```json title="Example payload" { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "webhook_endpoint.test", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": false, "data": { "object": { "object": "webhook_endpoint", "id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "url": "https://company-a.example/webhooks/print", "description": "Production order updates", "enabled_events": [ "order.shipped", "order.delivered" ], "status": "enabled", "disabled_reason": null, "mode": "live", "livemode": true, "created_at": "2026-10-21T14:32:05.000Z" } } } ```
--- # Objects reference > Every object the API returns — fields, types and descriptions, generated from the API schema. Source: https://swaglists.com/docs/objects (markdown: https://swaglists.com/docs/objects.md) The objects below are returned by the API and embedded in events. Field lists are generated from the API's OpenAPI schema, so they always match the API. Types: `string | null` means the field is always present but may be `null`. ## Conventions - Every object has an `object` field naming its type, and most have an `id` with a type prefix (`ord_`, `prod_`, …). - Amounts are integers in cents with a lowercase `currency` (`usd`). - Timestamps are ISO-8601 strings in UTC; fields that do not apply are `null`. - New fields can be added at any time — ignore fields you do not know. ## List List endpoints return a list object. See [Pagination](https://swaglists.com/docs/pagination.md). | Field | Type | Description | | --- | --- | --- | | `object` | string | Always `list`. | | `data` | array | The objects of this page. | | `has_more` | boolean | Whether more objects exist beyond this page (in the paging direction). | | `url` | string | Path of the list endpoint. | ## Account Your account as seen by the API key, including its mode and wallet summary. Returned by: `GET /account`. | Field | Type | Description | | --- | --- | --- | | `object` | "account" | Type of the object: always `account`. | | `id` | string | Unique identifier (starts with `org_`). | | `name` | string | Your account (organization) name. | | `brand_name` | string \| null | Brand printed on packing slips and shown on the tracking page. | | `support_email` | string \| null | Support email shown to your customers (packing slips, tracking page, shipment emails). | | `website` | string \| null | Your website. | | `country` | string \| null | Country of your business (ISO 3166-1 alpha-2). | | `mode` | string | Mode of the API key used for this request. One of: `live`, `test`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `live_mode_enabled` | boolean | Whether live mode is enabled for the account (live keys work and live orders are produced). | | `settings` | object | Order settings that change how the API behaves (Settings → Orders in the dashboard). | | `settings.auto_confirm_orders` | boolean | Default of `confirm` when creating orders. | | `settings.insufficient_funds_behavior` | string | What happens to an order your credits don't cover. hold: it waits on hold and is released automatically (oldest first) when credits arrive; hold_manual: it waits on hold until you release it (dashboard, or POST /orders/{id}/confirm); reject: POST /orders answers 402 insufficient_funds and nothing is created. One of: `hold`, `hold_manual`, `reject`. | | `wallet` | [Wallet](https://swaglists.com/docs/objects.md#wallet) | Wallet of the key's mode. | | `created_at` | string | When the object was created (ISO-8601, UTC). |
Example account ```json { "object": "account", "id": "org_01k8x3q8m4h2c9t6v5r7w1y0za", "name": "Company A", "brand_name": "Company A Apparel", "support_email": "help@company-a.example", "website": "https://company-a.example", "country": "US", "mode": "live", "livemode": true, "live_mode_enabled": true, "settings": { "auto_confirm_orders": true, "insufficient_funds_behavior": "hold" }, "wallet": { "object": "wallet", "mode": "live", "livemode": true, "currency": "usd", "available": 482550, "held": 12480, "total": 495030, "low_balance_threshold": 10000 }, "created_at": "2026-10-21T14:32:05.000Z" } ```
## Wallet Prepaid credit balance of one mode (live and test have separate wallets). Returned by: `GET /wallet`, `POST /test/wallet/fund`, `event wallet.low_balance`. | Field | Type | Description | | --- | --- | --- | | `object` | "wallet" | Type of the object: always `wallet`. | | `mode` | string | `live` or `test`. One of: `live`, `test`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `available` | integer | Spendable credits in cents. May be negative after a payment dispute. | | `held` | integer | Credits reserved for in-flight orders, in cents. | | `total` | integer | available + held, in cents. | | `low_balance_threshold` | integer \| null | Low-balance notification threshold in cents. |
Example wallet ```json { "object": "wallet", "mode": "live", "livemode": true, "currency": "usd", "available": 482550, "held": 12480, "total": 495030, "low_balance_threshold": 10000 } ```
## Transaction An immutable wallet ledger entry: every change to your balance is one transaction. Returned by: `GET /wallet/transactions`, `event wallet.credited`. | Field | Type | Description | | --- | --- | --- | | `object` | "transaction" | Type of the object: always `transaction`. | | `id` | string | Unique identifier (starts with `txn_`). | | `type` | string | Kind of balance change. One of: `topup`, `bonus`, `hold`, `release`, `capture`, `charge`, `refund`, `adjustment`, `reversal`, `test_credit`. | | `amount` | integer | Signed change to available credits (cents). | | `held_delta` | integer | Signed change to held credits (cents). | | `balance_after` | integer | Available credits after this transaction (cents). | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `description` | string | Human-readable description. | | `order_id` | string \| null | Order the transaction belongs to, if any. | | `topup_id` | string \| null | Top-up the transaction belongs to, if any. | | `created_at` | string | When the object was created (ISO-8601, UTC). |
Example transaction ```json { "object": "transaction", "id": "txn_01k8x4b2q8w0e2r4t6y8u0i2o4", "type": "hold", "amount": -2598, "held_delta": 2598, "balance_after": 479952, "currency": "usd", "description": "Credits reserved for order ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "topup_id": null, "created_at": "2026-10-21T14:32:05.000Z" } ```
## Top-up A credit purchase (made in the dashboard). Returned by: `GET /topups`, `GET /topups/{id}`, `events topup.*`. | Field | Type | Description | | --- | --- | --- | | `object` | "topup" | Type of the object: always `topup`. | | `id` | string | Unique identifier (starts with `top_`). | | `status` | string | Payment status of the top-up. One of: `pending`, `processing`, `succeeded`, `failed`, `canceled`, `refunded`, `partially_refunded`, `disputed`. | | `amount` | integer | Amount paid (cents). | | `bonus` | integer | Bonus credits granted (cents). | | `bonus_percent` | number | Bonus percentage, e.g. 5 for 5%. | | `credits` | integer | Credits added = amount + bonus (cents). | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `payment_method_type` | string \| null | How it was paid (e.g. `card`, `us_bank_account`). | | `receipt_url` | string \| null | Payment receipt, when available. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `succeeded_at` | string \| null | When the credits were added. |
Example top-up ```json { "object": "topup", "id": "top_01k8x1q3w5e7r9t1y3u5i7o9p1", "status": "succeeded", "amount": 500000, "bonus": 25000, "bonus_percent": 5, "credits": 525000, "currency": "usd", "payment_method_type": "us_bank_account", "receipt_url": null, "created_at": "2026-10-21T14:32:05.000Z", "succeeded_at": "2026-10-21T14:32:05.000Z" } ```
## Catalog category A group of catalog products (categories can be nested with `parent_id`). Returned by: `GET /catalog/categories`. | Field | Type | Description | | --- | --- | --- | | `object` | "catalog_category" | Type of the object: always `catalog_category`. | | `id` | string | Unique identifier (starts with `cat_`). | | `slug` | string | URL-friendly identifier; use it as the `category` filter of `GET /catalog/products`. | | `name` | string | Category name. | | `parent_id` | string \| null | Parent category (null for top-level categories). | | `description` | string \| null | Category description. | | `image_url` | string \| null | Category image. | | `sort_order` | integer | Display order (ascending). | | `product_count` | integer | Active products in this category and its sub-categories. | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). | | `updated_at` | string \| null | When the object was last changed (ISO-8601, UTC). |
Example catalog category ```json { "object": "catalog_category", "id": "cat_01k8w0q1w2e3r4t5y6u7i8o9p0", "slug": "t-shirts", "name": "T-Shirts", "parent_id": null, "description": "Classic and premium tees.", "image_url": null, "sort_order": 0, "product_count": 12, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ```
## Catalog product A product you can customize and sell, with your wholesale prices, print areas and mockup templates. Returned by: `GET /catalog/products`, `GET /catalog/products/{id_or_slug}`. | Field | Type | Description | | --- | --- | --- | | `object` | "catalog_product" | Type of the object: always `catalog_product`. | | `id` | string | Unique identifier (starts with `cp_`). | | `slug` | string | URL-friendly identifier; can be used instead of the id in `GET /catalog/products/{id_or_slug}`. | | `title` | string | Product title. | | `subtitle` | string \| null | Short tagline. | | `description` | string \| null | Markdown. | | `category` | [CatalogCategoryRef](https://swaglists.com/docs/objects.md#catalog_category) | Category of the product. | | `tags` | array of string | Tags (e.g. `bestseller`). | | `images` | array of object | Product photos. | | `images[].url` | string | Image URL. | | `images[].alt` | string \| null | Alt text. | | `features` | array of string | Selling points. | | `techniques` | array of string | Print techniques used (e.g. `dtg`, `embroidery`, `sublimation`, `digital`). | | `material` | string \| null | Material. | | `care_instructions` | string \| null | Care instructions. | | `size_guide` | map of any \| null | Size chart data, when available. | | `options` | array of object | Option names and values (e.g. Color, Size) in display order. | | `options[].name` | string | Option name (e.g. `Color`). | | `options[].values` | array of object | Values of the option. | | `options[].values[].value` | string | Value as used in variant `options`. | | `options[].values[].label` | string | Display label. | | `options[].values[].hex` | string \| null | Color swatch (hex), for color options. | | `production_days` | object | Business days to produce before shipping. | | `production_days.min` | integer | Fastest production time in business days. | | `production_days.max` | integer | Slowest production time in business days. | | `featured` | boolean | Featured products are listed first. | | `placements` | array of [Placement](https://swaglists.com/docs/objects.md#placement) | Print areas you can put artwork on. | | `variants` | array of [CatalogVariant](https://swaglists.com/docs/objects.md#catalog_variant) | Active variants with your wholesale price and availability. | | `mockup_templates` | array of [MockupTemplate](https://swaglists.com/docs/objects.md#mockup_template) | Templates to render previews. | | `price_range` | object \| null | Lowest and highest wholesale variant price (first placement). | | `price_range.min` | integer | Lowest variant price in cents. | | `price_range.max` | integer | Highest variant price in cents. | | `price_range.currency` | string | Currency of the prices. | | `price_from` | integer \| null | Lowest wholesale price over active variants (cents). | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). | | `updated_at` | string \| null | When the object was last changed (ISO-8601, UTC). |
Example catalog product ```json { "object": "catalog_product", "id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "slug": "classic-unisex-tee", "title": "Classic Unisex Tee", "subtitle": "100% combed cotton", "description": "A soft, everyday tee.", "category": { "object": "catalog_category", "id": "cat_01k8w0q1w2e3r4t5y6u7i8o9p0", "slug": "t-shirts", "name": "T-Shirts", "parent_id": null }, "tags": [ "bestseller" ], "images": [ { "url": "https://swaglists.com/f/file_01k8w1c2v4b6n8m0q2w4e6r8t1/1767225600.9b1c/tee.png", "alt": "Classic tee" } ], "features": [ "Pre-shrunk", "Side-seamed" ], "techniques": [ "dtg" ], "material": "100% cotton", "care_instructions": "Machine wash cold, inside out.", "size_guide": null, "options": [ { "name": "Color", "values": [ { "value": "Black", "label": "Black", "hex": "#111111" } ] }, { "name": "Size", "values": [ { "value": "M", "label": "M", "hex": null } ] } ], "production_days": { "min": 2, "max": 4 }, "featured": true, "placements": [ { "object": "placement", "key": "front", "label": "Front", "description": "Front print area", "technique": "dtg", "aspect_ratio": 0.833333, "width_in": 15, "height_in": 18, "recommended_width_px": 4500, "recommended_height_px": 5400, "min_dpi": 150, "required": false, "file_requirements": { "formats": [ "png", "jpeg", "webp", "tiff", "svg" ], "max_file_size_bytes": 104857600, "max_dimension_px": 30000, "recommended_width_px": 4500, "recommended_height_px": 5400, "recommended_dpi": 300, "min_dpi": 150, "color_profile": "sRGB", "transparent_background": true } } ], "variants": [ { "object": "catalog_variant", "id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "sku": "TEE-CLASSIC-BLK-M", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "color": "Black", "color_hex": "#111111", "size": "M", "image_url": null, "available": true, "price": 1299, "currency": "usd" } ], "mockup_templates": [ { "object": "mockup_template", "id": "cmt_01k8w1b3c5v7b9n1m3a5s7d9f1", "placement_key": "front", "color": "Black", "label": "Front", "image_url": "https://swaglists.com/f/file_01k8w1c2v4b6n8m0q2w4e6r8t0/1767225600.3f9a/tee-front.png", "image_width_px": 1600, "image_height_px": 2000, "print_area": { "x": 0.3, "y": 0.25, "width": 0.4, "height": 0.48 }, "blend_mode": "multiply", "is_primary": true } ], "price_range": { "min": 1299, "max": 1599, "currency": "usd" }, "price_from": 1299, "currency": "usd", "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ```
## Catalog variant One color/size (or other option combination) of a catalog product. Returned by: `GET /catalog/products/{id_or_slug}`. | Field | Type | Description | | --- | --- | --- | | `object` | "catalog_variant" | Type of the object: always `catalog_variant`. | | `id` | string | Unique identifier (starts with `cv_`). | | `catalog_product_id` | string | Catalog product of the variant. | | `sku` | string | Catalog SKU (ours). Your own SKUs live on product variants. | | `title` | string | Variant title (e.g. `Black / M`). | | `options` | map of string | Option values (e.g. `{"Color": "Black", "Size": "M"}`). | | `color` | string \| null | Color name, when the product has colors. | | `color_hex` | string \| null | Color swatch (hex). | | `size` | string \| null | Size, when the product has sizes. | | `image_url` | string \| null | Photo of this variant, when available. | | `available` | boolean | Currently in stock and orderable. | | `price` | integer \| null | Your wholesale price per unit in cents, printed on the first placement. Null when not purchasable. | | `currency` | string | Three-letter currency code, lowercase (`usd`). |
Example catalog variant ```json { "object": "catalog_variant", "id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "sku": "TEE-CLASSIC-BLK-M", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "color": "Black", "color_hex": "#111111", "size": "M", "image_url": null, "available": true, "price": 1299, "currency": "usd" } ```
## Placement A printable area of a catalog product (front, back, sleeve, …) with its recommended artwork size. Returned by: `GET /catalog/products/{id_or_slug}`. | Field | Type | Description | | --- | --- | --- | | `object` | "placement" | Type of the object: always `placement`. | | `key` | string | Use this key as `placement` in designs. | | `label` | string | Display name (e.g. `Front`). | | `description` | string \| null | Description of the print area. | | `technique` | string | Print technique, e.g. dtg, embroidery, sublimation, digital. | | `aspect_ratio` | number \| null | Width / height of the print area. | | `width_in` | number \| null | Print area width in inches. | | `height_in` | number \| null | Print area height in inches. | | `recommended_width_px` | integer \| null | Artwork width (px) that covers the full print area at 300 DPI. | | `recommended_height_px` | integer \| null | Artwork height (px) that covers the full print area at 300 DPI. | | `min_dpi` | integer | Below this effective DPI you get a `dpi_low` warning. | | `required` | boolean | Products must include a design for this placement. | | `file_requirements` | [FileRequirements](https://swaglists.com/docs/objects.md#file_requirements) | Artwork requirements of this placement. |
Example placement ```json { "object": "placement", "key": "front", "label": "Front", "description": "Front print area", "technique": "dtg", "aspect_ratio": 0.833333, "width_in": 15, "height_in": 18, "recommended_width_px": 4500, "recommended_height_px": 5400, "min_dpi": 150, "required": false, "file_requirements": { "formats": [ "png", "jpeg", "webp", "tiff", "svg" ], "max_file_size_bytes": 104857600, "max_dimension_px": 30000, "recommended_width_px": 4500, "recommended_height_px": 5400, "recommended_dpi": 300, "min_dpi": 150, "color_profile": "sRGB", "transparent_background": true } } ```
## File requirements Artwork requirements of a placement (`placement.file_requirements`). Returned by: `GET /catalog/products/{id_or_slug}`. | Field | Type | Description | | --- | --- | --- | | `formats` | array of string | Accepted artwork formats. SVG is rasterized; PNG with transparency is recommended. | | `max_file_size_bytes` | integer | Maximum upload size in bytes. | | `max_dimension_px` | integer | Maximum width or height of an image in pixels. | | `recommended_width_px` | integer \| null | Artwork width (px) that covers the full print area at 300 DPI. | | `recommended_height_px` | integer \| null | Artwork height (px) that covers the full print area at 300 DPI. | | `recommended_dpi` | integer | Recommended print resolution. | | `min_dpi` | integer | Below this effective DPI, prints may look blurry (you receive a warning). | | `color_profile` | string | Color profile artwork is printed in. | | `transparent_background` | boolean | Transparent pixels are not printed. |
Example file requirements ```json { "formats": [ "png", "jpeg", "webp", "tiff", "svg" ], "max_file_size_bytes": 104857600, "max_dimension_px": 30000, "recommended_width_px": 4500, "recommended_height_px": 5400, "recommended_dpi": 300, "min_dpi": 150, "color_profile": "sRGB", "transparent_background": true } ```
## Mockup template A background photo plus the print-area rectangle on it, to render previews. Returned by: `GET /catalog/products/{id_or_slug}`. | Field | Type | Description | | --- | --- | --- | | `object` | "mockup_template" | Type of the object: always `mockup_template`. | | `id` | string | Unique identifier (starts with `cmt_`). | | `placement_key` | string | Placement the template shows. | | `color` | string \| null | Variant color the photo shows (null = any color). | | `label` | string \| null | Display name. | | `image_url` | string | Background photo URL. | | `image_width_px` | integer \| null | Photo width in pixels. | | `image_height_px` | integer \| null | Photo height in pixels. | | `print_area` | object | Where the print area sits on the photo, as fractions of the photo. | | `print_area.x` | number \| null | Left edge (fraction of the photo width). | | `print_area.y` | number \| null | Top edge (fraction of the photo height). | | `print_area.width` | number \| null | Width (fraction of the photo width). | | `print_area.height` | number \| null | Height (fraction of the photo height). | | `blend_mode` | string | How artwork is blended onto the photo (e.g. `multiply`). | | `is_primary` | boolean | Default template for the placement. |
Example mockup template ```json { "object": "mockup_template", "id": "cmt_01k8w1b3c5v7b9n1m3a5s7d9f1", "placement_key": "front", "color": "Black", "label": "Front", "image_url": "https://swaglists.com/f/file_01k8w1c2v4b6n8m0q2w4e6r8t0/1767225600.3f9a/tee-front.png", "image_width_px": 1600, "image_height_px": 2000, "print_area": { "x": 0.3, "y": 0.25, "width": 0.4, "height": 0.48 }, "blend_mode": "multiply", "is_primary": true } ```
## File An uploaded image (artwork or logo) or a generated mockup. Use its id as `file_id` in designs. Returned by: `POST /files`, `GET /files`, `GET /files/{id}`, `event file.processed`. | Field | Type | Description | | --- | --- | --- | | `object` | "file" | Type of the object: always `file`. | | `id` | string | Unique identifier (starts with `file_`). | | `filename` | string | File name. | | `mime_type` | string | Media type (images are stored as PNG, JPEG or WebP). | | `size_bytes` | integer | Size in bytes. | | `width_px` | integer \| null | Width in pixels. | | `height_px` | integer \| null | Height in pixels. | | `dpi` | number \| null | Resolution stored in the image metadata, when present (informational — print resolution depends on the placed size). | | `status` | string | ready to use, still processing (pending), failed processing, or deleted. One of: `pending`, `ready`, `failed`, `deleted`. | | `purpose` | string | artwork or logo (your uploads), or mockup (generated product mockups). One of: `artwork`, `logo`, `mockup`. | | `url` | string \| null | Signed download URL, valid for about 24 hours. | | `created_at` | string | When the object was created (ISO-8601, UTC). |
Example file ```json { "object": "file", "id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "filename": "logo-front.png", "mime_type": "image/png", "size_bytes": 482113, "width_px": 4500, "height_px": 5400, "dpi": 300, "status": "ready", "purpose": "artwork", "url": "https://swaglists.com/f/file_01k8x3y1q3w5e7r9t1y3u5i7o9/1767225600.ab12cd/logo-front.png", "created_at": "2026-10-21T14:32:05.000Z" } ```
## Product Your product: a catalog product + your designs + variants with your SKUs. Returned by: `POST /products`, `GET /products`, `GET /products/{id}`, `PATCH /products/{id}`, `events product.*`. | Field | Type | Description | | --- | --- | --- | | `object` | "product" | Type of the object: always `product`. | | `id` | string | Unique identifier (starts with `prod_`). | | `title` | string | Product title. | | `description` | string \| null | Product description (plain text or markdown). | | `status` | string | draft, active, or archived (archived products can no longer be ordered). One of: `draft`, `active`, `archived`. | | `catalog_product_id` | string | Catalog product this product is made from. | | `external_id` | string \| null | Your id for the product (e.g. your store's product id). | | `tags` | array of string | Your tags. | | `metadata` | map of string | Your key/value pairs (string values). | | `retail_price` | integer \| null | Default retail price of the variants (informational, used in exports), in cents. | | `currency` | "usd" | Three-letter currency code, lowercase (`usd`). | | `designs` | array of [Design](https://swaglists.com/docs/objects.md#design) | Artwork per placement. | | `variants` | array of [ProductVariant](https://swaglists.com/docs/objects.md#product_variant) | Sellable variants with your SKUs. | | `mockups` | array of object | Generated preview images. Rendered in the background; `product.updated` fires when they are ready. | | `mockups[].url` | string | Image URL that does not expire (use it in your store). Replaced when the mockups are re-rendered; the old URL then returns 404. | | `mockups[].placement` | string | Placement shown. | | `mockups[].color` | string \| null | Variant color shown (null when not color-specific). | | `mockups[].is_primary` | boolean | Main image of the product. | | `warnings` | array of [ProductWarning](https://swaglists.com/docs/objects.md#product_warning) | Only on create/update responses: non-blocking design issues. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `updated_at` | string | When the object was last changed (ISO-8601, UTC). |
Example product ```json { "object": "product", "id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "title": "Company A Logo Tee", "description": "Our classic logo on a soft tee.", "status": "active", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "external_id": "shopify-product-9921", "tags": [ "logo" ], "metadata": {}, "retail_price": 2900, "currency": "usd", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "variants": [ { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ], "mockups": [ { "url": "https://swaglists.com/f/file_01k8x3y9a1s3d5f7g9h1j3k5l7/1782777600.c0ffee/mockup-front.png", "placement": "front", "color": "Black", "is_primary": true } ], "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-21T14:32:05.000Z" } ```
## Product variant A sellable variant of your product with your SKU. Returned by: `GET /products/{id}/variants`, `GET /product_variants/{id}`, `GET /products/by_sku/{sku}`. | Field | Type | Description | | --- | --- | --- | | `object` | "product_variant" | Type of the object: always `product_variant`. | | `id` | string | Unique identifier (starts with `pvar_`). | | `product_id` | string | Product the variant belongs to. | | `sku` | string | Your SKU (unique in your account). Order items can reference it. | | `catalog_variant_id` | string | Catalog variant that is produced for this SKU. | | `title` | string | Variant title (e.g. `Black / M`). | | `options` | map of string | Option values (e.g. `{"Color": "Black", "Size": "M"}`). | | `retail_price` | integer \| null | Your retail price (informational), cents. | | `price` | integer \| null | Your wholesale price per unit with this product's designs, cents. | | `currency` | "usd" | Three-letter currency code, lowercase (`usd`). | | `external_id` | string \| null | Your id for the variant (e.g. your store's variant id). | | `enabled` | boolean | Disabled variants cannot be ordered (their SKU stays reserved). | | `available` | boolean | Whether the variant can be ordered right now: enabled, product not archived, and the catalog variant offered and in stock. `false` while it is temporarily out of stock — mark it sold out in your store; `product.updated` fires when this changes. |
Example product variant ```json { "object": "product_variant", "id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "sku": "CA-TEE-BLK-M", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "title": "Black / M", "options": { "Color": "Black", "Size": "M" }, "retail_price": 2900, "price": 1299, "currency": "usd", "external_id": "shopify-variant-4410", "enabled": true, "available": true } ```
## Product warning A non-blocking design issue returned on product create/update. Returned by: `POST /products`, `PATCH /products/{id}`. | Field | Type | Description | | --- | --- | --- | | `code` | string | Warning code: `dpi_low` (artwork below the recommended resolution) or `layer_outside_area` (a layer is outside the print area and will not print). | | `message` | string | Human-readable explanation. | | `param` | string | The design layer the warning is about. | | `placement` | string | Placement key. | | `effective_dpi` | number \| null | Effective resolution of the artwork at its placed size (resolution warnings only). |
Example product warning ```json { "code": "dpi_low", "message": "Artwork on Front is 120 DPI at this size (recommended 150+). It may print slightly blurry.", "param": "designs[0].layers[0]", "placement": "front", "effective_dpi": 120 } ```
## Design Artwork layers on one placement (as returned on products and order items). Returned by: `Product.designs`, `OrderItem.designs`. | Field | Type | Description | | --- | --- | --- | | `placement` | string | Placement key (e.g. `front`). | | `layers` | array of object | Layers, bottom to top. | | `layers[].file_id` | string | Artwork file. | | `layers[].transform` | [Transform](https://swaglists.com/docs/objects.md#transform) | Position of the layer in the print area. | | `layers[].opacity` | number | Opacity from 0 to 1. |
Example design ```json { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ```
## Transform Where a layer sits in the print area, as fractions of the area (see Designs & transforms). Returned by: `Design.layers[].transform`. | Field | Type | Description | | --- | --- | --- | | `x` | number | Left edge of the artwork box as a fraction of the print area width. | | `y` | number | Top edge of the artwork box as a fraction of the print area height. | | `width` | number | Artwork width as a fraction of the print area width. | | `height` | number | Artwork height as a fraction of the print area height. | | `rotation` | number | Clockwise rotation in degrees around the artwork center. |
Example transform ```json { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 } ```
## Shipping rate Price and delivery time of one shipping method for a destination and items. Returned by: `POST /shipping/rates`, `OrderEstimate.available_shipping_methods`. | Field | Type | Description | | --- | --- | --- | | `object` | "shipping_rate" | Type of the object: always `shipping_rate`. | | `method` | string | Shipping method (use it as `shipping_method` on orders). One of: `economy`, `standard`, `express`, `priority`. | | `name` | string | Display name of the method. | | `price` | integer | Shipping price for these items (cents). | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `min_days` | integer | Minimum transit time in business days. | | `max_days` | integer | Maximum transit time in business days. | | `estimated_delivery_days` | object | Business days from order to delivery, including production time. | | `estimated_delivery_days.min` | integer | Fastest estimate in business days. | | `estimated_delivery_days.max` | integer | Slowest estimate in business days. |
Example shipping rate ```json { "object": "shipping_rate", "method": "standard", "name": "Standard", "price": 475, "currency": "usd", "min_days": 3, "max_days": 5, "estimated_delivery_days": { "min": 5, "max": 9 } } ```
## Order An order for your end customer: items, recipient, totals, status, shipments and tracking. Returned by: `POST /orders`, `GET /orders`, `GET /orders/{id}`, `events order.*`. | Field | Type | Description | | --- | --- | --- | | `object` | "order" | Type of the object: always `order`. | | `id` | string | Unique identifier (starts with `ord_`). | | `number` | integer | Sequential human-friendly order number. | | `external_id` | string \| null | Your order id (unique per mode). Look the order up with `GET /orders/ext:`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `status` | string | draft → processing → in_production → (partially_)shipped → delivered; on_hold (see hold_reason), canceled, failed. One of: `draft`, `on_hold`, `processing`, `in_production`, `partially_shipped`, `shipped`, `delivered`, `canceled`, `failed`. | | `hold_reason` | string \| null | Why the order is on hold; only set while `status` is `on_hold`. One of: `insufficient_funds`, `manual_review`, `artwork_issue`, `address_issue`, `fulfillment_issue`. | | `funds_status` | string | State of the credits for this order: `none` (draft), `awaiting` (on hold for credits), `held` (reserved), `partially_captured`/`captured` (spent), `released` (returned unspent), `partially_refunded`/`refunded`. One of: `none`, `awaiting`, `held`, `partially_captured`, `captured`, `released`, `partially_refunded`, `refunded`. | | `source` | string | Where the order was created. One of: `api`, `dashboard`, `import`, `admin`. | | `recipient` | [Recipient](https://swaglists.com/docs/objects.md#recipient) | Shipping address of the end customer. | | `shipping_method` | string | Shipping speed of the order. One of: `economy`, `standard`, `express`, `priority`. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `subtotal` | integer | Items total in cents. | | `shipping` | integer | Shipping price in cents. | | `tax` | integer | Tax in cents (0 unless tax applies to the destination). | | `discount` | integer | Discount in cents. | | `total` | integer | Credits charged for this order (cents). | | `refunded` | integer | Credits refunded to your wallet for this order, in cents. | | `items` | array of [OrderItem](https://swaglists.com/docs/objects.md#order_item) | Line items of the order. | | `shipments` | array of [Shipment](https://swaglists.com/docs/objects.md#shipment) | Parcels shipped so far (an order can ship in several parcels). | | `tracking_page_url` | string \| null | Your branded tracking page for the end customer. null while the order is a draft (the page exists once it is confirmed). | | `packing_slip` | object \| null | Packing slip overrides of this order (null when none were sent). | | `packing_slip.message` | string \| null | Message printed on the packing slip. | | `packing_slip.brand_name` | string \| null | Brand name printed instead of your account brand name. | | `packing_slip.logo_file_id` | string \| null | The uploaded file printed as the logo on the packing slip (null: your account logo). | | `packing_slip.customer_order_number` | string \| null | Order number your customer knows (e.g. `#1042`), printed on the slip. | | `gift_message` | string \| null | Gift message included with the parcel. | | `customer_note` | string \| null | Note from your customer. | | `metadata` | map of string | Your key/value pairs (string values). | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). | | `updated_at` | string \| null | When the object was last changed (ISO-8601, UTC). | | `placed_at` | string \| null | When the order was confirmed (credits reserved, or the order put on hold for funds). | | `in_production_at` | string \| null | When production started. | | `shipped_at` | string \| null | When the order shipped. | | `delivered_at` | string \| null | When every shipment was delivered. | | `canceled_at` | string \| null | When the order was canceled. | | `cancel_reason` | string \| null | Reason given when the order was canceled. | | `failed_at` | string \| null | When the order failed. | | `failure_reason` | string \| null | Why the order failed (reserved credits were released). |
Example order ```json { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } ```
## Order item A line of an order (prices and designs are snapshotted when the order is created). Returned by: `Order.items`. | Field | Type | Description | | --- | --- | --- | | `object` | "order_item" | Type of the object: always `order_item`. | | `id` | string | Unique identifier (starts with `oi_`). | | `product_id` | string \| null | Your product (null for items ordered by catalog variant with inline designs). | | `product_variant_id` | string \| null | Your product variant (null for inline catalog items). | | `catalog_product_id` | string | Catalog product of the item. | | `catalog_variant_id` | string | Catalog variant of the item. | | `sku` | string \| null | Your SKU (null for inline catalog items). | | `title` | string | Product title at order time. | | `variant_title` | string \| null | Variant title at order time (e.g. `Black / M`). | | `quantity` | integer | Units ordered. | | `shipped_quantity` | integer | Units shipped so far. | | `unit_price` | integer | Wholesale unit price (cents). | | `total_price` | integer | `unit_price` × `quantity`, in cents. | | `retail_price` | integer \| null | Your retail unit price to the end customer (cents), as given on the order or taken from the product; null when unknown. | | `status` | string | Production status of the item. One of: `draft`, `pending`, `in_production`, `shipped`, `delivered`, `canceled`, `failed`. | | `designs` | array of [Design](https://swaglists.com/docs/objects.md#design) | Snapshot of the designs at order time. | | `mockup_url` | string \| null | Preview image of the item, when available (signed URL). | | `external_line_id` | string \| null | Your line item id. | | `metadata` | map of string | Your key/value pairs (string values). |
Example order item ```json { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ```
## Recipient The end customer's shipping address. Returned by: `Order.recipient`. | Field | Type | Description | | --- | --- | --- | | `name` | string | Full name of the recipient. | | `company` | string \| null | Company name. | | `address1` | string | Street address. | | `address2` | string \| null | Apartment, suite, unit, etc. | | `city` | string | City. | | `state_code` | string \| null | State/province code (e.g. `CA`), where applicable. | | `postal_code` | string | Postal / ZIP code (empty for countries without postal codes). | | `country_code` | string | ISO 3166-1 alpha-2 country code. | | `phone` | string \| null | Phone number (carriers may use it for delivery questions). | | `email` | string \| null | Email address (used for shipment notifications, when enabled). |
Example recipient ```json { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" } ```
## Shipment A parcel with carrier tracking. An order can ship in several parcels. Returned by: `Order.shipments`, `events shipment.*`. | Field | Type | Description | | --- | --- | --- | | `object` | "shipment" | Type of the object: always `shipment`. | | `id` | string | Unique identifier (starts with `shp_`). | | `order_id` | string | Order the parcel belongs to. | | `status` | string | Tracking status of the parcel. One of: `pending`, `label_created`, `in_transit`, `out_for_delivery`, `delivered`, `exception`, `returned`. | | `carrier` | string \| null | Carrier code (e.g. `usps`, `ups`, `fedex`, `dhl`). | | `service` | string \| null | Carrier service name (e.g. `USPS Ground Advantage`). | | `tracking_number` | string \| null | Carrier tracking number. | | `tracking_url` | string \| null | Carrier tracking page. | | `items` | array of object | Order items (and quantities) in this parcel. | | `items[].order_item_id` | string | Order item in the parcel. | | `items[].quantity` | integer | Units of that item in the parcel. | | `shipped_at` | string \| null | When the parcel was handed to the carrier. | | `estimated_delivery_at` | string \| null | Carrier's estimated delivery date, when known. | | `delivered_at` | string \| null | When the parcel was delivered. | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). |
Example shipment ```json { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ```
## Order event An entry of an order's timeline. Returned by: `GET /orders/{id}/events`. | Field | Type | Description | | --- | --- | --- | | `object` | "order_event" | Type of the object: always `order_event`. | | `id` | string | Unique identifier (starts with `oev_`). | | `type` | string | Kind of entry (for example `confirmed`, `status_changed`, `on_hold`, `shipment_created`, `canceled`). | | `message` | string | Human-readable description. | | `from_status` | string \| null | Order status before the change (status changes only). | | `to_status` | string \| null | Order status after the change (status changes only). | | `created_at` | string \| null | When the object was created (ISO-8601, UTC). |
Example order event ```json { "object": "order_event", "id": "oev_01k8x4b2qc5v7b9n1m3k5j7h9g", "type": "status_changed", "message": "Order is in production", "from_status": "processing", "to_status": "in_production", "created_at": "2026-10-21T14:32:05.000Z" } ```
## Order estimate Exactly what an order would cost; nothing is created. Returned by: `POST /orders/estimate`. | Field | Type | Description | | --- | --- | --- | | `object` | "order_estimate" | Type of the object: always `order_estimate`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `currency` | string | Three-letter currency code, lowercase (`usd`). | | `shipping_method` | string | Shipping method the estimate was priced with. One of: `economy`, `standard`, `express`, `priority`. | | `items` | array of object | Priced items, in the order you sent them. | | `items[].index` | integer | Position of the item in your `items` array. | | `items[].catalog_variant_id` | string | Catalog variant that will be produced. | | `items[].product_variant_id` | string \| null | Your product variant (null for inline catalog items). | | `items[].sku` | string \| null | Your SKU (null for inline catalog items). | | `items[].title` | string | Product title. | | `items[].variant_title` | string \| null | Variant title. | | `items[].quantity` | integer | Units. | | `items[].unit_price` | integer | Wholesale unit price in cents. | | `items[].total_price` | integer | `unit_price` × `quantity`, in cents. | | `subtotal` | integer | Items total in cents. | | `shipping` | integer | Shipping price in cents. | | `tax` | integer | Tax in cents. | | `total` | integer | Exactly what POST /orders would charge. | | `available_shipping_methods` | array of [ShippingRate](https://swaglists.com/docs/objects.md#shipping_rate) | Every shipping method you could choose, with its price. | | `estimated_delivery_days` | object | Business days from order to delivery, including production. | | `estimated_delivery_days.min` | integer | Fastest estimate in business days. | | `estimated_delivery_days.max` | integer | Slowest estimate in business days. | | `wallet_available` | integer | Available credits (cents). | | `sufficient_funds` | boolean | Whether your available credits cover `total`. |
Example order estimate ```json { "object": "order_estimate", "livemode": true, "currency": "usd", "shipping_method": "standard", "items": [ { "index": 0, "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "unit_price": 1299, "total_price": 2598 } ], "subtotal": 2598, "shipping": 700, "tax": 0, "total": 3298, "available_shipping_methods": [ { "object": "shipping_rate", "method": "standard", "name": "Standard", "price": 475, "currency": "usd", "min_days": 3, "max_days": 5, "estimated_delivery_days": { "min": 5, "max": 9 } } ], "estimated_delivery_days": { "min": 5, "max": 9 }, "wallet_available": 482550, "sufficient_funds": true } ```
## Webhook endpoint A URL we POST events to. Returned by: `POST /webhook_endpoints`, `GET /webhook_endpoints`, `GET /webhook_endpoints/{id}`. | Field | Type | Description | | --- | --- | --- | | `object` | "webhook_endpoint" | Type of the object: always `webhook_endpoint`. | | `id` | string | Unique identifier (starts with `we_`). | | `url` | string | URL we POST events to. | | `description` | string \| null | Your description. | | `enabled_events` | array of string | Event types delivered to this endpoint; ["*"] = all. | | `status` | string | `enabled` endpoints receive events. One of: `enabled`, `disabled`. | | `disabled_reason` | string \| null | Why the endpoint was disabled: `disabled_by_user`, `failing_deliveries` (every delivery failed for 5 days) or `endpoint_gone` (it answered HTTP 410). | | `mode` | string | `live` or `test`. One of: `live`, `test`. | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `created_at` | string | When the object was created (ISO-8601, UTC). | | `secret` | string | Signing secret (whsec_…). Only returned when the endpoint is created and when the secret is rotated — store it safely. |
Example webhook endpoint ```json { "object": "webhook_endpoint", "id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "url": "https://company-a.example/webhooks/print", "description": "Production order updates", "enabled_events": [ "order.shipped", "order.delivered" ], "status": "enabled", "disabled_reason": null, "mode": "live", "livemode": true, "created_at": "2026-10-21T14:32:05.000Z" } ```
## Webhook delivery One event delivered (or being delivered) to one endpoint. Returned by: `POST /webhook_endpoints/{id}/test`. | Field | Type | Description | | --- | --- | --- | | `object` | "webhook_delivery" | Type of the object: always `webhook_delivery`. | | `id` | string | Unique identifier (starts with `whd_`). | | `event_id` | string | Event delivered. | | `endpoint_id` | string | Endpoint delivered to. | | `status` | string | `pending`, `retrying`, `succeeded` or `failed`. One of: `pending`, `retrying`, `succeeded`, `failed`. | | `attempts` | integer | Attempts made so far. | | `response_status` | integer \| null | HTTP status of the last attempt. | | `next_attempt_at` | string \| null | When the next attempt is scheduled. | | `last_attempt_at` | string \| null | When the last attempt was made. | | `created_at` | string | When the object was created (ISO-8601, UTC). |
Example webhook delivery ```json { "object": "webhook_delivery", "id": "whd_01k8x4b2r3e5w7q9a1s3d5f7g9", "event_id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "endpoint_id": "we_01k8x2m4n6b8v0c2x4z6a8s0d2", "status": "pending", "attempts": 0, "response_status": null, "next_attempt_at": "2026-10-21T14:32:05.000Z", "last_attempt_at": null, "created_at": "2026-10-21T14:32:05.000Z" } ```
## Event Something that happened in your account — also the exact JSON body POSTed to your webhook endpoints. Returned by: `GET /events`, `GET /events/{id}`, `webhook deliveries`. | Field | Type | Description | | --- | --- | --- | | `id` | string | Event id. Also sent as the `webhook-id` header and stable across retries — deduplicate on it. | | `object` | "event" | Type of the object: always `event`. | | `type` | string | Event type (e.g. `order.shipped`). | | `api_version` | string | API version the payload was rendered with. | | `created_at` | string | When the event happened (ISO-8601, UTC). | | `livemode` | boolean | `true` in live mode, `false` in test mode. | | `data` | map of any | `object` is the resource as of the event (same shape as the API returns); `previous_attributes` lists changed fields on *.updated events. |
Example event ```json { "id": "evt_01k8x4b2r1t3y5u7i9o1p3a5s7", "object": "event", "type": "order.shipped", "api_version": "2026-10-01", "created_at": "2026-10-21T14:32:05.000Z", "livemode": true, "data": { "object": { "object": "order", "id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "number": 1042, "external_id": "1042", "livemode": true, "status": "shipped", "hold_reason": null, "funds_status": "captured", "source": "api", "recipient": { "name": "Jane Doe", "company": null, "address1": "123 Market St", "address2": "Apt 4", "city": "San Francisco", "state_code": "CA", "postal_code": "94107", "country_code": "US", "phone": null, "email": "jane@example.com" }, "shipping_method": "standard", "currency": "usd", "subtotal": 2598, "shipping": 700, "tax": 0, "discount": 0, "total": 3298, "refunded": 0, "items": [ { "object": "order_item", "id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "product_id": "prod_01k8x3z7r1t3y5u7i9o1p3a5s7", "product_variant_id": "pvar_01k8x3z7r4f6g8h0j2k4l6z8x0", "catalog_product_id": "cp_01k8w1a2s3d4f5g6h7j8k9m0n1", "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "sku": "CA-TEE-BLK-M", "title": "Company A Logo Tee", "variant_title": "Black / M", "quantity": 2, "shipped_quantity": 2, "unit_price": 1299, "total_price": 2598, "retail_price": 2500, "status": "shipped", "designs": [ { "placement": "front", "layers": [ { "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9", "transform": { "x": 0.1, "y": 0.05, "width": 0.8, "height": 0.6, "rotation": 0 }, "opacity": 1 } ] } ], "mockup_url": null, "external_line_id": "line-1", "metadata": {} } ], "shipments": [ { "object": "shipment", "id": "shp_01k8x9z1m3n5b7v9c1x3z5a7s9", "order_id": "ord_01k8x4b2q9w7e3r5t1y8u6i0pa", "status": "in_transit", "carrier": "usps", "service": "USPS Ground Advantage", "tracking_number": "9400111899223197428490", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490", "items": [ { "order_item_id": "oi_01k8x4b2qa3s5d7f9g1h3j5k7l", "quantity": 2 } ], "shipped_at": "2026-10-23T09:12:44.000Z", "estimated_delivery_at": null, "delivered_at": null, "created_at": "2026-10-23T09:12:44.000Z" } ], "tracking_page_url": "https://swaglists.com/track/Zx81kLm2pQ", "packing_slip": { "message": "Thanks for shopping with Company A!", "brand_name": null, "logo_file_id": null, "customer_order_number": "#1042" }, "gift_message": null, "customer_note": null, "metadata": {}, "created_at": "2026-10-21T14:32:05.000Z", "updated_at": "2026-10-23T09:12:44.000Z", "placed_at": "2026-10-21T14:32:05.000Z", "in_production_at": "2026-10-21T14:32:05.000Z", "shipped_at": "2026-10-23T09:12:44.000Z", "delivered_at": null, "canceled_at": null, "cancel_reason": null, "failed_at": null, "failure_reason": null } } } ```
## Error Every error response has this body. Returned by: `any 4xx/5xx response`. | Field | Type | Description | | --- | --- | --- | | `error` | object | The error. | | `error.type` | string | Category of the error. One of: `invalid_request_error`, `authentication_error`, `permission_error`, `not_found_error`, `conflict_error`, `idempotency_error`, `rate_limit_error`, `insufficient_funds_error`, `api_error`. | | `error.code` | string | Stable, machine-readable error code (see doc_url). | | `error.message` | string | Human-readable explanation; safe to log, not meant for end customers. | | `error.param` | string | The request parameter the error relates to, e.g. items[0].quantity. | | `error.doc_url` | string | Documentation for this error code. | | `error.request_id` | string | Id of the request (also in the Request-Id header). Quote it when contacting support. | | `error.required_amount` | integer | insufficient_funds only: credits required (cents). | | `error.available_amount` | integer | insufficient_funds only: credits available (cents). | | `error.currency` | string | Currency of the amounts (insufficient_funds only). | | `error.existing_id` | string | duplicate_external_id / resource_already_exists only: id of the existing resource (e.g. the order that already has this external_id). | | `error.required_scope` | string | insufficient_scope only: the scope the key is missing. | | `error.errors` | array of object | Validation errors only: every problem found in the request (at most 50); the first one is the top-level code / param / message. Fix them all before retrying. Up to 50 items. | | `error.errors[].param` | string \| null | The parameter of this problem (null for the request as a whole). | | `error.errors[].code` | string | Error code of this problem (usually parameter_invalid). | | `error.errors[].message` | string | Human-readable explanation of this problem. | | `error.allowed_values` | array of string or number or boolean | For a value outside a fixed set (e.g. a placement key): the accepted values (`[true, false]` for a yes/no customization). | | `error.available_shipping_methods` | array of string | shipping_method_unavailable: the shipping methods that are available for this request; recipient errors another method would avoid (po_box_not_supported, recipient_phone_required, recipient_email_required): the methods that would deliver. | | `error.max_length` | integer | For a field that is too long: the largest length (in characters) that can be accepted. | | `error.max_quantity` | integer | For a quantity that is too large: the largest quantity that can be accepted. |
Example error ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "Invalid items[0].quantity: must be greater than or equal to 1.", "param": "items[0].quantity", "doc_url": "https://swaglists.com/docs/errors#parameter_invalid", "request_id": "req_01k8x4b2q7w9e1r3t5y7u9i1o3" } } ```
## Catalog price change The `data.object` of `catalog.price_changed` events. | Field | Type | Description | | --- | --- | --- | | `object` | string | Always `catalog_price_change`. | | `catalog_product_id` | string | Catalog product whose wholesale prices changed. | | `catalog_variant_ids` | array of string | Changed catalog variants that your products use. | | `reason` | string | One of: `cost_increase`, `cost_decrease`, `cost_change`, `pricing_change`. | --- # Changelog > Changes to the API and the developer documentation, newest first. Source: https://swaglists.com/docs/changelog (markdown: https://swaglists.com/docs/changelog.md) Backwards-compatible additions ship continuously; breaking changes only in a new API version (see [Versioning](https://swaglists.com/docs/versioning.md)). ## 2026-10-24 — Order requirements checked up front Orders, estimates and confirmations now check **what the production options of every item can accept before anything is created or reserved**. An order that no option could make used to be accepted, had its credits reserved, and failed hours later; it is now a `400` that names the field to fix. If at least one option fits, the order goes through as before. - **Recipient** — `recipient_field_too_long` (with `max_length`: the longest value at least one option accepts, counted in characters as people count them), `recipient_phone_required`, `recipient_email_required`, `recipient_tax_id_required`, `recipient_state_required`, `po_box_not_supported`, `military_address_not_supported` and `recipient_local_script_required`, each with `param` on the recipient field (`recipient.city`, `recipient.phone`, …). Messages never repeat the recipient's data. - **Shipping method** — `shipping_method_unavailable` now also comes with `available_shipping_methods` when the chosen method is the only problem. `POST /shipping/rates` only lists methods every item can actually be shipped with to that destination, and prices each method with the grouping an order with that method gets. - **Quantities** — `quantity_too_large` (with `max_quantity`, `param` = `items[i].quantity`) when the item's production options take fewer units per order. An order can have at most 10,000 units in total (`400 parameter_invalid`, `param: items`). - **Customization options** — when a catalog product has options beyond its variants (a frame color, thread colors…), orders of saved products get their values from the product and the options' defaults; problems are reported with the new `customization_*` codes on `items[i].customizations.`, every one of them in `errors`. Yes/no options report `allowed_values: [true, false]`. - **Artwork URLs** — the `url` layers of an order are downloaded a few at a time within one time budget per order (45 seconds); when it runs out the request fails with `artwork_download_budget_exceeded` instead of running into a gateway timeout. An order can reference at most 50 different artwork URLs (`400 parameter_invalid`, `param: items`); upload larger sets with `POST /files` first. - **Errors** — an `item_unavailable` error can list `placement_unavailable` in `errors` when no production option prints on one of the item's placements. See [Errors](https://swaglists.com/docs/errors.md). ## 2026-10-24 — Stricter input validation **Unknown query parameters are now rejected** with `400 parameter_invalid`, exactly like unknown body parameters. Before, they were ignored — so a misspelled or unsupported filter such as `GET /orders?updated_since=…` or `?statuss=failed` silently returned *every* order. The error names the parameter (`param`, message `Received unknown parameter: updated_since (query parameter).`). The query parameter `_` stays accepted and ignored on every endpoint for cache-busters. This ships without a new API version because it only rejects requests that never worked as intended; if your integration appends its own tracking parameters to API URLs, remove them. - **Cursors** — `starting_after` / `ending_before` must be the id of an object of that list (`ord_…` on `/orders`, `prod_…` on `/products`, `evt_…` on `/events`, …); another resource's id is a `400` instead of a wrong page. See [Pagination](https://swaglists.com/docs/pagination.md#cursors). - **Timestamps** — `created[gte]` / `created[lte]` read a date-time without an offset (`2026-10-01T00:00:00`) as **UTC** (it depended on the server's time zone before) and accept Unix milliseconds; impossible dates (`2026-02-30`) and years outside 2000–2100 are a `400`. See [Pagination](https://swaglists.com/docs/pagination.md#time-filters). - **Text** — NUL characters and unpaired UTF-16 surrogates are rejected with `400 parameter_invalid` naming the field on every endpoint (JSON bodies, form uploads, query parameters, path ids, `Idempotency-Key`); they used to cause a `500` on some endpoints. Recipient names and address lines are stored in Unicode NFC with control and bidi-override characters removed. - **Empty strings** — `""` in an optional recipient field (`company`, `address2`, `state_code`, `postal_code`, `phone`, `tax_id`, `email`) means "not set", for no-code tools that send empty strings for unmapped fields (`state_code: ""` used to be a `400`). - **Errors** — validation errors list every problem of the request in `error.errors` (`param`, `code`, `message`), and errors carry the accepted values or limits where they apply: `allowed_values`, `available_shipping_methods` (`shipping_method_unavailable`), `max_length`, `max_quantity`. See [Errors](https://swaglists.com/docs/error-handling.md). - **Orders** — `packing_slip.logo_file_id` is now returned, so a read-modify-write of `packing_slip` keeps the logo. - **Product webhooks** — `product.created`, `product.updated` and `product.deleted` events carry every variant's wholesale `price`, the same object as `GET /products/{id}` (it was `null` in events). ## 2026-10-24 — Every order lifecycle step - **Webhooks** — every order lifecycle step is now sent exactly once, also when one status update covers several steps (for example an order produced, shipped and delivered between two status checks). The skipped `order.in_production` and `order.shipped` events are created with that update, in lifecycle order, interleaved with the `shipment.*` events; each carries the current object and `previous_attributes.status` names the previous step. Their deliveries are spaced about a second apart. See [Every lifecycle step, exactly once](https://swaglists.com/docs/webhooks.md#lifecycle-steps). - **Orders** — `updated_at` now changes with every change visible to you, including parcel-only updates such as "out for delivery", and always moves forward. ## 2026-10-01 — API v1 The first version of the Swaglists API (`Api-Version: 2026-10-01`). - **Catalog** — `GET /catalog/products`, `GET /catalog/products/{id_or_slug}`, `GET /catalog/categories` with your wholesale prices, placements, recommended artwork sizes and mockup templates. - **Files** — upload artwork by multipart upload or by URL (`POST /files`), list, retrieve and delete files. - **Products** — create products from catalog products and designs, manage variants and SKUs (`/products`, `/products/{id}/variants`, `/product_variants/{id}`, `/products/by_sku/{sku}`). - **Shipping** — `POST /shipping/rates` for every method with price and delivery estimate. - **Orders** — create (by SKU, product variant or catalog variant with inline designs), estimate, update drafts, confirm, cancel, list with filters, look up by `ext:`, order timeline. - **Credits** — wallet, transactions and top-ups. - **Webhooks** — endpoints, Standard Webhooks signatures, secret rotation with overlap, test events, `GET /events` with 30-day retention. - **Test mode** — `sk_test_` keys, free test credits (`POST /test/wallet/fund`), simulated production and `POST /test/orders/{id}/advance`, magic values for failure scenarios. - **Conventions** — idempotency keys, cursor pagination, Stripe-style errors with documentation links, rate limit headers, request ids. - **Docs for AI** — `/llms.txt`, `/llms-full.txt` and a Markdown version of every docs page. --- # FAQ > Short answers to the questions developers ask most. Source: https://swaglists.com/docs/faq (markdown: https://swaglists.com/docs/faq.md) ## Getting started ### Do I need to create products before I can order? No. You can order a catalog variant with inline designs ([personalized orders](https://swaglists.com/docs/personalized-orders.md)). For a regular store, creating products is easier: each variant gets a SKU your store sends with orders. ### Does test mode cost anything? No. Test keys use a separate wallet with free test credits, and production is simulated. See [Test mode](https://swaglists.com/docs/test-mode.md). ### Can I use the API from my storefront's JavaScript? No. Secret keys must stay on your server. Call the API from your backend and expose only what your frontend needs (for example shipping options). ### Is there an SDK? The API is plain JSON over HTTPS and works with any HTTP client; every page has cURL, Node.js and Python examples. You can generate a typed client from the [OpenAPI document](https://swaglists.com/api/v1/openapi.json). For webhooks, use the official Standard Webhooks libraries ([Webhooks](https://swaglists.com/docs/webhooks.md#verify-signatures)). ## Orders ### How do I avoid duplicate orders? Set `external_id` to your store's order id and send an `Idempotency-Key` derived from it. A duplicate returns `409 duplicate_external_id` with the `existing_id`. See [Idempotency](https://swaglists.com/docs/idempotency.md). ### When are credits taken? They are reserved when the order is confirmed and spent once it is accepted for production; canceled or failed orders give them back. See [Credits & billing](https://swaglists.com/docs/billing.md#order-flow). ### My order is `on_hold`. What do I do? Check `hold_reason`. `insufficient_funds` resolves automatically when you add credits; most other reasons are handled by our team. See [hold reasons](https://swaglists.com/docs/orders.md#hold-reasons). ### Can I change an order after creating it? Only drafts can be changed (`PATCH /orders/{id}`). A confirmed order can be canceled while it is not yet in production; create a corrected order afterwards. ### Can I cancel an order? Yes, while it is `draft`, `on_hold` or `processing`. Once it is `in_production` it can no longer be canceled. Credits are returned to your wallet immediately. ### How long does production and delivery take? Each catalog product lists `production_days`; shipping rates list transit time and `estimated_delivery_days` (production + transit, in business days). ### Which countries do you ship to? `POST /shipping/rates` tells you for any destination and set of items. If nothing can ship there, order creation fails with `shipping_method_unavailable`. ## Products & artwork ### What artwork resolution do I need? 300 DPI at the printed size is ideal; the placement's `recommended_width_px` × `recommended_height_px` covers the full area. Below 150 DPI you get a warning, below 75 DPI the artwork is rejected. See [Designs](https://swaglists.com/docs/designs.md#resolution). ### Which file formats are accepted? PNG (recommended, with transparency), JPEG, WebP, TIFF, GIF and SVG, up to 100 MB. ### Can I use my existing store SKUs? Yes — set `sku` on each variant when you create the product, or change it later. SKUs must be unique in your account. See [Products & SKUs](https://swaglists.com/docs/products.md#skus). ### Do my customers see your name anywhere? We never put our name in front of your customers: the tracking page and shipment emails use your branding, packing slips carry your brand wherever the product ships with one, and tracking links go to the carrier. ## Webhooks ### My webhook signatures do not verify. Almost always the body was parsed and re-serialized before verification. Verify the raw bytes, use the secret of the right endpoint and mode (test and live differ), and make sure your server clock is correct (5-minute tolerance). ### I missed webhooks while my server was down. We retry for about 28 hours. After that, list missed events with `GET /events` (30 days) or resend them from **Developers → Events**. ### Can events arrive twice or out of order? Yes. Deduplicate by event `id` and compare statuses/timestamps instead of assuming order. See [best practices](https://swaglists.com/docs/webhooks.md#best-practices). ## Account ### How do I go live? Verify your email, buy credits, create a live key and a live webhook endpoint, then follow the [going live checklist](https://swaglists.com/docs/going-live.md). ### Who can see or rotate API keys? Every member can see the list of keys (never the secrets) in **Developers → API keys**; owners, admins and developers can create, roll and revoke them. ### Where do I get help? Email support@swaglists.com and include the `request_id` of any failing request.