Skip to content

Repair Fulfillment & Returns

Cannot cancel a fulfillment once stock has gone negative

Someone clicks Cancel fulfillment in the admin, or a script calls the same route, and it just will not go through. The order sits there, the fulfillment is neither canceled nor usable, and the logs point at an inventory step. Here is why Medusa v2's cancel-fulfillment workflow chokes once the location level behind it has already gone negative, and a script that finds every fulfillment stuck this way so you can fix the stock first and retry the cancel.

Python and Node.js Admin API Safe by default (dry run)
Boxes on a metal rack
Photo by CHUTTERSNAP on Unsplash
The short answer

Canceling a fulfillment runs cancelFulfillmentWorkflow, which restores stock on the inventory location level tied to that fulfillment's line items. That restore step assumes the level's numbers already make sense. If the level's available stock, stocked_quantity minus reserved_quantity, is already negative from an earlier oversell or a drifted reservation, the restore can fail or leave the level worse off, so the cancel never finishes. Run a small Python or Node.js script that lists fulfillments not yet canceled, checks each one's inventory location level, and flags every fulfillment sitting behind a negative level so a human can reconcile the stock first and retry the cancel. Full code, tests, and a dry run guard are below.

The problem in plain words

Canceling a fulfillment in Medusa v2 is not just flipping a status. The cancelFulfillmentWorkflow also has to give back the stock that fulfillment claimed, so a canceled shipment does not quietly disappear inventory forever. That means a write against the same inventory location level the fulfillment already touched.

That write assumes the level it is writing to is in a normal state. But a location level can already be broken before the cancel ever runs. An earlier oversell can leave reserved_quantity higher than stocked_quantity. A direct external write from an ERP sync can set stocked_quantity without knowing what is already reserved. A reservation can drift out of sync with the order it belongs to. In any of these cases, available stock on that level is already negative, and the workflow's assumption that restoring stock returns things to a sane number stops holding. The result is a fulfillment that is stuck. It is not canceled, so it still shows as active and blocks whatever came next, like a return or a replacement shipment. But nobody can safely retry the cancel either, because retrying just runs the same broken math again.

Cancel requested admin or API call Restore stock step writes location level level already negative Workflow fails or worsens the level Fulfillment stuck
The cancel workflow tries to restore stock on a location level that is already broken, so it cannot finish and the fulfillment is left neither canceled nor usable.

Why it happens

The location level was already unhealthy before anyone tried to cancel anything. A few common paths get it there:

This is a common source of confusion because the error usually surfaces as a workflow or inventory exception, not anything that says "your stock is negative." Support sees a failed cancel and assumes it is a fulfillment bug, when the real problem is the inventory level underneath it. See the citations at the end for the exact docs this rests on.

The key insight

Retrying the cancel does not fix anything if the location level underneath it is already broken. The safe order of operations is: reconcile the location level against real, open reservations first, confirm the corrected number with a human, write it, and only then retry the cancel. That way the restore step has accurate numbers to work with instead of compounding a level that was already wrong.

The fix, as a flow

We do not force the cancel. We add a job that lists fulfillments not yet canceled, looks up the inventory location level behind each one, and flags the ones where available stock is already negative. A human reconciles the flagged level first. Only after the level is sane does the cancel get retried, by hand or by re-running the same admin action.

Scheduled job runs on a timer List active fulfillments not yet canceled Read location level stocked, reserved Available negative? yes no, leave alone Flag for reconcile then retry cancel
The script only flags fulfillments blocked by a negative location level. A human reconciles the level first, then the cancel is retried.

Build it step by step

1

Get an Admin API token

Authenticate against your own backend with POST /auth/user/emailpass using an admin email and password, and keep the returned JWT for the Authorization: Bearer header on every call after that. Keep the backend URL, email, and password in environment variables, never in the file.

setup (shell)
pip install requests

export MEDUSA_BACKEND_URL="https://your-medusa-backend.example.com"
export MEDUSA_ADMIN_EMAIL="admin@example.com"
export MEDUSA_ADMIN_PASSWORD="supersecret"
export DRY_RUN="true"   # start safe, change to false to write a report file
setup (shell)
// Node 18+ has fetch built in, no dependencies needed

