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