Diagnostic Inventory

BigCommerce variant inventory sum silently fails to save past int32 max

You PUT a new inventory_level, BigCommerce answers 200, and the number on the variant does not move. Nothing errors. Nothing warns you. The Catalog API stores inventory_level as a 32-bit signed integer with a ceiling of 2147483647, and it checks that ceiling against the product's summed variant inventory, not just the one variant you touched. Once a write would push that sum over the edge, the platform quietly keeps the old value and tells you everything is fine. Here is why that happens and a small check that catches it before you ever trust the response.

Python and Node.js BigCommerce Catalog v3 API Report only, no auto-write
Photo of library interior
Photo by Cameron Cress on Unsplash
The short answer

BigCommerce's Catalog v3 API stores inventory_level as a 32-bit signed integer, capped at 2147483647, and it enforces that cap against the product's summed variant inventory. A write via PUT /v3/catalog/products/{id}/variants/{variant_id}, the Update Products batch endpoint, or POST /v3/inventory/adjustments/absolute or /relative that would push that sum past the ceiling does not get clamped and does not get rejected with a validation error. It returns HTTP 200 and simply does not persist the new value. Detect this by re-reading the variant after every write and comparing it to what you attempted to set, and by summing all of a product's variant.inventory_level values and comparing against the parent product's own inventory_level before you ever issue the write. Full code and a pure predicate function are below.

The problem in plain words

A 32-bit signed integer maxes out at 2147483647. That is the ceiling BigCommerce's Catalog v3 API uses for inventory_level, on both the product and every one of its variants. Most stores never get near that number on a single SKU. But the cap is not just per variant. BigCommerce also checks the product's summed variant inventory against the same ceiling, so a product with many variants, or one fed by an ERP integration that occasionally emits a garbage-large delta, can hit the wall even when no single variant looks unreasonable on its own.

When a write would push that sum over 2147483647, whether it comes through PUT /v3/catalog/products/{product_id}/variants/{variant_id}, the Update Products batch endpoint, or POST /v3/inventory/adjustments/absolute or /relative, the Catalog API does not clamp the value down to the max and does not return a validation error explaining what happened. It returns the same HTTP 200 you would get from a normal successful write, with a response body that looks unremarkable, and the stored inventory_level is left exactly where it was before the call. Nothing in the response tells you the write was dropped.

Integration sends new inventory_level Sum exceeds 2147483647 Write dropped HTTP 200 value unchanged Systems believe it succeeded
The API replies exactly like a successful write. The stored inventory_level never moved, and nothing in the response says why.

Why it happens

The ceiling is a storage-format limit, not a business rule, and BigCommerce applies it at more than one layer:

Merchants asking how to reliably read variant stock levels have run into the gap between what the API reports success on and what actually persisted; see the citations at the end for the exact docs and support threads.

The key insight

HTTP 200 from BigCommerce's Catalog API does not mean your value was saved. It only means the request was well formed. The only proof a write took effect is re-reading the variant afterward and comparing it to what you sent. And because the ceiling applies to the summed variant inventory, not just the field you touched, you can predict the failure before you ever call the API: fetch every other variant's current inventory_level, add the new value you are about to write, and check that sum against 2147483647 first.

The fix, as a flow

We do not change how BigCommerce stores or caps inventory_level. We add a pure predicate that runs before every write, using the pre-fetched inventory levels of every other variant on the product, and we verify after every write by re-reading the variant. Anything the predicate flags, or anything the post-write re-read shows unchanged, gets reported. Nothing is auto-corrected.

List variants GET current levels Compute projected sum others + new_level Sum over 2147483647? yes, skip write and report no Report mismatch Write, then re-GET confirm value changed
The predicate runs before the write and needs no network access. The re-read after the write catches anything the predicate could not see coming, like stale list-cache reads.

Build it step by step

1

Get a store hash and an API access token

Create an API account in your BigCommerce control panel under Settings, API, or use the store's existing app credentials. Grant it Products (modify) scope so it can read variants and write inventory_level. You need the store hash from your control panel URL and the access token, sent on every call as the X-Auth-Token header. Keep both in environment variables, never in the file.

