Repair Orders and payments

BigCommerce disputed order not flagged

A customer files a chargeback with their bank. The gateway starts pulling the money back and opens a dispute case. But the order sitting in your BigCommerce admin still shows the same status it always had, Completed, Awaiting Fulfillment, whatever it was before, with nothing anywhere telling you a dispute is in progress. BigCommerce does not watch your gateway for chargebacks on its own, so status_id 13, Disputed, only gets set if a webhook happens to fire and a listener happens to catch it, or a person notices and changes it by hand. Here is why that gap opens up and a small script that reads each order's own transactions and flags the ones that actually need a human's attention.

Python and Node.js BigCommerce V2 Orders API Safe by default (dry run)
The short answer

A chargeback is opened at the card network and the payment gateway, not inside BigCommerce, so nothing forces the order's status_id to move to 13, Disputed. If the gateway never sends a webhook for the dispute, or the store never wired one up, the order just sits there looking normal while money is already being reversed. Run a small Python or Node.js script that lists recent orders, reads each one's transactions with GET /v2/orders/{id}/transactions, looks for a transaction that shows a dispute or chargeback in its type or status, and moves only the genuinely affected orders to status_id 13 with PUT /v2/orders/{id}, skipping anything already Refunded, Cancelled, or Disputed. Full code, tests, and a dry run guard are below.

The problem in plain words

When a customer disputes a charge, that fight happens between their bank, the card network, and your payment gateway. BigCommerce is not a party to any of it. The store's order record only changes when something explicitly tells BigCommerce to change it, either a webhook event the gateway or app sends, or a person opening the order and setting status_id to 13, Disputed, by hand.

In practice, that update is optional and easy to miss. Some gateways do not emit a dispute event to BigCommerce at all, since the chargeback lives entirely in the processor's own dashboard. Others send it, but only if the store actually built a listener for it, which many stores never do because dispute volume is usually low and easy to forget about. The result is an order that looks completely healthy, Completed, Awaiting Fulfillment, whatever it was, while the merchant's payout is already being reduced behind the scenes.

Customer disputes with bank Gateway opens a chargeback case No webhook reaches the store Order unchanged status_id never moves Merchant has no warning
The dispute is real and already moving money, but nothing carried the news from the gateway into the order, so status_id 13 never gets set.

Why it happens

BigCommerce's order status is a field the store or an integration sets explicitly, not a live mirror of what the gateway is doing. A few common ways a genuine dispute stays invisible on the order:

This is a quiet but expensive gap. Money leaves through the payout while the storefront-facing order record insists everything is fine, so a merchant can keep shipping or crediting an order that is actively under dispute, with no flag anywhere to stop them. See the citations at the end for the exact docs on order status and webhooks.

The key insight

status_id 13 is not proof a dispute exists, and its absence is not proof one does not. The transaction record is the real source of truth. So the safe pattern is not "trust the webhook to update the order." It is "read each order's own transactions and flag the ones that show a dispute or chargeback, whether or not status_id ever moved." We check GET /v2/orders/{id}/transactions directly, and we only ever move an order into status_id 13 when it is not already sitting in a status that reflects a settled outcome, like Refunded or Cancelled.

The fix, as a flow

We do not touch checkout, the gateway, or any money movement. We add a job that lists recent orders, reads each one's transactions, and decides whether the evidence shows a real dispute. If it does and the order is not already in a status that already reflects an outcome, the job sets status_id to 13 so the order is visibly flagged for a human. Everything else is left untouched.

Scheduled job runs on a timer List recent orders within the lookback window Read transactions type, status per order Dispute found and not settled? yes no, leave alone status_id 13 Disputed, flagged
The script only flags orders where the transaction record itself shows a dispute or chargeback, and only when the order is not already in a settled status.

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 Orders (modify) scope so it can read transactions and update status_id. 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 LOOKBACK_DAYS="30"
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 LOOKBACK_DAYS="30"
export DRY_RUN="true"   // start safe, change to false to write
2

Talk to the V2 Orders REST API

Every call goes to https://api.bigcommerce.com/stores/{store_hash}/v2/ with the token in the X-Auth-Token header. A small helper handles GET and PUT and raises on a non-2xx response. We reuse it to list orders, read transactions, and write the status update.

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}/v2"

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() if r.text else []

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}/v2`;

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}`);
  const text = await res.text();
  return text ? JSON.parse(text) : [];
}

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 recent orders and read their transactions

