Skip to content

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.

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

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

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), fetch the order (GET /orders/{order_id}) and parse its external_id (bc:<store>:<order>:<address>). Then create the shipment in BigCommerce — external_line_id holds the BigCommerce order product id:

JavaScript
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:<store_hash>:<order_id>:<shipping address id>
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.