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.
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):
- At checkout — show shipping options and prices:
POST /shipping/rates(orPOST /orders/estimatefor the full total). - After payment succeeds — create the order:
POST /orders. - Afterwards — receive webhooks and update your order, email your customer, show tracking.
1. Shipping options at checkout
// 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 }));
}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"]]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.
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})`);
}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']})")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.
3. Keep your order in sync
Subscribe a webhook endpoint 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). |
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. |
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-Keyfrom your order id. - Shipping options from
POST /shipping/rates, mapped toshipping_method. - Webhook endpoint with signature verification and deduplication by event id.
- Our order id stored on your order;
GET /orders/ext:<your id>for lookups.