setup (shell)
pip install requests

export BIGCOMMERCE_STORE_HASH="abc123"
export BIGCOMMERCE_ACCESS_TOKEN="..."
export DRY_RUN="true"   # start safe, change to false to write
setup (shell)
// Node 18+ has fetch built in, no dependencies needed

export BIGCOMMERCE_STORE_HASH="abc123"
export BIGCOMMERCE_ACCESS_TOKEN="..."
export DRY_RUN="true"   // start safe, change to false to write
2

Talk to the Catalog v3 REST API

Every call goes to https://api.bigcommerce.com/stores/{store_hash}/v3/ with the token in the X-Auth-Token header. V3 responses wrap the payload in {data, meta.pagination}. A small helper handles GET and PUT and raises on a non-2xx response. We reuse it to list variants, read the parent product, and write an inventory_level.

step2.py
import os, requests

STORE_HASH = os.environ["BIGCOMMERCE_STORE_HASH"]
ACCESS_TOKEN = os.environ["BIGCOMMERCE_ACCESS_TOKEN"]
API_BASE = f"https://api.bigcommerce.com/stores/{STORE_HASH}/v3"

HEADERS = {
    "X-Auth-Token": ACCESS_TOKEN,
    "Content-Type": "application/json",
    "Accept": "application/json",
}

def bc_get(path, params=None):
    r = requests.get(f"{API_BASE}{path}", headers=HEADERS, params=params or {}, timeout=30)
    r.raise_for_status()
    return r.json()

def bc_put(path, body):
    r = requests.put(f"{API_BASE}{path}", headers=HEADERS, json=body, timeout=30)
    r.raise_for_status()
    return r.json()
step2.js
const STORE_HASH = process.env.BIGCOMMERCE_STORE_HASH;
const ACCESS_TOKEN = process.env.BIGCOMMERCE_ACCESS_TOKEN;
const API_BASE = `https://api.bigcommerce.com/stores/${STORE_HASH}/v3`;

const HEADERS = {
  "X-Auth-Token": ACCESS_TOKEN,
  "Content-Type": "application/json",
  Accept: "application/json",
};

async function bcGet(path, params = {}) {
  const url = new URL(`${API_BASE}${path}`);
  for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
  const res = await fetch(url, { headers: HEADERS });
  if (!res.ok) throw new Error(`BigCommerce ${res.status}`);
  return res.json();
}

async function bcPut(path, body) {
  const res = await fetch(`${API_BASE}${path}`, { method: "PUT", headers: HEADERS, body: JSON.stringify(body) });
  if (!res.ok) throw new Error(`BigCommerce ${res.status}`);
  return res.json();
}
3

List every variant's current inventory_level

Call GET /v3/catalog/products/{product_id}/variants paginated with limit=250 and include_fields=id,sku,inventory_level, so the predicate has the current level of every variant on the product. Separately call GET /v3/catalog/products/{product_id}?include=variants to read the parent's own inventory_level, since a mismatch between the two is itself a signal a past write was silently dropped.

step3.py
def list_variants(product_id):
    page = 1
    variants = []
    while True:
        resp = bc_get(f"/catalog/products/{product_id}/variants", {
            "limit": 250,
            "page": page,
            "include_fields": "id,sku,inventory_level",
        })
        rows = resp.get("data", [])
        if not rows:
            return variants
        variants.extend(rows)
        page += 1

def get_product_inventory_level(product_id):
    resp = bc_get(f"/catalog/products/{product_id}", {"include": "variants"})
    return resp["data"]["inventory_level"]
step3.js
async function listVariants(productId) {
  const variants = [];
  let page = 1;
  while (true) {
    const resp = await bcGet(`/catalog/products/${productId}/variants`, {
      limit: 250,
      page,
      include_fields: "id,sku,inventory_level",
    });
    const rows = resp.data || [];
    if (!rows.length) return variants;
    variants.push(...rows);
    page += 1;
  }
}

