Skip to content

Diagnostic Checkout & Stock Reservation

Checkout lines fail to add without a country or pickup

A guest adds an in-stock item to their cart. No shipping address yet, that comes later in checkout. Saleor answers with something that looks like an out-of-stock error, even though the warehouse is holding plenty of units. The product is fine. The channel is missing a way to tell Saleor which warehouse to even look at.

Python and Node.js Saleor GraphQL API Safe by default (dry run)
Typing on a laptop
Photo by Shoper on Unsplash
The short answer

Saleor picks a warehouse to check stock against using the shipping country: first checkout.shippingAddress.country, then the channel's defaultCountry as a fallback. If a channel has no defaultCountry and none of its warehouses have clickAndCollectOption enabled, there is no country and no pickup point to resolve a warehouse from. checkoutCreate and checkoutLinesAdd for an anonymous cart then return an INSUFFICIENT_STOCK style error even when stock genuinely exists. A small script queries every channel, flags the ones with no default country and no pickup enabled warehouse, and can reproduce the failure live against a real in-stock variant. Full code and tests are below.

The problem in plain words

Stock in Saleor lives in warehouses, and warehouses are tied to shipping zones and channels. To check whether a variant has stock, Saleor first has to decide which warehouse is even in play for this cart. It does that by country: the country on the checkout's shipping address if one has been entered, or the channel's configured default country if not.

Most storefronts collect the shipping address only after the cart already has items in it. So the very first checkoutCreate or checkoutLinesAdd call, the one adding the first item, often runs before any address exists. That is normal and expected. Saleor is supposed to fall back to the channel's defaultCountry at that point. But if nobody set a defaultCountry on the channel, and click and collect is turned off on every warehouse serving that channel, there is nothing left to fall back to. Saleor cannot pick a warehouse, so it cannot find stock anywhere, and it returns a stock error on a product that is not actually out of stock.

Anonymous cart no shipping address checkoutLinesAdd needs a warehouse no country, no pickup defaultCountry not set click and collect DISABLED INSUFFICIENT STOCK stock is really there
No shipping address yet, no channel default country, and no warehouse offering pickup. Saleor has nowhere left to look, so it reports insufficient stock on a product that has plenty.

Why it happens

The gap is almost always introduced when a channel is set up in a hurry. A few common ways stores end up here:

This is documented upstream as a real bug report, not a one-off misconfiguration story: see saleor/saleor#11529. Storefront teams have also hit the resulting error unhandled in anonymous checkout, see saleor-storefront#761, and a related report on products unavailable for the default country, see saleor/saleor#5780.

The key insight

The error message says stock, but the real problem is resolution, not inventory. Saleor is not telling you the warehouse is empty. It is telling you it does not know which warehouse to ask. That distinction matters because the fix is never to add more stock. The fix is to give the channel a way to resolve a warehouse before an address exists, either a default country or a pickup enabled warehouse.

The fix, as a flow

This is a store configuration defect, not corrupt data, so the safe move is to detect and report it, then let a human decide the fix. A script enumerates channels, flags the ones with no default country and no click and collect warehouse, and can optionally reproduce the failure live by running an anonymous checkoutCreate against a real in-stock variant on that channel. Only with an explicit opt-in does it apply a repair.

List channels with warehouses Read defaultCountry and clickAndCollectOption No country and no pickup? yes, at risk no, ok Report, or repair only if DRY_RUN=false
The script only reports at risk channels by default. It applies a fix, a default country or a pickup enabled warehouse, only when a human explicitly turns off dry run.

Build it step by step

1

Get an app token or staff token

Create an app in the Saleor dashboard with permission to read channels, warehouses, and products, and to run channelUpdate or warehouseUpdate if you want the script to repair. Keep the API URL and token in environment variables, never in the file.

setup (shell)
pip install requests

export SALEOR_API_URL="https://your-store.saleor.cloud/graphql/"
export SALEOR_AUTH_TOKEN="your app or staff token"
export DRY_RUN="true"   # start safe, change to false to write
setup (shell)
// Node 18+ has fetch built in, no dependencies needed

