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.
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 webhookBefore 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 -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
// 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:
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.