async function getProductInventoryLevel(productId) {
  const resp = await bcGet(`/catalog/products/${productId}`, { include: "variants" });
  return resp.data.inventory_level;
}
4

Predict the silent failure with one pure function

Before ever writing, sum every other variant's inventory_level, add the new value you are about to set on the target variant, and compare that projected sum to int32 max. This function takes only plain values, the pre-fetched list of (id, level) tuples, the target variant_id, and the proposed new_level, so it needs no network access and is fully testable on its own.

predicate.py
from typing import NamedTuple

INT32_MAX = 2147483647

class VariantLevel(NamedTuple):
    id: int
    level: int

def would_overflow_and_be_dropped(
    current_variant_levels: list, variant_id: int, new_level: int, int32_max: int = INT32_MAX
):
    total_excluding_target = sum(
        v.level for v in current_variant_levels if v.id != variant_id
    )
    projected_sum = total_excluding_target + new_level
    is_unsafe = projected_sum > int32_max or new_level > int32_max
    return is_unsafe, projected_sum
predicate.js
const INT32_MAX = 2147483647;

function wouldOverflowAndBeDropped(currentVariantLevels, variantId, newLevel, int32Max = INT32_MAX) {
  const totalExcludingTarget = currentVariantLevels
    .filter((v) => v.id !== variantId)
    .reduce((sum, v) => sum + v.level, 0);
  const projectedSum = totalExcludingTarget + newLevel;
  const isUnsafe = projectedSum > int32Max || newLevel > int32Max;
  return [isUnsafe, projectedSum];
}
5

Write, then re-GET to confirm it actually took effect

When the predicate says the write is safe, issue it with PUT /v3/catalog/products/{product_id}/variants/{variant_id} and body {"inventory_level": new_level}, or POST /v3/inventory/adjustments/absolute for a bulk adjustment. Then re-GET the same variant directly, not from a cached list response, and compare. If the stored inventory_level still equals the value from before the call while the API returned 200, the write was silently dropped and belongs in the report, regardless of what the predicate said, since stale list-cache reads can mask the same symptom.

apply.py
def get_variant(product_id, variant_id):
    resp = bc_get(f"/catalog/products/{product_id}/variants/{variant_id}")
    return resp["data"]

def write_variant_inventory_level(product_id, variant_id, new_level):
    return bc_put(
        f"/catalog/products/{product_id}/variants/{variant_id}",
        {"inventory_level": new_level},
    )
apply.js
async function getVariant(productId, variantId) {
  const resp = await bcGet(`/catalog/products/${productId}/variants/${variantId}`);
  return resp.data;
}

async function writeVariantInventoryLevel(productId, variantId, newLevel) {
  return bcPut(`/catalog/products/${productId}/variants/${variantId}`, {
    inventory_level: newLevel,
  });
}
6

Wire it together with a dry run guard, report only

The loop ties every piece together. On the first few runs, leave DRY_RUN on so the script only reports each {product_id, variant_id, sku, attempted_inventory_level, current_persisted_inventory_level, projected_sum} it would flag, whether the predicate caught it ahead of time or the post-write re-read caught it after. Nothing is auto-corrected, even with DRY_RUN off. A separate, explicit clamp flag exists for a human to invoke on purpose, never inferred automatically from the overflowed input.

Run it safe

Always start with DRY_RUN=true, and treat every flagged mismatch as a question for the merchant, not an answer to compute yourself. A number large enough to overflow int32 could be a genuine mistake or a deliberate aggregate SKU, and only a human can tell which.

The full code

Here is the complete script in one file for each language. It reads settings from the environment, predicts the overflow before writing whenever possible, verifies with a re-read after every write, and only reports mismatches. It never guesses at a corrected value on its own.

View this code on GitHub Full runnable folder with tests in the bigcommerce-fixes repo.