Call GET /v2/orders?min_date_created=..., paginated, to get orders within your lookback window. For each one, call GET /v2/orders/{id}/transactions to get the transaction list the decision needs: type, status, and the order's own current status_id.

step3.py
def recent_orders(lookback_days):
    page = 1
    while True:
        orders = bc_get("/orders", {
            "min_date_created": f"-{lookback_days} days",
            "page": page,
            "limit": 50,
        })
        if not orders:
            return
        for order in orders:
            yield order
        page += 1

def order_transactions(order_id):
    return bc_get(f"/orders/{order_id}/transactions")
step3.js
async function* recentOrders(lookbackDays) {
  let page = 1;
  while (true) {
    const orders = await bcGet("/orders", {
      min_date_created: `-${lookbackDays} days`,
      page,
      limit: 50,
    });
    if (!orders.length) return;
    for (const order of orders) yield order;
    page += 1;
  }
}

async function orderTransactions(orderId) {
  return bcGet(`/orders/${orderId}/transactions`);
}
4

Decide, with one pure function

Keep the decision in its own function that takes the order's current status_id and its transactions, and returns whether the order should be flagged as Disputed. The rule is strict on purpose. Only a transaction whose type or status clearly reads as a dispute or chargeback counts as evidence. An order already in a settled status, Disputed, Refunded, or Cancelled, is left alone, since that status already reflects an outcome and re-flagging it would only create noise.

decide.py
DISPUTED = 13
SETTLED_STATUSES = {13, 4, 5, 14}  # Disputed, Refunded, Cancelled, Partially Refunded
DISPUTE_MARKERS = {"chargeback", "dispute", "disputed"}

def needs_dispute_flag(status_id, transactions):
    if status_id in SETTLED_STATUSES:
        return False
    for txn in transactions or []:
        kind = (txn.get("type") or txn.get("event") or "").lower()
        status = (txn.get("status") or "").lower()
        if any(marker in kind or marker in status for marker in DISPUTE_MARKERS):
            return True
    return False
decide.js
const DISPUTED = 13;
const SETTLED_STATUSES = new Set([13, 4, 5, 14]); // Disputed, Refunded, Cancelled, Partially Refunded
const DISPUTE_MARKERS = ["chargeback", "dispute", "disputed"];

export function needsDisputeFlag(statusId, transactions) {
  if (SETTLED_STATUSES.has(statusId)) return false;
  for (const txn of transactions || []) {
    const kind = (txn.type || txn.event || "").toLowerCase();
    const status = (txn.status || "").toLowerCase();
    if (DISPUTE_MARKERS.some((marker) => kind.includes(marker) || status.includes(marker))) {
      return true;
    }
  }
  return false;
}
5

Flag the order the same way a person would

When the decision says the order needs a flag, call PUT /v2/orders/{id} with {"status_id": 13}. That is the same field a merchant would set by hand after opening the order and marking it Disputed. The script never touches refunds, cancels, or fulfillment. It only raises the flag so a human can look at the actual dispute case with the gateway.

apply.py
def flag_disputed(order_id):
    return bc_put(f"/orders/{order_id}", {"status_id": DISPUTED})
apply.js
async function flagDisputed(orderId) {
  return bcPut(`/orders/${orderId}`, { status_id: DISPUTED });
}
6

Wire it together with a dry run guard

The loop ties every piece together. Notice the dry run guard. On the first few runs, leave DRY_RUN on so the script only logs which orders it would flag, with the order id and the matching transaction detail. Read the output, agree with it, then switch it off. Run it on a schedule that matches how quickly you want to catch a dispute, for example once a day.

Run it safe

Always start with DRY_RUN=true, and never let this job touch refunds, cancellations, or fulfillment. Its only job is raising a flag on status_id so a person notices the dispute and handles the actual case with the gateway.

The full code

Here is the complete script in one file for each language. It reads settings from the environment, logs what it does, respects the dry run flag, and is safe to run again and again because it only flags orders with clear dispute evidence in their transactions, and skips anything already in a settled status.

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

