Skip to content

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):

  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 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 }));
}

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})`);
}

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.
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:<your id> for lookups.