check_inventory_overflow.py
"""Detect BigCommerce variant inventory writes that silently fail past int32 max.

BigCommerce's Catalog v3 API stores inventory_level as a 32-bit signed integer
with a ceiling of 2147483647, and it enforces that ceiling against the product's
summed variant inventory, not just the single variant being written. A write via
PUT /v3/catalog/products/{id}/variants/{variant_id}, the Update Products batch
endpoint, or POST /v3/inventory/adjustments/absolute|relative that would push that
sum over the ceiling does not get clamped and does not return a validation error.
It returns HTTP 200 and the stored inventory_level is left unchanged. This job
predicts the overflow before writing using only pre-fetched variant levels (no
network call needed for the decision itself), and after every write it re-reads
the same variant directly to confirm the value actually changed. Everything it
finds is reported, nothing is auto-corrected. Safe to run again and again.

Guide: https://www.allanninal.dev/bigcommerce/inventory-int32-overflow-silent-failure/
"""
import os
import logging
from typing import NamedTuple

import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("check_inventory_overflow")

STORE_HASH = os.environ["BIGCOMMERCE_STORE_HASH"]
ACCESS_TOKEN = os.environ["BIGCOMMERCE_ACCESS_TOKEN"]
API_BASE = f"https://api.bigcommerce.com/stores/{STORE_HASH}/v3"
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"

INT32_MAX = 2147483647

HEADERS = {
    "X-Auth-Token": ACCESS_TOKEN,
    "Content-Type": "application/json",
    "Accept": "application/json",
}


class VariantLevel(NamedTuple):
    id: int
    level: int


def bc_get(path, params=None):
    r = requests.get(f"{API_BASE}{path}", headers=HEADERS, params=params or {}, timeout=30)
    r.raise_for_status()
    return r.json()


def bc_put(path, body):
    r = requests.put(f"{API_BASE}{path}", headers=HEADERS, json=body, timeout=30)
    r.raise_for_status()
    return r.json()


def would_overflow_and_be_dropped(
    current_variant_levels: list, variant_id: int, new_level: int, int32_max: int = INT32_MAX
):
    """Pure decision. No network, no side effects.

    total_excluding_target = sum of every other variant's inventory_level.
    projected_sum = total_excluding_target + new_level.
    is_unsafe when projected_sum exceeds int32_max, or when new_level alone
    already exceeds int32_max. Returns (is_unsafe, projected_sum) so the caller
    can log the projected total whether or not it is unsafe.
    """
    total_excluding_target = sum(
        v.level for v in current_variant_levels if v.id != variant_id
    )
    projected_sum = total_excluding_target + new_level
    is_unsafe = projected_sum > int32_max or new_level > int32_max
    return is_unsafe, projected_sum


def list_variants(product_id):
    page = 1
    variants = []
    while True:
        resp = bc_get(
            f"/catalog/products/{product_id}/variants",
            {"limit": 250, "page": page, "include_fields": "id,sku,inventory_level"},
        )
        rows = resp.get("data", [])
        if not rows:
            return variants
        variants.extend(rows)
        page += 1


def get_variant(product_id, variant_id):
    resp = bc_get(f"/catalog/products/{product_id}/variants/{variant_id}")
    return resp["data"]


def write_variant_inventory_level(product_id, variant_id, new_level):
    return bc_put(
        f"/catalog/products/{product_id}/variants/{variant_id}",
        {"inventory_level": new_level},
    )