flag_disputed_orders.py
"""Flag BigCommerce orders that were disputed at the gateway but never marked Disputed.

A chargeback happens between the customer's bank, the card network, and the
payment gateway. BigCommerce is not part of that conversation, so an order's
status_id only reaches 13 (Disputed) if a webhook happens to arrive and a
listener happens to catch it, or a person opens the order and sets it by hand.
Many gateways never send that event to BigCommerce at all, so a genuinely
disputed order can sit at its old status indefinitely while the payout is
already being reduced. This job lists recent orders, reads each order's
transactions, and flags only the ones with a clear dispute or chargeback
marker in a transaction's type or status, skipping any order already in a
settled status such as Disputed, Refunded, Cancelled, or Partially Refunded.
It never touches refunds, cancellations, or fulfillment. Run on a schedule.
Safe to run again and again.

Guide: https://www.allanninal.dev/bigcommerce/disputed-order-not-flagged/
"""
import os
import logging

import requests

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

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

DISPUTED = 13
SETTLED_STATUSES = {13, 4, 5, 14}  # Disputed, Refunded, Cancelled, Partially Refunded
DISPUTE_MARKERS = {"chargeback", "dispute", "disputed"}

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()
    if not r.text:
        return []
    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 needs_dispute_flag(status_id, transactions):
    """Pure decision. No network, no side effects.

    An order already sitting in a settled status (Disputed, Refunded,
    Cancelled, Partially Refunded) is left alone, since that status already
    reflects an outcome. Otherwise, scan the order's own transactions for a
    type or status that clearly reads as a dispute or chargeback. Any match
    means the order needs a flag.
    """
    if status_id in SETTLED_STATUSES:
        return False

    for txn in transactions or []:
        txn_kind = (txn.get("type") or txn.get("event") or "").lower()
        txn_status = (txn.get("status") or "").lower()
        if any(marker in txn_kind or marker in txn_status for marker in DISPUTE_MARKERS):
            return True
    return False


def recent_orders():
    """Page through orders created within the lookback window."""
    page = 1
    while True:
        orders = bc_get(
            "/orders",
            {
                "min_date_created": f"-{LOOKBACK_DAYS} days",
                "page": page,
                "limit": 50,
            },
        )
        if not orders:
            return
        for order in orders:
            yield order
        page += 1


def order_transactions(order_id):
    return bc_get(f"/orders/{order_id}/transactions")


def flag_disputed(order_id):
    return bc_put(f"/orders/{order_id}", {"status_id": DISPUTED})


def run():
    flagged = 0
    for order in recent_orders():
        order_id = order["id"]
        status_id = order.get("status_id")
        transactions = order_transactions(order_id)

        if not needs_dispute_flag(status_id, transactions):
            continue

        matching_txn = None
        for txn in transactions or []:
            txn_kind = (txn.get("type") or txn.get("event") or "").lower()
            txn_status = (txn.get("status") or "").lower()
            if any(marker in txn_kind or marker in txn_status for marker in DISPUTE_MARKERS):
                matching_txn = txn
                break

        log.warning(
            "order_id=%s current_status_id=%s transaction_id=%s transaction_type=%s %s",
            order_id, status_id,
            matching_txn.get("id") if matching_txn else None,
            matching_txn.get("type") if matching_txn else None,
            "would flag as Disputed" if DRY_RUN else "flagging as Disputed",
        )
        if not DRY_RUN:
            flag_disputed(order_id)
        flagged += 1

    log.info("Done. %d order(s) %s.", flagged, "to flag" if DRY_RUN else "flagged")


if __name__ == "__main__":
    run()
