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