def check_and_apply(product_id, variant_id, sku, new_level):
    """Predict, then write if safe, then re-read to confirm. Returns a report dict or None."""
    variants = list_variants(product_id)
    levels = [VariantLevel(id=v["id"], level=v.get("inventory_level") or 0) for v in variants]

    before = get_variant(product_id, variant_id)
    current_persisted = before.get("inventory_level")

    is_unsafe, projected_sum = would_overflow_and_be_dropped(levels, variant_id, new_level)

    if is_unsafe:
        log.warning(
            "Predicted overflow: product_id=%s variant_id=%s sku=%s "
            "attempted=%s current=%s projected_sum=%s",
            product_id, variant_id, sku, new_level, current_persisted, projected_sum,
        )
        return {
            "product_id": product_id,
            "variant_id": variant_id,
            "sku": sku,
            "attempted_inventory_level": new_level,
            "current_persisted_inventory_level": current_persisted,
            "projected_sum": projected_sum,
        }

    if DRY_RUN:
        log.info(
            "Dry run, would write: product_id=%s variant_id=%s sku=%s "
            "attempted=%s current=%s projected_sum=%s",
            product_id, variant_id, sku, new_level, current_persisted, projected_sum,
        )
        return None

    write_variant_inventory_level(product_id, variant_id, new_level)
    after = get_variant(product_id, variant_id)

    if after.get("inventory_level") == current_persisted and new_level != current_persisted:
        log.warning(
            "Silent failure detected: product_id=%s variant_id=%s sku=%s "
            "attempted=%s current=%s (unchanged after 200 response) projected_sum=%s",
            product_id, variant_id, sku, new_level, current_persisted, projected_sum,
        )
        return {
            "product_id": product_id,
            "variant_id": variant_id,
            "sku": sku,
            "attempted_inventory_level": new_level,
            "current_persisted_inventory_level": after.get("inventory_level"),
            "projected_sum": projected_sum,
        }

    return None


def run(pending_writes):
    """pending_writes: iterable of (product_id, variant_id, sku, new_level)."""
    reports = []
    for product_id, variant_id, sku, new_level in pending_writes:
        report = check_and_apply(product_id, variant_id, sku, new_level)
        if report:
            reports.append(report)

    log.info("Done. %d mismatch(es) reported.", len(reports))
    return reports


if __name__ == "__main__":
    run([])
check-inventory-overflow.js
/**
 * Detect BigCommerce variant inventory writes that silently fail past int32 max.
 *
 * BigCommerce's Catalog v3 API stores inventory_level as a 32-bit signed integer
 * with a ceiling of 2147483647, and it enforces that ceiling against the product's
 * summed variant inventory, not just the single variant being written. A write via
 * PUT /v3/catalog/products/{id}/variants/{variant_id}, the Update Products batch
 * endpoint, or POST /v3/inventory/adjustments/absolute|relative that would push
 * that sum over the ceiling does not get clamped and does not return a validation
 * error. It returns HTTP 200 and the stored inventory_level is left unchanged.
 * This job predicts the overflow before writing using only pre-fetched variant
 * levels (no network call needed for the decision itself), and after every write
 * it re-reads the same variant directly to confirm the value actually changed.
 * Everything it finds is reported, nothing is auto-corrected.
 *
 * Guide: https://www.allanninal.dev/bigcommerce/inventory-int32-overflow-silent-failure/
 */
import { pathToFileURL } from "node:url";

const STORE_HASH = process.env.BIGCOMMERCE_STORE_HASH || "example_hash";
const ACCESS_TOKEN = process.env.BIGCOMMERCE_ACCESS_TOKEN || "bc_dummy";
const API_BASE = `https://api.bigcommerce.com/stores/${STORE_HASH}/v3`;
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";

const INT32_MAX = 2147483647;

const HEADERS = {
  "X-Auth-Token": ACCESS_TOKEN,
  "Content-Type": "application/json",
  Accept: "application/json",
};

/**
 * Pure decision. No network, no side effects.
 *
 * totalExcludingTarget = sum of every other variant's level.
 * projectedSum = totalExcludingTarget + newLevel.
 * isUnsafe when projectedSum exceeds int32Max, or when newLevel alone already
 * exceeds int32Max. Returns [isUnsafe, projectedSum] so the caller can log the
 * projected total whether or not it is unsafe.
 */
export function wouldOverflowAndBeDropped(currentVariantLevels, variantId, newLevel, int32Max = INT32_MAX) {
  const totalExcludingTarget = currentVariantLevels
    .filter((v) => v.id !== variantId)
    .reduce((sum, v) => sum + v.level, 0);
  const projectedSum = totalExcludingTarget + newLevel;
  const isUnsafe = projectedSum > int32Max || newLevel > int32Max;
  return [isUnsafe, projectedSum];
}