flag-disputed-orders.js
/**
 * Flag BigCommerce orders that were disputed at the gateway but never marked Disputed.
 *
 * A chargeback happens between the customer's bank, the card network, and the
 * payment gateway. BigCommerce is not part of that conversation, so an order's
 * status_id only reaches 13 (Disputed) if a webhook happens to arrive and a
 * listener happens to catch it, or a person opens the order and sets it by
 * hand. Many gateways never send that event to BigCommerce at all, so a
 * genuinely disputed order can sit at its old status indefinitely while the
 * payout is already being reduced. This job lists recent orders, reads each
 * order's transactions, and flags only the ones with a clear dispute or
 * chargeback marker in a transaction's type or status, skipping any order
 * already in a settled status such as Disputed, Refunded, Cancelled, or
 * Partially Refunded. It never touches refunds, cancellations, or
 * fulfillment. Run on a schedule.
 *
 * Guide: https://www.allanninal.dev/bigcommerce/disputed-order-not-flagged/
 */
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}/v2`;
const LOOKBACK_DAYS = Number(process.env.LOOKBACK_DAYS || 30);
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";

const DISPUTED = 13;
const SETTLED_STATUSES = new Set([13, 4, 5, 14]); // Disputed, Refunded, Cancelled, Partially Refunded
const DISPUTE_MARKERS = ["chargeback", "dispute", "disputed"];

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

/**
 * Pure decision. No network, no side effects.
 *
 * An order already sitting in a settled status (Disputed, Refunded,
 * Cancelled, Partially Refunded) is left alone, since that status already
 * reflects an outcome. Otherwise, scan the order's own transactions for a
 * type or status that clearly reads as a dispute or chargeback. Any match
 * means the order needs a flag.
 */
export function needsDisputeFlag(statusId, transactions) {
  if (SETTLED_STATUSES.has(statusId)) return false;

  for (const txn of transactions || []) {
    const txnKind = (txn.type || txn.event || "").toLowerCase();
    const txnStatus = (txn.status || "").toLowerCase();
    if (DISPUTE_MARKERS.some((marker) => txnKind.includes(marker) || txnStatus.includes(marker))) {
      return true;
    }
  }
  return false;
}

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}`);
  const text = await res.text();
  return text ? JSON.parse(text) : [];
}

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* recentOrders() {
  let page = 1;
  while (true) {
    const orders = await bcGet("/orders", {
      min_date_created: `-${LOOKBACK_DAYS} days`,
      page,
      limit: 50,
    });
    if (!orders.length) return;
    for (const order of orders) yield order;
    page += 1;
  }
}

async function orderTransactions(orderId) {
  return bcGet(`/orders/${orderId}/transactions`);
}

async function flagDisputed(orderId) {
  return bcPut(`/orders/${orderId}`, { status_id: DISPUTED });
}

export async function run() {
  let flagged = 0;

  for await (const order of recentOrders()) {
    const orderId = order.id;
    const statusId = order.status_id;
    const transactions = await orderTransactions(orderId);

    if (!needsDisputeFlag(statusId, transactions)) continue;

    let matchingTxn = null;
    for (const txn of transactions || []) {
      const txnKind = (txn.type || txn.event || "").toLowerCase();
      const txnStatus = (txn.status || "").toLowerCase();
      if (DISPUTE_MARKERS.some((marker) => txnKind.includes(marker) || txnStatus.includes(marker))) {
        matchingTxn = txn;
        break;
      }
    }

    console.warn(
      `order_id=${orderId} current_status_id=${statusId} transaction_id=${matchingTxn ? matchingTxn.id : null} ` +
      `transaction_type=${matchingTxn ? matchingTxn.type : null} ` +
      `${DRY_RUN ? "would flag as Disputed" : "flagging as Disputed"}`
    );
    if (!DRY_RUN) await flagDisputed(orderId);
    flagged += 1;
  }

  console.log(`Done. ${flagged} order(s) ${DRY_RUN ? "to flag" : "flagged"}.`);
}

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

Add a test

The decision rule is the part most worth testing, because it decides whether a real order gets flagged for a human to review. Because needs_dispute_flag takes only plain values and returns a plain boolean, the test needs no network and no BigCommerce store. It just feeds in plain objects and checks the answer.

test_disputed_order_flag.py
from flag_disputed_orders import needs_dispute_flag


def chargeback_txn(type_="chargeback", status="pending"):
    return {"type": type_, "status": status, "amount": "50.00", "id": 1}


def test_flags_when_transaction_type_is_chargeback():
    assert needs_dispute_flag(10, [chargeback_txn()]) is True


def test_flags_when_transaction_status_reads_disputed():
    txn = {"type": "capture", "status": "disputed", "amount": "50.00", "id": 2}
    assert needs_dispute_flag(10, [txn]) is True


def test_no_flag_when_no_dispute_marker_present():
    txn = {"type": "capture", "status": "success", "amount": "50.00", "id": 3}
    assert needs_dispute_flag(10, [txn]) is False


def test_no_flag_when_already_disputed():
    assert needs_dispute_flag(13, [chargeback_txn()]) is False