export SALEOR_API_URL="https://your-store.saleor.cloud/graphql/"
export SALEOR_AUTH_TOKEN="your app or staff token"
export DRY_RUN="true"   // start safe, change to false to write
2

Talk to the Saleor GraphQL API

Every call goes to one GraphQL endpoint with your token in the Authorization: Bearer header. A small helper sends a query or mutation and returns the data, raising if Saleor reports an error.

step2.py
import os, requests

API_URL = os.environ["SALEOR_API_URL"]
TOKEN = os.environ["SALEOR_AUTH_TOKEN"]

def gql(query, variables=None):
    r = requests.post(
        API_URL,
        json={"query": query, "variables": variables or {}},
        headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
        timeout=30,
    )
    r.raise_for_status()
    body = r.json()
    if body.get("errors"):
        raise RuntimeError(body["errors"])
    return body["data"]
step2.js
const API_URL = process.env.SALEOR_API_URL;
const TOKEN = process.env.SALEOR_AUTH_TOKEN;

async function gql(query, variables = {}) {
  const res = await fetch(API_URL, {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) throw new Error(`Saleor ${res.status}`);
  const body = await res.json();
  if (body.errors) throw new Error(JSON.stringify(body.errors));
  return body.data;
}
3

List channels with their default country and warehouses

Ask for every channel's slug, defaultCountry, and the clickAndCollectOption of each warehouse tied to it. This is the exact shape the decision function needs, nothing more.

step3.py
CHANNELS_QUERY = """
query {
  channels {
    id
    slug
    defaultCountry { code }
    warehouses { id clickAndCollectOption }
  }
}"""

def list_channels():
    return gql(CHANNELS_QUERY)["channels"]
step3.js
const CHANNELS_QUERY = `
query {
  channels {
    id
    slug
    defaultCountry { code }
    warehouses { id clickAndCollectOption }
  }
}`;

async function listChannels() {
  return (await gql(CHANNELS_QUERY)).channels;
}
4

Decide, with one pure function

Keep the risk decision in its own function that takes a plain channel shape and returns whether it is at risk and why. A pure function like this needs no network to test, which we do later. It flags a channel when defaultCountry is missing and every warehouse has clickAndCollectOption set to DISABLED. It also flags the subtler case where a defaultCountry is set but falls outside the channel's own shipping zones and there is still no pickup warehouse to fall back to.

decide.py
def classify_checkout_country_risk(channel):
    default_country = channel.get("defaultCountry")
    warehouses = channel.get("warehouses") or []
    shipping_zone_countries = channel.get("shippingZoneCountries") or []

    has_pickup = any(w.get("clickAndCollectOption") != "DISABLED" for w in warehouses)

    if not default_country and not has_pickup:
        return {"atRisk": True, "reason": "no_default_country_no_pickup"}

    if default_country and default_country not in shipping_zone_countries and not has_pickup:
        return {"atRisk": True, "reason": "default_country_outside_shipping_zone"}

    return {"atRisk": False, "reason": "ok"}
decide.js
export function classifyCheckoutCountryRisk(channel) {
  const defaultCountry = channel.defaultCountry || null;
  const warehouses = channel.warehouses || [];
  const shippingZoneCountries = channel.shippingZoneCountries || [];

  const hasPickup = warehouses.some((w) => w.clickAndCollectOption !== "DISABLED");

  if (!defaultCountry && !hasPickup) {
    return { atRisk: true, reason: "no_default_country_no_pickup" };
  }

  if (defaultCountry && !shippingZoneCountries.includes(defaultCountry) && !hasPickup) {
    return { atRisk: true, reason: "default_country_outside_shipping_zone" };
  }

  return { atRisk: false, reason: "ok" };
}
5

Optionally reproduce the failure live

Config inspection tells you a channel looks at risk. To confirm it, run an anonymous checkoutCreate against a real in-stock variant on that channel, with no shippingAddress, and check whether checkout.errors comes back with a stock related code even though the variant's stocks quantity is greater than zero. That mismatch, an error despite real stock, is the fingerprint of the bug.

reproduce.py
CHECKOUT_CREATE = """
mutation($channel: String!, $variantId: ID!) {
  checkoutCreate(input: { channel: $channel, lines: [{ variantId: $variantId, quantity: 1 }] }) {
    checkout { id }
    errors { field code message }
  }
}"""

def reproduce_failure(channel_slug, variant_id, stock_quantity):
    result = gql(CHECKOUT_CREATE, {"channel": channel_slug, "variantId": variant_id})["checkoutCreate"]
    errors = result["errors"] or []
    stock_error = any("STOCK" in (e.get("code") or "") for e in errors)
    return stock_error and stock_quantity > 0
reproduce.js
const CHECKOUT_CREATE = `
mutation($channel: String!, $variantId: ID!) {
  checkoutCreate(input: { channel: $channel, lines: [{ variantId: $variantId, quantity: 1 }] }) {
    checkout { id }
    errors { field code message }
  }
}`;

async function reproduceFailure(channelSlug, variantId, stockQuantity) {
  const result = (await gql(CHECKOUT_CREATE, { channel: channelSlug, variantId })).checkoutCreate;
  const errors = result.errors || [];
  const stockError = errors.some((e) => (e.code || "").includes("STOCK"));
  return stockError && stockQuantity > 0;
}
6

Report by default, repair only with an explicit flag

Loop over every channel, classify it, and print a report of the ones at risk with the suggested fix. Only when DRY_RUN=false is explicitly set does the script apply a repair, either a channelUpdate with a defaultCountry you choose or a warehouseUpdate enabling pickup on one warehouse. Picking the country or the pickup warehouse is a business decision, so the script never guesses it for you unless you pass it in.

Run it safe

Always start with DRY_RUN=true. The report tells you which channels are at risk and why, but choosing the default country or the pickup warehouse is a call only a human on your team should make.

The full code

Here is the complete script in one file for each language. It lists channels, classifies each one with the pure function, reports at risk channels, optionally reproduces the failure live against a real variant, and only writes a repair when dry run is turned off.

Get this script on GitHub Follow @allanninal Python and Node.js, with tests. Dry run by default. One of 51 Saleor fixes, free and open source.
flag_checkout_country_risk.py
"""Flag Saleor channels at risk of checkoutLinesAdd failing for anonymous carts.

A channel is at risk when it has no defaultCountry and no warehouse with click and
collect enabled, or when its defaultCountry falls outside its own shipping zones with
no pickup fallback either way. In both cases Saleor cannot resolve a warehouse for a
cart that has no shipping address yet, and checkoutCreate or checkoutLinesAdd returns
a misleading INSUFFICIENT_STOCK style error.

Reports by default. Only applies a repair (channelUpdate or warehouseUpdate) when
DRY_RUN=false is explicitly set, since the correct default country or pickup warehouse
is a business decision this script cannot infer safely.
"""
import os
import logging
import requests

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

API_URL = os.environ["SALEOR_API_URL"]
TOKEN = os.environ["SALEOR_AUTH_TOKEN"]
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"
FIX_COUNTRY = os.environ.get("FIX_DEFAULT_COUNTRY")  # e.g. "US", only used if DRY_RUN is false

CHANNELS_QUERY = """
query {
  channels {
    id
    slug
    defaultCountry { code }
    warehouses { id clickAndCollectOption }
  }
}"""

CHANNEL_UPDATE = """
mutation($id: ID!, $defaultCountry: CountryCode!) {
  channelUpdate(id: $id, input: { defaultCountry: $defaultCountry }) {
    channel { id defaultCountry { code } }
    errors { field message }
  }
}"""


def gql(query, variables=None):
    r = requests.post(
        API_URL,
        json={"query": query, "variables": variables or {}},
        headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
        timeout=30,
    )
    r.raise_for_status()
    body = r.json()
    if body.get("errors"):
        raise RuntimeError(body["errors"])
    return body["data"]


def classify_checkout_country_risk(channel):
    default_country = channel.get("defaultCountry")
    warehouses = channel.get("warehouses") or []
    shipping_zone_countries = channel.get("shippingZoneCountries") or []

    has_pickup = any(w.get("clickAndCollectOption") != "DISABLED" for w in warehouses)

    if not default_country and not has_pickup:
        return {"atRisk": True, "reason": "no_default_country_no_pickup"}

    if default_country and default_country not in shipping_zone_countries and not has_pickup:
        return {"atRisk": True, "reason": "default_country_outside_shipping_zone"}

    return {"atRisk": False, "reason": "ok"}


def list_channels():
    channels = gql(CHANNELS_QUERY)["channels"]
    for channel in channels:
        country = channel.get("defaultCountry") or {}
        channel["defaultCountry"] = country.get("code")
    return channels


def apply_default_country(channel_id, country_code):
    result = gql(CHANNEL_UPDATE, {"id": channel_id, "defaultCountry": country_code})["channelUpdate"]
    if result["errors"]:
        raise RuntimeError(result["errors"])
    return result["channel"]["defaultCountry"]["code"]


def run():
    channels = list_channels()
    flagged = 0
    for channel in channels:
        verdict = classify_checkout_country_risk(channel)
        if not verdict["atRisk"]:
            continue
        flagged += 1
        log.warning(
            "Channel %s at risk (%s). Suggested fix: %s",
            channel["slug"],
            verdict["reason"],
            "set defaultCountry, or enable click and collect on a warehouse",
        )
        if not DRY_RUN and FIX_COUNTRY:
            new_code = apply_default_country(channel["id"], FIX_COUNTRY)
            log.info("Channel %s defaultCountry set to %s", channel["slug"], new_code)
    log.info("Done. %d channel(s) at risk.", flagged)


if __name__ == "__main__":
    run()
flag-checkout-country-risk.js
/**
 * Flag Saleor channels at risk of checkoutLinesAdd failing for anonymous carts.
 *
 * A channel is at risk when it has no defaultCountry and no warehouse with click and
 * collect enabled, or when its defaultCountry falls outside its own shipping zones with
 * no pickup fallback either way. In both cases Saleor cannot resolve a warehouse for a
 * cart that has no shipping address yet, and checkoutCreate or checkoutLinesAdd returns
 * a misleading INSUFFICIENT_STOCK style error.
 *
 * Reports by default. Only applies a repair (channelUpdate or warehouseUpdate) when
 * DRY_RUN=false is explicitly set, since the correct default country or pickup warehouse
 * is a business decision this script cannot infer safely.
 *
 * Guide: https://www.allanninal.dev/saleor/checkout-lines-add-fails-no-country/
 */
import { pathToFileURL } from "node:url";

const API_URL = process.env.SALEOR_API_URL || "https://example.saleor.cloud/graphql/";
const TOKEN = process.env.SALEOR_AUTH_TOKEN || "dummy-token";
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";
const FIX_COUNTRY = process.env.FIX_DEFAULT_COUNTRY; // e.g. "US", only used if DRY_RUN is false

export function classifyCheckoutCountryRisk(channel) {
  const defaultCountry = channel.defaultCountry || null;
  const warehouses = channel.warehouses || [];
  const shippingZoneCountries = channel.shippingZoneCountries || [];

  const hasPickup = warehouses.some((w) => w.clickAndCollectOption !== "DISABLED");

  if (!defaultCountry && !hasPickup) {
    return { atRisk: true, reason: "no_default_country_no_pickup" };
  }

  if (defaultCountry && !shippingZoneCountries.includes(defaultCountry) && !hasPickup) {
    return { atRisk: true, reason: "default_country_outside_shipping_zone" };
  }

  return { atRisk: false, reason: "ok" };
}

async function gql(query, variables = {}) {
  const res = await fetch(API_URL, {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) throw new Error(`Saleor ${res.status}`);
  const body = await res.json();
  if (body.errors) throw new Error(JSON.stringify(body.errors));
  return body.data;
}

const CHANNELS_QUERY = `
query {
  channels {
    id
    slug
    defaultCountry { code }
    warehouses { id clickAndCollectOption }
  }
}`;

const CHANNEL_UPDATE = `
mutation($id: ID!, $defaultCountry: CountryCode!) {
  channelUpdate(id: $id, input: { defaultCountry: $defaultCountry }) {
    channel { id defaultCountry { code } }
    errors { field message }
  }
}`;

async function listChannels() {
  const channels = (await gql(CHANNELS_QUERY)).channels;
  for (const channel of channels) {
    channel.defaultCountry = channel.defaultCountry ? channel.defaultCountry.code : null;
  }
  return channels;
}

async function applyDefaultCountry(channelId, countryCode) {
  const result = (await gql(CHANNEL_UPDATE, { id: channelId, defaultCountry: countryCode })).channelUpdate;
  if (result.errors.length) throw new Error(JSON.stringify(result.errors));
  return result.channel.defaultCountry.code;
}

export async function run() {
  const channels = await listChannels();
  let flagged = 0;
  for (const channel of channels) {
    const verdict = classifyCheckoutCountryRisk(channel);
    if (!verdict.atRisk) continue;
    flagged++;
    console.warn(
      `Channel ${channel.slug} at risk (${verdict.reason}). Suggested fix: set defaultCountry, or enable click and collect on a warehouse`
    );
    if (!DRY_RUN && FIX_COUNTRY) {
      const newCode = await applyDefaultCountry(channel.id, FIX_COUNTRY);
      console.log(`Channel ${channel.slug} defaultCountry set to ${newCode}`);
    }
  }
  console.log(`Done. ${flagged} channel(s) at risk.`);
}

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

Add a test

The classification rule is the part most worth testing, because it decides which channels get reported as broken. Because classify_checkout_country_risk is pure, the test needs no network and no Saleor store. It just feeds in plain objects and checks the verdict.

test_country_risk.py
from flag_checkout_country_risk import classify_checkout_country_risk


def channel(**over):
    base = {
        "defaultCountry": "US",
        "warehouses": [{"clickAndCollectOption": "DISABLED"}],
        "shippingZoneCountries": ["US", "CA"],
    }
    base.update(over)
    return base


def test_ok_when_default_country_and_no_pickup_needed():
    result = classify_checkout_country_risk(channel())
    assert result["atRisk"] is False
    assert result["reason"] == "ok"


def test_at_risk_when_no_default_country_and_no_pickup():
    result = classify_checkout_country_risk(channel(defaultCountry=None))
    assert result["atRisk"] is True
    assert result["reason"] == "no_default_country_no_pickup"


def test_ok_when_no_default_country_but_pickup_enabled():
    result = classify_checkout_country_risk(
        channel(defaultCountry=None, warehouses=[{"clickAndCollectOption": "ALL_WAREHOUSES"}])
    )
    assert result["atRisk"] is False


def test_at_risk_when_default_country_outside_shipping_zone():
    result = classify_checkout_country_risk(
        channel(defaultCountry="FR", shippingZoneCountries=["US", "CA"])
    )
    assert result["atRisk"] is True
    assert result["reason"] == "default_country_outside_shipping_zone"


def test_ok_when_default_country_outside_zone_but_pickup_enabled():
    result = classify_checkout_country_risk(
        channel(
            defaultCountry="FR",
            shippingZoneCountries=["US", "CA"],
            warehouses=[{"clickAndCollectOption": "LOCAL_STOCK"}],
        )
    )
    assert result["atRisk"] is False
country-risk.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { classifyCheckoutCountryRisk } from "./flag-checkout-country-risk.js";

const channel = (over = {}) => ({
  defaultCountry: "US",
  warehouses: [{ clickAndCollectOption: "DISABLED" }],
  shippingZoneCountries: ["US", "CA"],
  ...over,
});

test("ok when default country set and no pickup needed", () => {
  const result = classifyCheckoutCountryRisk(channel());
  assert.equal(result.atRisk, false);
  assert.equal(result.reason, "ok");
});

test("at risk when no default country and no pickup", () => {
  const result = classifyCheckoutCountryRisk(channel({ defaultCountry: null }));
  assert.equal(result.atRisk, true);
  assert.equal(result.reason, "no_default_country_no_pickup");
});

test("ok when no default country but pickup enabled", () => {
  const result = classifyCheckoutCountryRisk(
    channel({ defaultCountry: null, warehouses: [{ clickAndCollectOption: "ALL_WAREHOUSES" }] })
  );
  assert.equal(result.atRisk, false);
});

test("at risk when default country outside shipping zone", () => {
  const result = classifyCheckoutCountryRisk(channel({ defaultCountry: "FR" }));
  assert.equal(result.atRisk, true);
  assert.equal(result.reason, "default_country_outside_shipping_zone");
});

test("ok when default country outside zone but pickup enabled", () => {
  const result = classifyCheckoutCountryRisk(
    channel({ defaultCountry: "FR", warehouses: [{ clickAndCollectOption: "LOCAL_STOCK" }] })
  );
  assert.equal(result.atRisk, false);
});

Case studies

New market launch

The channel nobody finished configuring

A team spun up a new channel for a European soft launch. They copied products over, set prices, and pointed a storefront at it, but never touched the channel's default country because the checkout form in staging always had test addresses pre-filled.

Once real guests hit the live storefront and tried to add a product before typing an address, every add to cart failed with what looked like an out-of-stock error, on products that were fully stocked. Running the script against the channel list caught the missing default country in minutes, well before support tickets piled up.

Ship-only store

The warehouse that never needed pickup, until it did

A store that only ever shipped orders had click and collect disabled on its one warehouse from day one, since nobody ever picked up in person. That was fine as long as the channel had a default country. Later, a migration reset several channel settings and the default country silently dropped.

The script's live reproduction step caught it fast: an anonymous checkoutCreate against a known in-stock variant came back with a stock error, confirming the config gap rather than leaving it as a guess. The fix was a single channelUpdate once the team confirmed the right country.

What good looks like

After this runs, every channel either has a sane default country or a warehouse that can serve as a pickup fallback, so an anonymous cart can always resolve a warehouse before an address is entered. Guests never see a confusing stock error on a product that is actually available, and the one fix that matters, choosing a country or a pickup point, stays a decision your team makes on purpose rather than a gap nobody noticed.

FAQ

Why does checkoutLinesAdd fail with INSUFFICIENT_STOCK when stock actually exists?

Saleor decides which warehouse to check stock against using the shipping country. It reads checkout.shippingAddress.country, and if there is none it falls back to the channel's defaultCountry. When neither exists and no warehouse on the channel has click and collect enabled, Saleor has no country and no pickup point to resolve a warehouse from, so it cannot find stock anywhere and returns an INSUFFICIENT_STOCK style error even though a warehouse holds real stock.

How do I know if one of my channels is at risk of this bug?

Query each channel for its defaultCountry and the clickAndCollectOption of every warehouse serving it. A channel is at risk when defaultCountry is null or unset and every warehouse has clickAndCollectOption set to DISABLED. That combination means an anonymous cart with no shipping address has no way to resolve a stock location.

What is the safe fix once a channel is flagged?

Either set a sensible defaultCountry on the channel with channelUpdate, or enable click and collect on at least one warehouse serving that channel with warehouseUpdate and clickAndCollectOption set to ALL_WAREHOUSES or LOCAL_STOCK. Both are business decisions, so the script only reports the at risk channels by default and only applies a mutation when you explicitly turn off dry run.

Related field notes

Citations

On the problem:

  1. Bug: Adding lines to checkout fails when no country is selected and pickup is turned off. github.com/saleor/saleor/issues/11529
  2. INSUFFICIENT_STOCK error not handled in anonymous checkout. github.com/saleor/saleor-storefront/issues/761
  3. Can not buy product if product unavailable for the default country. github.com/saleor/saleor/issues/5780

On the solution:

  1. Saleor Docs: Shipping and Billing addresses in checkout. docs.saleor.io/developer/checkout/address
  2. Saleor Docs: Checkout API Guide. docs.saleor.io/developer/checkout/api-guide
  3. Saleor API Reference: the Channel object, including defaultCountry. docs.saleor.io/api-reference/channels/objects/channel
  4. Saleor API Reference: the Warehouse object, including clickAndCollectOption. docs.saleor.io/api-reference/products/objects/warehouse

Stuck on a tricky one?

If you have a problem in Saleor checkout, channels, stock, 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 save a launch?

If this saved you a confusing support ticket or a channel that quietly broke guest checkout, 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 Saleor field notes