async function bcGet(path, params = {}) {
  const url = new URL(`${API_BASE}${path}`);
  for (const [key, value] of Object.entries(params)) {
    if (value !== undefined && value !== null) url.searchParams.set(key, value);
  }
  const res = await fetch(url, { headers: HEADERS });
  if (!res.ok) throw new Error(`BigCommerce ${res.status}`);
  return res.json();
}

async function bcPut(path, body) {
  const res = await fetch(`${API_BASE}${path}`, {
    method: "PUT",
    headers: HEADERS,
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`BigCommerce ${res.status}`);
  return res.json();
}

async function listVariants(productId) {
  const variants = [];
  let page = 1;
  while (true) {
    const resp = await bcGet(`/catalog/products/${productId}/variants`, {
      limit: 250,
      page,
      include_fields: "id,sku,inventory_level",
    });
    const rows = resp.data || [];
    if (!rows.length) return variants;
    variants.push(...rows);
    page += 1;
  }
}

async function getVariant(productId, variantId) {
  const resp = await bcGet(`/catalog/products/${productId}/variants/${variantId}`);
  return resp.data;
}

async function writeVariantInventoryLevel(productId, variantId, newLevel) {
  return bcPut(`/catalog/products/${productId}/variants/${variantId}`, {
    inventory_level: newLevel,
  });
}

async function checkAndApply(productId, variantId, sku, newLevel) {
  const variants = await listVariants(productId);
  const levels = variants.map((v) => ({ id: v.id, level: v.inventory_level || 0 }));

  const before = await getVariant(productId, variantId);
  const currentPersisted = before.inventory_level;

  const [isUnsafe, projectedSum] = wouldOverflowAndBeDropped(levels, variantId, newLevel);

  if (isUnsafe) {
    console.warn(
      `Predicted overflow: product_id=${productId} variant_id=${variantId} sku=${sku} ` +
      `attempted=${newLevel} current=${currentPersisted} projected_sum=${projectedSum}`
    );
    return {
      product_id: productId,
      variant_id: variantId,
      sku,
      attempted_inventory_level: newLevel,
      current_persisted_inventory_level: currentPersisted,
      projected_sum: projectedSum,
    };
  }

  if (DRY_RUN) {
    console.log(
      `Dry run, would write: product_id=${productId} variant_id=${variantId} sku=${sku} ` +
      `attempted=${newLevel} current=${currentPersisted} projected_sum=${projectedSum}`
    );
    return null;
  }

  await writeVariantInventoryLevel(productId, variantId, newLevel);
  const after = await getVariant(productId, variantId);

  if (after.inventory_level === currentPersisted && newLevel !== currentPersisted) {
    console.warn(
      `Silent failure detected: product_id=${productId} variant_id=${variantId} sku=${sku} ` +
      `attempted=${newLevel} current=${currentPersisted} (unchanged after 200 response) projected_sum=${projectedSum}`
    );
    return {
      product_id: productId,
      variant_id: variantId,
      sku,
      attempted_inventory_level: newLevel,
      current_persisted_inventory_level: after.inventory_level,
      projected_sum: projectedSum,
    };
  }

  return null;
}

export async function run(pendingWrites) {
  const reports = [];
  for (const [productId, variantId, sku, newLevel] of pendingWrites) {
    const report = await checkAndApply(productId, variantId, sku, newLevel);
    if (report) reports.push(report);
  }

  console.log(`Done. ${reports.length} mismatch(es) reported.`);
  return reports;
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  run([]).catch((err) => { console.error(err); process.exit(1); });
}

Add a test

The predicate is the part most worth testing, because it decides whether a write is even attempted. Because would_overflow_and_be_dropped takes only plain values, a list of (id, level) tuples, a target variant_id, and a new_level, the test needs no network and no BigCommerce store. It just feeds in plain values and checks the answer.

test_inventory_overflow_predicate.py
from check_inventory_overflow import would_overflow_and_be_dropped, VariantLevel

INT32_MAX = 2147483647


def test_safe_when_sum_is_well_under_max():
    levels = [VariantLevel(id=1, level=100), VariantLevel(id=2, level=200)]
    is_unsafe, projected_sum = would_overflow_and_be_dropped(levels, variant_id=2, new_level=300)
    assert is_unsafe is False
    assert projected_sum == 400


def test_unsafe_when_projected_sum_exceeds_int32_max():
    levels = [VariantLevel(id=1, level=2000000000), VariantLevel(id=2, level=100)]
    is_unsafe, projected_sum = would_overflow_and_be_dropped(levels, variant_id=2, new_level=500000000)
    assert is_unsafe is True
    assert projected_sum == 2500000000


def test_unsafe_when_new_level_alone_exceeds_int32_max():
    levels = [VariantLevel(id=1, level=0)]
    is_unsafe, projected_sum = would_overflow_and_be_dropped(levels, variant_id=1, new_level=INT32_MAX + 1)
    assert is_unsafe is True
    assert projected_sum == INT32_MAX + 1


def test_safe_at_exactly_int32_max():
    levels = [VariantLevel(id=1, level=0)]
    is_unsafe, projected_sum = would_overflow_and_be_dropped(levels, variant_id=1, new_level=INT32_MAX)
    assert is_unsafe is False
    assert projected_sum == INT32_MAX


def test_excludes_target_variant_current_level_from_the_sum():
    levels = [VariantLevel(id=1, level=INT32_MAX), VariantLevel(id=2, level=50)]
    is_unsafe, projected_sum = would_overflow_and_be_dropped(levels, variant_id=1, new_level=10)
    assert is_unsafe is False
    assert projected_sum == 60


def test_other_variants_pushing_sum_over_max_is_unsafe():
    levels = [VariantLevel(id=1, level=INT32_MAX - 10), VariantLevel(id=2, level=0)]
    is_unsafe, projected_sum = would_overflow_and_be_dropped(levels, variant_id=2, new_level=11)
    assert is_unsafe is True
    assert projected_sum == INT32_MAX + 1
check-inventory-overflow.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { wouldOverflowAndBeDropped } from "./check-inventory-overflow.js";

const INT32_MAX = 2147483647;

test("safe when sum is well under max", () => {
  const levels = [{ id: 1, level: 100 }, { id: 2, level: 200 }];
  const [isUnsafe, projectedSum] = wouldOverflowAndBeDropped(levels, 2, 300);
  assert.equal(isUnsafe, false);
  assert.equal(projectedSum, 400);
});

test("unsafe when projected sum exceeds int32 max", () => {
  const levels = [{ id: 1, level: 2000000000 }, { id: 2, level: 100 }];
  const [isUnsafe, projectedSum] = wouldOverflowAndBeDropped(levels, 2, 500000000);
  assert.equal(isUnsafe, true);
  assert.equal(projectedSum, 2500000000);
});

test("unsafe when new level alone exceeds int32 max", () => {
  const levels = [{ id: 1, level: 0 }];
  const [isUnsafe, projectedSum] = wouldOverflowAndBeDropped(levels, 1, INT32_MAX + 1);
  assert.equal(isUnsafe, true);
  assert.equal(projectedSum, INT32_MAX + 1);
});

test("safe at exactly int32 max", () => {
  const levels = [{ id: 1, level: 0 }];
  const [isUnsafe, projectedSum] = wouldOverflowAndBeDropped(levels, 1, INT32_MAX);
  assert.equal(isUnsafe, false);
  assert.equal(projectedSum, INT32_MAX);
});

test("excludes target variant current level from the sum", () => {
  const levels = [{ id: 1, level: INT32_MAX }, { id: 2, level: 50 }];
  const [isUnsafe, projectedSum] = wouldOverflowAndBeDropped(levels, 1, 10);
  assert.equal(isUnsafe, false);
  assert.equal(projectedSum, 60);
});

test("other variants pushing sum over max is unsafe", () => {
  const levels = [{ id: 1, level: INT32_MAX - 10 }, { id: 2, level: 0 }];
  const [isUnsafe, projectedSum] = wouldOverflowAndBeDropped(levels, 2, 11);
  assert.equal(isUnsafe, true);
  assert.equal(projectedSum, INT32_MAX + 1);
});

Case studies

ERP double-conversion bug

The integration that applied a unit conversion twice

A store synced inventory from an ERP that tracked stock in individual units but occasionally sent a bulk-pack quantity multiplied by a case size that had already been applied upstream. Most SKUs looked fine. One aggregate warehouse SKU, fed a doubled conversion on a promotional run, produced a write large enough to push its variant sum past 2147483647.

The integration's own logs showed a clean 200 response and moved on. The merchant only noticed weeks later when a stock report looked stale. Running the predicate against the ERP's proposed value before the write would have caught it at the source, and the post-write re-read would have caught it even if the predicate had been skipped.

Many-variant product

The product where no single variant looked unusual

A configurable product had several dozen variants, each with an ordinary-looking inventory_level in the low thousands. A batch restock event pushed one variant's count up substantially as part of a larger warehouse consolidation. Individually the number was unremarkable. Summed against every sibling variant on the same product, it crossed the ceiling.

Because the check sums every other variant's current level before proposing the write, it caught this even though no one field, taken alone, looked like a problem. The report gave the merchant the exact projected sum, so they could decide whether the consolidation number was correct or needed correcting upstream.

What good looks like

After this runs before and after every inventory write, a write that would silently fail gets caught before it is even attempted, with the exact projected sum in the log. Anything that still slips through gets caught by the post-write re-read instead of surfacing weeks later as a stale stock report. Every flagged mismatch is a question for a human, with the attempted value, the currently persisted value, and the projected sum right there, never a guess an automated script made on its own.

FAQ

Why does my BigCommerce inventory_level update return 200 but not actually save?

BigCommerce stores inventory_level as a 32-bit signed integer, with a hard ceiling of 2147483647, and enforces that ceiling against the product's summed variant inventory, not just the single variant you are writing. When your update would push that sum over the ceiling, the Catalog API declines to persist the new value but still returns a 200 success response, so the stored inventory_level is left exactly where it was before your call.

Is this the same as a normal validation error I can catch with a try/except?

No. A validation error would come back as a non-2xx status with an error body you could catch and log. This failure mode returns HTTP 200, the same response shape as a successful write, with no error field indicating anything went wrong. The only way to know the write did not take effect is to re-read the variant after the call and compare it to what you attempted to set.

Should a script auto-correct an inventory_level that overflowed?

No, not automatically. A number that large could be a data-entry mistake, a bug in an ERP integration producing a garbage delta, or an intentional aggregate SKU the merchant actually wants tracked at that scale. The safe pattern is to report every mismatch with the attempted and currently persisted values and only clamp to an operator-approved ceiling after a human confirms it, never inferred automatically from the overflowed input.

Related field notes

Citations

On the problem:

  1. BigCommerce Developer Center: the Product Variants API reference and the inventory_level field. developer.bigcommerce.com product variants
  2. bigcommerce/docs on GitHub: the Catalog v3 product-variants YAML schema definition. github.com bigcommerce/docs product-variants_catalog.v3.yml
  3. BigCommerce Help Center: a merchant asking how to reliably read variant stock levels. support.bigcommerce.com is there any way to get variant stock level

On the solution:

  1. BigCommerce Developer Center: the Inventory Adjustments endpoint, absolute and relative. developer.bigcommerce.com inventory adjustments
  2. BigCommerce API Reference: the Inventory management endpoints. docs.bigcommerce.com inventory
  3. BigCommerce API Reference: Create Product Variant, the inventory_level field on write. docs.bigcommerce.com create product variant

Stuck on a tricky one?

If you have a problem in BigCommerce orders, payments, webhooks, inventory, or fulfillment that you would rather hand off, this is the kind of work I do. Message me and we can work through it together.

Contact me on LinkedIn

Did this catch a silent inventory failure?

If this saved you from a stock count quietly drifting out of sync, you can buy me a coffee. It is the best way to keep these field notes free and growing.

Buy me a coffee on Ko-fi

Back to all BigCommerce field notes