def test_no_flag_when_already_refunded():
    assert needs_dispute_flag(4, [chargeback_txn()]) is False


def test_no_flag_when_already_cancelled():
    assert needs_dispute_flag(5, [chargeback_txn()]) is False


def test_no_flag_with_no_transactions():
    assert needs_dispute_flag(10, []) is False
flag-disputed-orders.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { needsDisputeFlag } from "./flag-disputed-orders.js";

const chargebackTxn = ({ type = "chargeback", status = "pending" } = {}) => ({
  type, status, amount: "50.00", id: 1,
});

test("flags when transaction type is chargeback", () => {
  assert.equal(needsDisputeFlag(10, [chargebackTxn()]), true);
});

test("flags when transaction status reads disputed", () => {
  const txn = { type: "capture", status: "disputed", amount: "50.00", id: 2 };
  assert.equal(needsDisputeFlag(10, [txn]), true);
});

test("no flag when no dispute marker present", () => {
  const txn = { type: "capture", status: "success", amount: "50.00", id: 3 };
  assert.equal(needsDisputeFlag(10, [txn]), false);
});

test("no flag when already disputed", () => {
  assert.equal(needsDisputeFlag(13, [chargebackTxn()]), false);
});

test("no flag when already refunded", () => {
  assert.equal(needsDisputeFlag(4, [chargebackTxn()]), false);
});

test("no flag when already cancelled", () => {
  assert.equal(needsDisputeFlag(5, [chargebackTxn()]), false);
});

test("no flag with no transactions", () => {
  assert.equal(needsDisputeFlag(10, []), false);
});

Case studies

Silent gateway

The store whose processor never sent a single dispute event

A mid-size store used a payment gateway that handled all chargeback management inside its own merchant portal, with no webhook to BigCommerce for any part of the dispute lifecycle. Weeks would pass between a chargeback landing and someone in finance noticing the payout was short, and by then the order had usually already shipped.

Now a daily job reads every recent order's transactions directly instead of waiting on an event that was never coming. The moment a transaction shows a chargeback, the order is flagged Disputed the same day, well before the fulfillment team would have shipped it blind.

Deactivated webhook

The integration that quietly stopped listening

A store had a webhook wired up for dispute events, but a deploy briefly broke the endpoint, and BigCommerce deactivated the hook after enough failed deliveries. No one recreated it, so months of disputes went completely unflagged even though the original setup had been correct.

The transaction-based check does not depend on that webhook staying alive. It caught the entire backlog on its first run and has kept catching new disputes ever since, regardless of whether any particular webhook happens to be healthy that day.

What good looks like

After this runs on a schedule, a chargeback is never more than one run away from a visible flag on the order, whether or not the gateway ever sent BigCommerce a webhook for it. Orders that are already Disputed, Refunded, or Cancelled are left exactly as they are, so no one gets a noisy re-flag on a case that is already settled, and the fulfillment team stops finding out about disputes from a payout report instead of the order itself.

FAQ

Why does not BigCommerce mark an order Disputed on its own when a customer files a chargeback?

A chargeback happens at the card network and the payment gateway, not inside BigCommerce. Unless the gateway sends BigCommerce a webhook or the merchant changes the order by hand, the order's status_id never moves to 13, Disputed. Many gateways either do not send that event at all or send it to a dashboard the store never watches, so the order looks completely normal while the money is already being pulled back.

Is status_id 13 the only sign of a dispute I should trust?

No. Treat status_id 13 as a nice-to-have, not a guarantee. The reliable source is the order's own transactions from GET /v2/orders/{id}/transactions, since a chargeback shows up there as a transaction whose type or status reflects a dispute or reversal, whether or not anything ever updated status_id. Reading transactions directly means the script does not depend on a webhook that may never arrive.

Is it safe to change status_id automatically when a dispute is found?

Do not use this to guess at outcomes. Only flag orders that show clear evidence of a dispute or chargeback transaction, and only set status_id to 13 when the order is not already Disputed, Refunded, or Cancelled, since those already reflect an outcome. Never let the script decide to ship, refund, or cancel anything. That decision belongs to a person looking at the actual dispute.

Related field notes

Citations

On the problem:

  1. a. a.com

On the solution:

  1. a. a.com

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 dispute you would have missed?

If this saved you from shipping an order that was already under dispute, or caught a chargeback your gateway never told you about, 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