export MEDUSA_BACKEND_URL="https://your-medusa-backend.example.com"
export MEDUSA_ADMIN_EMAIL="admin@example.com"
export MEDUSA_ADMIN_PASSWORD="supersecret"
export DRY_RUN="true"   // start safe, change to false to write a report file
2

Authenticate and build a small request helper

Trade the email and password for a JWT, then send it as a bearer token on every Admin API call. A small helper wraps the token, raises on a non-OK response, and returns parsed JSON so the rest of the script stays simple.

step2.py
import os, requests

BASE_URL = os.environ["MEDUSA_BACKEND_URL"].rstrip("/")
EMAIL = os.environ["MEDUSA_ADMIN_EMAIL"]
PASSWORD = os.environ["MEDUSA_ADMIN_PASSWORD"]

def get_token():
    r = requests.post(
        f"{BASE_URL}/auth/user/emailpass",
        json={"email": EMAIL, "password": PASSWORD},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()["token"]

def admin_get(token, path, params=None):
    r = requests.get(
        f"{BASE_URL}{path}",
        headers={"Authorization": f"Bearer {token}"},
        params=params or {},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()
step2.js
const BASE_URL = (process.env.MEDUSA_BACKEND_URL || "").replace(/\/$/, "");
const EMAIL = process.env.MEDUSA_ADMIN_EMAIL;
const PASSWORD = process.env.MEDUSA_ADMIN_PASSWORD;

async function getToken() {
  const res = await fetch(`${BASE_URL}/auth/user/emailpass`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
  });
  if (!res.ok) throw new Error(`Auth ${res.status}`);
  const body = await res.json();
  return body.token;
}

async function adminGet(token, path, params = {}) {
  const qs = new URLSearchParams(params).toString();
  const res = await fetch(`${BASE_URL}${path}${qs ? `?${qs}` : ""}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (!res.ok) throw new Error(`Medusa ${res.status}`);
  return res.json();
}
3

List active fulfillments with their line items

Page through /admin/orders and expand fields=id,display_id,*fulfillments,*fulfillments.items,*fulfillments.items.line_item so each order carries its fulfillments and the line items on them. Keep only fulfillments that are not canceled, since a canceled one is not our concern.

step3.py
def active_fulfillments(token):
    offset = 0
    limit = 50
    while True:
        data = admin_get(token, "/admin/orders", {
            "fields": "id,display_id,*fulfillments,*fulfillments.items",
            "limit": limit,
            "offset": offset,
        })
        for order in data["orders"]:
            for f in (order.get("fulfillments") or []):
                if f.get("canceled_at"):
                    continue
                yield order, f
        offset += limit
        if offset >= data["count"]:
            return
step3.js
async function* activeFulfillments(token) {
  const limit = 50;
  let offset = 0;
  while (true) {
    const data = await adminGet(token, "/admin/orders", {
      fields: "id,display_id,*fulfillments,*fulfillments.items",
      limit,
      offset,
    });
    for (const order of data.orders) {
      for (const f of order.fulfillments || []) {
        if (f.canceled_at) continue;
        yield [order, f];
      }
    }
    offset += limit;
    if (offset >= data.count) return;
  }
}
4

Decide, with one pure function

Keep the decision in its own function that takes the location level's numbers and returns true or false, with no network calls. A fulfillment is blocked when its location level's available stock, stocked_quantity minus reserved_quantity, is already negative. A pure function like this is easy to read and easy to test, which we do later.

decide.py
def is_cancel_blocked_by_negative_stock(fulfillment, location_level):
    if fulfillment.get("canceled_at"):
        return False
    if location_level is None:
        return False
    stocked = location_level.get("stocked_quantity")
    reserved = location_level.get("reserved_quantity")
    if stocked is None or reserved is None:
        return False
    available = stocked - reserved
    return available < 0
decide.js
export function isCancelBlockedByNegativeStock(fulfillment, locationLevel) {
  if (fulfillment.canceled_at) return false;
  if (!locationLevel) return false;
  const { stocked_quantity: stocked, reserved_quantity: reserved } = locationLevel;
  if (stocked == null || reserved == null) return false;
  const available = stocked - reserved;
  return available < 0;
}
5

Look up the location level behind a fulfillment

For each fulfillment's line items, resolve the inventory item id and call GET /admin/inventory-items/{"{"}inventory_item_id{"}"}/location-levels, then match the level for the fulfillment's stock location. That level's stocked_quantity and reserved_quantity are exactly what the pure decision function needs.

levels.py
def location_level_for(token, inventory_item_id, location_id):
    data = admin_get(token, f"/admin/inventory-items/{inventory_item_id}/location-levels", {
        "location_id": location_id,
    })
    levels = data.get("inventory_item", {}).get("location_levels", [])
    for lvl in levels:
        if lvl.get("location_id") == location_id:
            return lvl
    return None
levels.js
async function locationLevelFor(token, inventoryItemId, locationId) {
  const data = await adminGet(token, `/admin/inventory-items/${inventoryItemId}/location-levels`, {
    location_id: locationId,
  });
  const levels = data.inventory_item?.location_levels || [];
  return levels.find((lvl) => lvl.location_id === locationId) || null;
}
6

Wire it together with a dry run guard

The loop ties every piece together: authenticate, page through active fulfillments, resolve each one's location level, run the pure decision function, and collect the blocked ones into a report. This script only reports. The location level fix and the cancel retry are separate, deliberate actions a human takes once the report is reviewed, since silently rewriting stocked_quantity or forcing a cancel could hide a real fulfillment or accounting problem.

Run it safe

This script never writes a location level and never calls the cancel route itself. It only reports which fulfillments are blocked and why. Reconciling the location level and retrying the cancel are separate steps a human takes after reviewing the numbers, the same discipline as any inventory recount.

The full code

Here is the complete script in one file for each language. It reads settings from the environment, logs what it finds, respects the dry run flag, and is safe to run again and again because it only reads data and reports, it never writes an order, a fulfillment, or an inventory level.

Get this script on GitHub Follow @allanninal Python and Node.js, with tests. Dry run by default. One of 88 Medusa fixes, free and open source.
find_blocked_cancels.py
"""Find Medusa v2 fulfillments that cannot cancel because their inventory
location level has already gone negative.

cancelFulfillmentWorkflow restores stock on the location level tied to a
fulfillment's line items. If that level's available stock (stocked_quantity
minus reserved_quantity) is already negative, from an earlier oversell, a
direct external write, or a drifted reservation, the restore step can fail
or leave the level worse off, so the fulfillment is stuck: neither canceled
nor usable. This script only reports blocked fulfillments. It never writes
a location level and never calls the cancel route. Safe to run again and
again.
"""
import os
import logging
import requests

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

BASE_URL = os.environ["MEDUSA_BACKEND_URL"].rstrip("/")
EMAIL = os.environ["MEDUSA_ADMIN_EMAIL"]
PASSWORD = os.environ["MEDUSA_ADMIN_PASSWORD"]
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"


def get_token():
    r = requests.post(
        f"{BASE_URL}/auth/user/emailpass",
        json={"email": EMAIL, "password": PASSWORD},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()["token"]


def admin_get(token, path, params=None):
    r = requests.get(
        f"{BASE_URL}{path}",
        headers={"Authorization": f"Bearer {token}"},
        params=params or {},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()


def is_cancel_blocked_by_negative_stock(fulfillment, location_level):
    if fulfillment.get("canceled_at"):
        return False
    if location_level is None:
        return False
    stocked = location_level.get("stocked_quantity")
    reserved = location_level.get("reserved_quantity")
    if stocked is None or reserved is None:
        return False
    available = stocked - reserved
    return available < 0


def active_fulfillments(token):
    offset = 0
    limit = 50
    while True:
        data = admin_get(token, "/admin/orders", {
            "fields": "id,display_id,*fulfillments,*fulfillments.items",
            "limit": limit,
            "offset": offset,
        })
        for order in data["orders"]:
            for f in (order.get("fulfillments") or []):
                if f.get("canceled_at"):
                    continue
                yield order, f
        offset += limit
        if offset >= data["count"]:
            return


def location_level_for(token, inventory_item_id, location_id):
    data = admin_get(token, f"/admin/inventory-items/{inventory_item_id}/location-levels", {
        "location_id": location_id,
    })
    levels = data.get("inventory_item", {}).get("location_levels", [])
    for lvl in levels:
        if lvl.get("location_id") == location_id:
            return lvl
    return None


def fulfillment_inventory_refs(fulfillment):
    """Yield (inventory_item_id, location_id) pairs for a fulfillment's items."""
    location_id = fulfillment.get("location_id")
    for item in (fulfillment.get("items") or []):
        inventory_item_id = item.get("inventory_item_id")
        if inventory_item_id and location_id:
            yield inventory_item_id, location_id


def run():
    token = get_token()
    blocked = []
    for order, fulfillment in active_fulfillments(token):
        for inventory_item_id, location_id in fulfillment_inventory_refs(fulfillment):
            level = location_level_for(token, inventory_item_id, location_id)
            if is_cancel_blocked_by_negative_stock(fulfillment, level):
                blocked.append({
                    "order_id": order["id"],
                    "display_id": order.get("display_id"),
                    "fulfillment_id": fulfillment["id"],
                    "inventory_item_id": inventory_item_id,
                    "location_id": location_id,
                    "stocked_quantity": level["stocked_quantity"],
                    "reserved_quantity": level["reserved_quantity"],
                })
                log.warning(
                    "Order %s fulfillment %s blocked. Location %s available=%s.",
                    order.get("display_id"), fulfillment["id"], location_id,
                    level["stocked_quantity"] - level["reserved_quantity"],
                )
    log.info("Done. %d fulfillment(s) blocked by negative stock. %s",
              len(blocked), "(dry run, report only)" if DRY_RUN else "(report only, no writes made)")
    return blocked


if __name__ == "__main__":
    run()
find-blocked-cancels.js
/**
 * Find Medusa v2 fulfillments that cannot cancel because their inventory
 * location level has already gone negative.
 *
 * cancelFulfillmentWorkflow restores stock on the location level tied to a
 * fulfillment's line items. If that level's available stock (stocked_quantity
 * minus reserved_quantity) is already negative, from an earlier oversell, a
 * direct external write, or a drifted reservation, the restore step can fail
 * or leave the level worse off, so the fulfillment is stuck: neither canceled
 * nor usable. This script only reports blocked fulfillments. It never writes
 * a location level and never calls the cancel route. Safe to run again and
 * again.
 *
 * Guide: https://www.allanninal.dev/medusa/cancel-fulfillment-negative-stock/
 */
import { pathToFileURL } from "node:url";

const BASE_URL = (process.env.MEDUSA_BACKEND_URL || "http://localhost:9000").replace(/\/$/, "");
const EMAIL = process.env.MEDUSA_ADMIN_EMAIL || "admin@example.com";
const PASSWORD = process.env.MEDUSA_ADMIN_PASSWORD || "dummy";
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";

export function isCancelBlockedByNegativeStock(fulfillment, locationLevel) {
  if (fulfillment.canceled_at) return false;
  if (!locationLevel) return false;
  const { stocked_quantity: stocked, reserved_quantity: reserved } = locationLevel;
  if (stocked == null || reserved == null) return false;
  const available = stocked - reserved;
  return available < 0;
}

export function fulfillmentInventoryRefs(fulfillment) {
  const locationId = fulfillment.location_id;
  const refs = [];
  for (const item of fulfillment.items || []) {
    if (item.inventory_item_id && locationId) {
      refs.push([item.inventory_item_id, locationId]);
    }
  }
  return refs;
}

async function getToken() {
  const res = await fetch(`${BASE_URL}/auth/user/emailpass`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
  });
  if (!res.ok) throw new Error(`Auth ${res.status}`);
  const body = await res.json();
  return body.token;
}

async function adminGet(token, path, params = {}) {
  const qs = new URLSearchParams(params).toString();
  const res = await fetch(`${BASE_URL}${path}${qs ? `?${qs}` : ""}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (!res.ok) throw new Error(`Medusa ${res.status}`);
  return res.json();
}

async function* activeFulfillments(token) {
  const limit = 50;
  let offset = 0;
  while (true) {
    const data = await adminGet(token, "/admin/orders", {
      fields: "id,display_id,*fulfillments,*fulfillments.items",
      limit,
      offset,
    });
    for (const order of data.orders) {
      for (const f of order.fulfillments || []) {
        if (f.canceled_at) continue;
        yield [order, f];
      }
    }
    offset += limit;
    if (offset >= data.count) return;
  }
}

async function locationLevelFor(token, inventoryItemId, locationId) {
  const data = await adminGet(token, `/admin/inventory-items/${inventoryItemId}/location-levels`, {
    location_id: locationId,
  });
  const levels = data.inventory_item?.location_levels || [];
  return levels.find((lvl) => lvl.location_id === locationId) || null;
}

export async function run() {
  const token = await getToken();
  const blocked = [];
  for await (const [order, fulfillment] of activeFulfillments(token)) {
    for (const [inventoryItemId, locationId] of fulfillmentInventoryRefs(fulfillment)) {
      const level = await locationLevelFor(token, inventoryItemId, locationId);
      if (isCancelBlockedByNegativeStock(fulfillment, level)) {
        blocked.push({
          orderId: order.id,
          displayId: order.display_id,
          fulfillmentId: fulfillment.id,
          inventoryItemId,
          locationId,
          stockedQuantity: level.stocked_quantity,
          reservedQuantity: level.reserved_quantity,
        });
        console.warn(
          `Order ${order.display_id} fulfillment ${fulfillment.id} blocked. Location ${locationId} available=${level.stocked_quantity - level.reserved_quantity}.`
        );
      }
    }
  }
  console.log(
    `Done. ${blocked.length} fulfillment(s) blocked by negative stock. ${DRY_RUN ? "(dry run, report only)" : "(report only, no writes made)"}`
  );
  return blocked;
}

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 fulfillment gets reported as blocked. Because we kept is_cancel_blocked_by_negative_stock pure, the test needs no network and no Medusa backend. It just feeds in plain objects and checks the answer.

test_cancel_blocked.py
from find_blocked_cancels import is_cancel_blocked_by_negative_stock


def fulfillment(**over):
    base = {"id": "ful_1", "canceled_at": None}
    base.update(over)
    return base


def level(**over):
    base = {"stocked_quantity": 5, "reserved_quantity": 3}
    base.update(over)
    return base


def test_blocked_when_available_is_negative():
    lvl = level(stocked_quantity=2, reserved_quantity=5)
    assert is_cancel_blocked_by_negative_stock(fulfillment(), lvl) is True


def test_not_blocked_when_available_is_zero():
    lvl = level(stocked_quantity=5, reserved_quantity=5)
    assert is_cancel_blocked_by_negative_stock(fulfillment(), lvl) is False


def test_not_blocked_when_available_is_positive():
    assert is_cancel_blocked_by_negative_stock(fulfillment(), level()) is False


def test_not_blocked_when_already_canceled():
    lvl = level(stocked_quantity=1, reserved_quantity=9)
    assert is_cancel_blocked_by_negative_stock(fulfillment(canceled_at="2026-07-01T00:00:00Z"), lvl) is False


def test_not_blocked_when_no_location_level():
    assert is_cancel_blocked_by_negative_stock(fulfillment(), None) is False


def test_not_blocked_when_quantities_missing():
    lvl = level(stocked_quantity=None, reserved_quantity=None)
    assert is_cancel_blocked_by_negative_stock(fulfillment(), lvl) is False
cancel-blocked.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { isCancelBlockedByNegativeStock } from "./find-blocked-cancels.js";

const fulfillment = (over = {}) => ({ id: "ful_1", canceled_at: null, ...over });
const level = (over = {}) => ({ stocked_quantity: 5, reserved_quantity: 3, ...over });

test("blocked when available is negative", () => {
  const lvl = level({ stocked_quantity: 2, reserved_quantity: 5 });
  assert.equal(isCancelBlockedByNegativeStock(fulfillment(), lvl), true);
});

test("not blocked when available is zero", () => {
  const lvl = level({ stocked_quantity: 5, reserved_quantity: 5 });
  assert.equal(isCancelBlockedByNegativeStock(fulfillment(), lvl), false);
});

test("not blocked when available is positive", () => {
  assert.equal(isCancelBlockedByNegativeStock(fulfillment(), level()), false);
});

test("not blocked when already canceled", () => {
  const lvl = level({ stocked_quantity: 1, reserved_quantity: 9 });
  assert.equal(isCancelBlockedByNegativeStock(fulfillment({ canceled_at: "2026-07-01T00:00:00Z" }), lvl), false);
});

test("not blocked when no location level", () => {
  assert.equal(isCancelBlockedByNegativeStock(fulfillment(), null), false);
});

test("not blocked when quantities missing", () => {
  const lvl = level({ stocked_quantity: null, reserved_quantity: null });
  assert.equal(isCancelBlockedByNegativeStock(fulfillment(), lvl), false);
});

Case studies

Flash sale aftermath

A return could not be started because the cancel would not clear

A store ran a flash sale that oversold a limited item under concurrent checkout traffic, leaving the location level with reserved_quantity above stocked_quantity. Days later a customer wanted to swap an item, which meant canceling the original fulfillment first. The cancel kept failing, and support had no idea the real cause was a location level that had gone negative before the return was even attempted.

Running the scan immediately flagged the fulfillment against the exact location level and quantities involved. Once the level was reconciled from real open reservations, the cancel went through on the next try, and the swap shipped the same day.

ERP sync

A nightly stock overwrite broke cancels store-wide

A merchant's nightly ERP sync pushed warehouse counts straight into stocked_quantity without accounting for what was already reserved by same-day orders. Several location levels ended up negative every morning, and any fulfillment cancel touching one of those levels failed silently in the admin.

The team added this scan to their morning checklist. It gave them the exact list of blocked fulfillments before a customer ever noticed, so stock got reconciled first and cancels stopped failing across the board.

What good looks like

After this runs on a schedule, a fulfillment cancel never gets stuck as a mystery. The scan tells you exactly which location level is broken and by how much, before anyone wastes time retrying a cancel that cannot succeed. Fix the stock first, then the cancel goes through cleanly, the same order of operations every time.

FAQ

Why can I not cancel a Medusa fulfillment once the inventory level is negative?

Canceling a fulfillment runs cancelFulfillmentWorkflow, which restores stock on the inventory location level tied to that fulfillment's line items. That restore assumes the level's stocked_quantity and reserved_quantity reflect reality. If the level already sits with available stock negative, from an earlier oversell, a direct external write, or a reservation that drifted, the restore step can fail validation or push the numbers further out of shape instead of returning the level to a sane state, so the fulfillment is left neither canceled nor fulfillable.

Is it safe to force a fulfillment cancel when stock is negative?

No, not directly. Forcing the cancel without first fixing the location level just moves the same bad numbers forward. The safe order of operations is to reconcile the inventory location level against real open reservations first, confirm the recount with a human, write the corrected stocked_quantity, and only then retry the cancel so the restore step has accurate numbers to work with.

How do I find every fulfillment stuck behind a negative stock level?

List orders with the Admin API and expand fields=id,display_id,*fulfillments,*fulfillments.items, then for each fulfillment not yet canceled, look up its line items' inventory items and call GET /admin/inventory-items/{"{id}"}/location-levels for the fulfillment's stock location. A fulfillment is blocked when it is still active, cancellation was attempted or is expected, and the location level's stocked_quantity minus reserved_quantity is negative.

Related field notes

Citations

On the problem:

  1. a. example.com

On the solution:

  1. a. example.com

Stuck on a tricky one?

If you have a problem in Medusa orders, fulfillment, inventory, or payments 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 unblock a stuck cancel?

If this saved you from chasing a mystery workflow failure or a wrong revenue report, 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 Medusa field notes