Diagnostic Catalog, metadata, and scheduling

Currency not enabled on Stripe

A shopper checks out in a currency your store now offers, but Stripe was never approved to accept it. The charge is refused, the order sits unpaid, and the error in the logs just says something like currency_not_enabled, which tells nobody what to actually do. Here is why it happens and a small script that finds every order at risk before it fails, and every order that already has.

Python and Node.js Runs on a schedule Safe by default (dry run)
A fan of 100 us dollar bills
Photo by omid armin on Unsplash
The short answer

Stripe accounts only accept charges in a fixed list of currencies approved for the account's country. When an order's currency is not on that list, Stripe rejects the PaymentIntent with an error such as currency_not_enabled, and the order is left pending or failed with no clear reason on the order itself. Run a small Python or Node.js check on a schedule that reads the Stripe account's accepted currencies once, then flags any order whose currency is not on that list, or whose PaymentIntent already failed with that exact error. Full code, tests, and a dry run guard are below.

The problem in plain words

A Stripe account is approved to settle in a specific set of currencies, based on the country it was created in. That list is not the same as "any currency with a symbol Stripe recognizes." A US account, for example, can accept many currencies for the charge amount, but the exact list still depends on the account's country and its approved settlement currencies, and it is not infinite.

WooCommerce does not check this list before sending the charge. If a store adds a new store currency, turns on a multi-currency plugin, or a manual order is created in a currency nobody tested, the order looks completely normal right up until the moment Stripe returns the charge. The buyer sees a generic payment error. The shop manager sees an order stuck on Pending or Failed. Nothing on the order screen mentions currency at all.

Buyer checks out in a new currency Woo sends charge to Stripe currency not enabled Charge refused order unpaid No clear reason shown
The charge is refused at the Stripe step because the account was never approved for that currency. The order and the buyer are both left guessing why.

Why it happens

Stripe's own documentation is clear that a connected account can only accept payments in the currencies approved for its country, and that list is fixed unless the account requests a change. A few common ways a store ends up sending a charge in a currency Stripe will refuse:

This is a known source of confusion because the failure shows up as a generic decline or a vague processing error, not a message that says "this currency is not enabled." Merchants report spending hours checking card details and 3D Secure settings before finding the real cause in the Stripe error code. See the citations at the end for the exact references.

The key insight

The Stripe account, not the order, decides which currencies are possible. If an order's currency is not on the account's approved list, no amount of retrying the charge will fix it, the currency itself has to be enabled in Stripe, or the order has to be rebilled in a currency that already works. A checker that reads the account's real list and compares it against every order catches this before the shopper ever sees the decline.

The fix, as a flow

We do not touch the live checkout. We add a small job that runs on a schedule, asks Stripe once which currencies the account can actually accept, then walks recent pending, on-hold, and failed orders. Any order whose currency is not on that list, or whose PaymentIntent already failed with a currency related error code, gets a clear note added to it so the shop manager knows exactly what to do next.

Scheduled job reads Stripe's list List open and failed orders Read order currency + last error Currency enabled? yes, skip no Flag order clear note added
The checker reads the Stripe account's real currency list and only flags orders whose currency is missing from it, or whose charge already failed for that exact reason.

Build it step by step

1

Get access to both systems

You need a Stripe secret key and a WooCommerce REST API key pair (a consumer key and a consumer secret) with read and write access to orders. Create the WooCommerce key under WooCommerce, Settings, Advanced, REST API. Keep every value in environment variables, never in the file.

setup (shell)
pip install stripe requests

export STRIPE_SECRET_KEY="sk_live_..."
export WOO_STORE_URL="https://yourstore.com"
export WOO_CONSUMER_KEY="ck_..."
export WOO_CONSUMER_SECRET="cs_..."
export REVIEW_HOLD="false"   # true also moves flagged orders to on-hold
export DRY_RUN="true"        # start safe, change to false to write
setup (shell)
npm install stripe

export STRIPE_SECRET_KEY="sk_live_..."
export WOO_STORE_URL="https://yourstore.com"
export WOO_CONSUMER_KEY="ck_..."
export WOO_CONSUMER_SECRET="cs_..."
export REVIEW_HOLD="false"   // true also moves flagged orders to on-hold
export DRY_RUN="true"        // start safe, change to false to write
2

Ask Stripe which currencies the account can actually accept

Stripe exposes the settlement currencies approved for an account's country through the country spec endpoint. We look up the account's country once, then fetch its supported payment currencies. This is the real source of truth, not a guess based on what the storefront happens to show.

step2.py
import os, stripe

stripe.api_key = os.environ["STRIPE_SECRET_KEY"]

def get_enabled_currencies():
    account = stripe.Account.retrieve()
    country = account.get("country", "US")
    spec = stripe.CountrySpec.retrieve(country)
    supported = spec.get("supported_payment_currencies") or []
    return {c.lower() for c in supported}
step2.js
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

async function getEnabledCurrencies() {
  const account = await stripe.accounts.retrieve();
  const country = account.country || "US";
  const spec = await stripe.countrySpecs.retrieve(country);
  const supported = spec.supported_payment_currencies || [];
  return new Set(supported.map((c) => c.toLowerCase()));
}
3

Load the orders worth checking

We only care about orders that are still open or already failed, since a completed order already proved its currency worked. Use the WooCommerce REST API to page through orders with status pending, on-hold, or failed. Going through the REST API keeps this working the same on stores with High Performance Order Storage (HPOS) turned on.

step3.py
import os, requests
from requests.auth import HTTPBasicAuth

WOO_URL = os.environ["WOO_STORE_URL"].rstrip("/")
AUTH = HTTPBasicAuth(os.environ["WOO_CONSUMER_KEY"], os.environ["WOO_CONSUMER_SECRET"])

def checkable_orders():
    page = 1
    while True:
        r = requests.get(
            f"{WOO_URL}/wp-json/wc/v3/orders",
            params={"status": "pending,on-hold,failed", "per_page": 50, "page": page},
            auth=AUTH, timeout=30,
        )
        r.raise_for_status()
        batch = r.json()
        if not batch:
            return
        for order in batch:
            yield order
        page += 1
step3.js
const WOO_URL = process.env.WOO_STORE_URL.replace(/\/$/, "");
const AUTH = "Basic " + Buffer.from(
  `${process.env.WOO_CONSUMER_KEY}:${process.env.WOO_CONSUMER_SECRET}`
).toString("base64");

async function woo(path, options = {}) {
  const res = await fetch(`${WOO_URL}/wp-json/wc/v3${path}`, {
    ...options,
    headers: { "Content-Type": "application/json", Authorization: AUTH, ...(options.headers || {}) },
  });
  if (!res.ok) throw new Error(`Woo ${path} returned ${res.status}`);
  return res.json();
}

async function* checkableOrders() {
  let page = 1;
  while (true) {
    const batch = await woo(`/orders?status=pending,on-hold,failed&per_page=50&page=${page}`);
    if (!batch.length) return;
    for (const order of batch) yield order;
    page++;
  }
}
4

Decide, with one pure function

Keep the decision in its own function that takes an order, the set of enabled currencies, and the PaymentIntent (or null). A pure function like this is easy to read and easy to test, which we do later. The rule is simple. Skip orders in a status we do not care about, or with no currency set. Flag an order when its currency is not on the account's list, or when Stripe already returned a currency related error code. Otherwise, leave it alone.

decide.py
CHECKABLE_STATUSES = {"pending", "on-hold", "failed"}
CURRENCY_ERROR_CODES = {"currency_not_enabled", "moto_not_supported"}

def order_currency(order):
    return (order.get("currency") or "").lower()

def decide(order, enabled_currencies, intent=None):
    if order["status"] not in CHECKABLE_STATUSES:
        return ("skip", "order is not pending, on-hold, or failed")

    currency = order_currency(order)
    if not currency:
        return ("skip", "order has no currency set")

    enabled = {c.lower() for c in enabled_currencies}
    last_error_code = None
    if intent is not None:
        last_error_code = (intent.get("last_payment_error") or {}).get("code")

    if currency not in enabled:
        return ("flag", f"currency {currency} is not enabled on the Stripe account")
    if last_error_code in CURRENCY_ERROR_CODES:
        return ("flag", f"Stripe rejected the charge with {last_error_code}")
    return ("skip", "currency is enabled and no currency related error was found")
decide.js
const CHECKABLE_STATUSES = new Set(["pending", "on-hold", "failed"]);
const CURRENCY_ERROR_CODES = new Set(["currency_not_enabled", "moto_not_supported"]);

export function orderCurrency(order) {
  return (order.currency || "").toLowerCase();
}

export function decide(order, enabledCurrencies, intent = null) {
  if (!CHECKABLE_STATUSES.has(order.status)) {
    return ["skip", "order is not pending, on-hold, or failed"];
  }
  const currency = orderCurrency(order);
  if (!currency) return ["skip", "order has no currency set"];

  const enabled = new Set(Array.from(enabledCurrencies, (c) => c.toLowerCase()));
  const lastErrorCode = intent && intent.last_payment_error ? intent.last_payment_error.code : null;

  if (!enabled.has(currency)) {
    return ["flag", `currency ${currency} is not enabled on the Stripe account`];
  }
  if (lastErrorCode && CURRENCY_ERROR_CODES.has(lastErrorCode)) {
    return ["flag", `Stripe rejected the charge with ${lastErrorCode}`];
  }
  return ["skip", "currency is enabled and no currency related error was found"];
}
5

Flag the order with a clear, actionable note

When the action is flag, we do not guess at a fix. We add an order note that says exactly what to do: enable the currency in the Stripe Dashboard, or refund and rebill in a currency that already works. Optionally, when REVIEW_HOLD is true, the order is also moved to On hold so it does not get missed in a busy orders list.

apply.py
def flag(order, reason):
    requests.post(
        f"{WOO_URL}/wp-json/wc/v3/orders/{order['id']}/notes",
        json={"note": f"Payment check failed: {reason}. Enable this currency in the Stripe "
                      f"Dashboard under Settings, Payment methods, or refund and rebill the "
                      f"buyer in a supported currency. Please review."},
        auth=AUTH, timeout=30,
    ).raise_for_status()
    if REVIEW_HOLD and order["status"] != "on-hold":
        requests.put(
            f"{WOO_URL}/wp-json/wc/v3/orders/{order['id']}",
            json={"status": "on-hold"}, auth=AUTH, timeout=30,
        ).raise_for_status()
apply.js
async function flag(order, reason) {
  await woo(`/orders/${order.id}/notes`, {
    method: "POST",
    body: JSON.stringify({
      note: `Payment check failed: ${reason}. Enable this currency in the Stripe Dashboard ` +
            `under Settings, Payment methods, or refund and rebill the buyer in a supported ` +
            `currency. Please review.`,
    }),
  });
  if (REVIEW_HOLD && order.status !== "on-hold") {
    await woo(`/orders/${order.id}`, { method: "PUT", body: JSON.stringify({ status: "on-hold" }) });
  }
}
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 reports what it would flag. Read the output, trust it, then switch it off to let it write notes. Run it on a schedule with cron every hour or so, since currency problems do not need minute by minute checking.

Run it safe

Always start with DRY_RUN=true. The script never changes an order's currency or resubmits a charge on its own, that decision needs a human, but it does write order notes and can move orders to on-hold, so you still want to see its plan before it acts.

The full code

Here is the complete checker 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 never touches an order whose currency already works.

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

detect_currency_not_enabled.py
"""Detect WooCommerce orders that failed, or are about to fail, because their
currency is not enabled on the connected Stripe account.

Stripe accounts only accept a fixed list of settlement currencies. If a store adds
a new store currency, a multi-currency plugin, or a manual order in a currency the
Stripe account was never approved for, the charge fails with an error such as
"currency_not_enabled" or "moto_not_supported" (or amount_too_small/large depending
on the mismatched minor unit). This script lists the account's enabled currencies
once, then walks recent orders and flags any whose currency Stripe will reject or
already rejected, before the shopper hits a confusing decline. Read only by
default. Run on a schedule.
"""
import os
import logging
import stripe
import requests
from requests.auth import HTTPBasicAuth

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

stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
WOO_URL = os.environ["WOO_STORE_URL"].rstrip("/")
AUTH = HTTPBasicAuth(os.environ["WOO_CONSUMER_KEY"], os.environ["WOO_CONSUMER_SECRET"])
REVIEW_HOLD = os.environ.get("REVIEW_HOLD", "false").lower() == "true"
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"

# Orders worth checking: still open (pending, on-hold) or already failed.
CHECKABLE_STATUSES = {"pending", "on-hold", "failed"}

# Stripe error codes that mean the currency itself is the problem, not the card.
CURRENCY_ERROR_CODES = {"currency_not_enabled", "moto_not_supported"}


def intent_id_of(order):
    """The saved Stripe PaymentIntent id, from meta _stripe_intent_id or transaction_id."""
    for meta in order.get("meta_data") or []:
        if meta.get("key") == "_stripe_intent_id" and meta.get("value"):
            return meta["value"]
    tid = order.get("transaction_id")
    return tid if tid and tid.startswith("pi_") else None


def order_currency(order):
    """WooCommerce order currency, lower cased to match Stripe's format."""
    return (order.get("currency") or "").lower()


def decide(order, enabled_currencies, intent=None):
    """Pure decision function. No I/O.

    order: dict with at least "status" and "currency".
    enabled_currencies: set/list of lower case currency codes the Stripe account accepts.
    intent: optional Stripe PaymentIntent dict (or None if none was ever created).

    Returns a tuple of (action, reason).
    """
    if order["status"] not in CHECKABLE_STATUSES:
        return ("skip", "order is not pending, on-hold, or failed")

    currency = order_currency(order)
    if not currency:
        return ("skip", "order has no currency set")

    enabled = {c.lower() for c in enabled_currencies}
    currency_supported = currency in enabled

    last_error_code = None
    if intent is not None:
        last_error = intent.get("last_payment_error") or {}
        last_error_code = last_error.get("code")

    if not currency_supported:
        return ("flag", f"currency {currency} is not enabled on the Stripe account")

    if last_error_code in CURRENCY_ERROR_CODES:
        return ("flag", f"Stripe rejected the charge with {last_error_code}")

    return ("skip", "currency is enabled and no currency related error was found")


def get_enabled_currencies():
    """The settlement currencies the connected Stripe account can charge in.

    Stripe's capabilities are not currency specific, so the reliable source is the
    country spec for the account's country, which lists every currency that country
    is allowed to accept payments in.
    """
    account = stripe.Account.retrieve()
    country = account.get("country", "US")
    spec = stripe.CountrySpec.retrieve(country)
    supported = spec.get("supported_payment_currencies") or []
    return {c.lower() for c in supported}


def get_intent(intent_id):
    if not intent_id:
        return None
    try:
        return stripe.PaymentIntent.retrieve(intent_id)
    except stripe.error.InvalidRequestError:
        return None


def checkable_orders():
    page = 1
    while True:
        r = requests.get(
            f"{WOO_URL}/wp-json/wc/v3/orders",
            params={
                "status": "pending,on-hold,failed",
                "per_page": 50,
                "page": page,
            },
            auth=AUTH, timeout=30,
        )
        r.raise_for_status()
        batch = r.json()
        if not batch:
            return
        for order in batch:
            yield order
        page += 1


def flag(order, reason):
    requests.post(
        f"{WOO_URL}/wp-json/wc/v3/orders/{order['id']}/notes",
        json={"note": f"Payment check failed: {reason}. Enable this currency in the Stripe "
                      f"Dashboard under Settings, Payment methods, or refund and rebill the "
                      f"buyer in a supported currency. Please review."},
        auth=AUTH, timeout=30,
    ).raise_for_status()
    if REVIEW_HOLD and order["status"] != "on-hold":
        requests.put(
            f"{WOO_URL}/wp-json/wc/v3/orders/{order['id']}",
            json={"status": "on-hold"}, auth=AUTH, timeout=30,
        ).raise_for_status()


def run():
    enabled_currencies = get_enabled_currencies()
    log.info("Stripe account accepts: %s", ", ".join(sorted(enabled_currencies)) or "(none found)")
    flagged = 0
    for order in checkable_orders():
        intent = get_intent(intent_id_of(order))
        action, reason = decide(order, enabled_currencies, intent)
        if action != "flag":
            continue
        log.warning("Order %s: %s. %s", order["id"], reason, "would flag" if DRY_RUN else "flagging")
        if not DRY_RUN:
            flag(order, reason)
        flagged += 1
    log.info("Done. %d order(s) %s.", flagged, "to flag" if DRY_RUN else "flagged")


if __name__ == "__main__":
    run()
detect-currency-not-enabled.js
/**
 * Detect WooCommerce orders that failed, or are about to fail, because their
 * currency is not enabled on the connected Stripe account.
 *
 * Stripe accounts only accept a fixed list of settlement currencies. If a store
 * adds a new store currency, a multi-currency plugin, or a manual order in a
 * currency the Stripe account was never approved for, the charge fails with an
 * error such as "currency_not_enabled" (or a related code). This script lists the
 * account's enabled currencies once, then walks recent orders and flags any whose
 * currency Stripe will reject or already rejected, before the shopper hits a
 * confusing decline. Read only by default. Run on a schedule.
 *
 * Guide: https://www.allanninal.dev/woocommerce/currency-not-enabled-on-stripe/
 */
import Stripe from "stripe";
import { pathToFileURL } from "node:url";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "sk_test_dummy");
const WOO_URL = (process.env.WOO_STORE_URL || "https://example.com").replace(/\/$/, "");
const AUTH = "Basic " + Buffer.from(
  `${process.env.WOO_CONSUMER_KEY || "ck_dummy"}:${process.env.WOO_CONSUMER_SECRET || "cs_dummy"}`
).toString("base64");
const REVIEW_HOLD = (process.env.REVIEW_HOLD || "false").toLowerCase() === "true";
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";

// Orders worth checking: still open (pending, on-hold) or already failed.
const CHECKABLE_STATUSES = new Set(["pending", "on-hold", "failed"]);

// Stripe error codes that mean the currency itself is the problem, not the card.
const CURRENCY_ERROR_CODES = new Set(["currency_not_enabled", "moto_not_supported"]);

export function intentIdOf(order) {
  for (const meta of order.meta_data || []) {
    if (meta.key === "_stripe_intent_id" && meta.value) return meta.value;
  }
  const tid = order.transaction_id;
  return tid && tid.startsWith("pi_") ? tid : null;
}

export function orderCurrency(order) {
  return (order.currency || "").toLowerCase();
}

/**
 * Pure decision function. No I/O.
 *
 * order: object with at least "status" and "currency".
 * enabledCurrencies: Set or array of lower case currency codes the Stripe account accepts.
 * intent: optional Stripe PaymentIntent object (or null if none was ever created).
 *
 * Returns [action, reason]. action is one of "skip" or "flag".
 */
export function decide(order, enabledCurrencies, intent = null) {
  if (!CHECKABLE_STATUSES.has(order.status)) {
    return ["skip", "order is not pending, on-hold, or failed"];
  }

  const currency = orderCurrency(order);
  if (!currency) return ["skip", "order has no currency set"];

  const enabled = new Set(Array.from(enabledCurrencies, (c) => c.toLowerCase()));
  const currencySupported = enabled.has(currency);

  const lastErrorCode = intent && intent.last_payment_error ? intent.last_payment_error.code : null;

  if (!currencySupported) {
    return ["flag", `currency ${currency} is not enabled on the Stripe account`];
  }

  if (lastErrorCode && CURRENCY_ERROR_CODES.has(lastErrorCode)) {
    return ["flag", `Stripe rejected the charge with ${lastErrorCode}`];
  }

  return ["skip", "currency is enabled and no currency related error was found"];
}

async function woo(path, options = {}) {
  const res = await fetch(`${WOO_URL}/wp-json/wc/v3${path}`, {
    ...options,
    headers: { "Content-Type": "application/json", Authorization: AUTH, ...(options.headers || {}) },
  });
  if (!res.ok) throw new Error(`Woo ${path} returned ${res.status}`);
  return res.json();
}

async function getEnabledCurrencies() {
  // Stripe's capabilities are not currency specific, so the reliable source is the
  // country spec for the account's country, which lists every currency that
  // country is allowed to accept payments in.
  const account = await stripe.accounts.retrieve();
  const country = account.country || "US";
  const spec = await stripe.countrySpecs.retrieve(country);
  const supported = spec.supported_payment_currencies || [];
  return new Set(supported.map((c) => c.toLowerCase()));
}

async function getIntent(intentId) {
  if (!intentId) return null;
  try {
    return await stripe.paymentIntents.retrieve(intentId);
  } catch {
    return null;
  }
}

async function* checkableOrders() {
  let page = 1;
  while (true) {
    const batch = await woo(`/orders?status=pending,on-hold,failed&per_page=50&page=${page}`);
    if (!batch.length) return;
    for (const order of batch) yield order;
    page++;
  }
}

async function flag(order, reason) {
  await woo(`/orders/${order.id}/notes`, {
    method: "POST",
    body: JSON.stringify({
      note: `Payment check failed: ${reason}. Enable this currency in the Stripe Dashboard ` +
            `under Settings, Payment methods, or refund and rebill the buyer in a supported ` +
            `currency. Please review.`,
    }),
  });
  if (REVIEW_HOLD && order.status !== "on-hold") {
    await woo(`/orders/${order.id}`, { method: "PUT", body: JSON.stringify({ status: "on-hold" }) });
  }
}

export async function run() {
  const enabledCurrencies = await getEnabledCurrencies();
  console.log(`Stripe account accepts: ${Array.from(enabledCurrencies).sort().join(", ") || "(none found)"}`);
  let flagged = 0;
  for await (const order of checkableOrders()) {
    const intent = await getIntent(intentIdOf(order));
    const [action, reason] = decide(order, enabledCurrencies, intent);
    if (action !== "flag") continue;
    console.warn(`Order ${order.id}: ${reason}. ${DRY_RUN ? "would flag" : "flagging"}`);
    if (!DRY_RUN) await flag(order, reason);
    flagged++;
  }
  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 which orders get a note added and possibly moved to on-hold. Because we kept decide pure, the test needs no network and no Stripe account. It just feeds in plain objects and checks the action.

test_currency_decide.py
from detect_currency_not_enabled import decide, intent_id_of, order_currency

ENABLED = {"usd", "eur", "gbp"}


def intent(**over):
    base = {"last_payment_error": None}
    base.update(over)
    return base


def test_flag_when_currency_not_enabled():
    order = {"status": "pending", "currency": "SEK"}
    action, reason = decide(order, ENABLED, intent())
    assert action == "flag"
    assert "sek" in reason


def test_skip_when_currency_enabled_and_no_intent():
    order = {"status": "pending", "currency": "USD"}
    assert decide(order, ENABLED, None)[0] == "skip"


def test_flag_when_stripe_reports_currency_not_enabled_error():
    order = {"status": "failed", "currency": "EUR"}
    bad_intent = intent(last_payment_error={"code": "currency_not_enabled"})
    action, reason = decide(order, ENABLED, bad_intent)
    assert action == "flag"
    assert "currency_not_enabled" in reason


def test_skip_when_order_not_in_checkable_status():
    order = {"status": "processing", "currency": "SEK"}
    assert decide(order, ENABLED, None)[0] == "skip"


def test_intent_id_from_meta():
    order = {"meta_data": [{"key": "_stripe_intent_id", "value": "pi_123"}], "transaction_id": ""}
    assert intent_id_of(order) == "pi_123"
detect-currency-not-enabled.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { decide, intentIdOf, orderCurrency } from "./detect-currency-not-enabled.js";

const ENABLED = new Set(["usd", "eur", "gbp"]);

test("flag when currency not enabled", () => {
  const [action, reason] = decide({ status: "pending", currency: "SEK" }, ENABLED, null);
  assert.equal(action, "flag");
  assert.match(reason, /sek/);
});

test("skip when currency enabled and no intent", () => {
  assert.equal(decide({ status: "pending", currency: "USD" }, ENABLED, null)[0], "skip");
});

test("flag when Stripe reports currency_not_enabled error", () => {
  const intent = { last_payment_error: { code: "currency_not_enabled" } };
  const [action, reason] = decide({ status: "failed", currency: "EUR" }, ENABLED, intent);
  assert.equal(action, "flag");
  assert.match(reason, /currency_not_enabled/);
});

test("skip when order not in checkable status", () => {
  assert.equal(decide({ status: "processing", currency: "SEK" }, ENABLED, null)[0], "skip");
});

test("intentIdOf from meta", () => {
  assert.equal(intentIdOf({ meta_data: [{ key: "_stripe_intent_id", value: "pi_123" }], transaction_id: "" }), "pi_123");
});

Case studies

Multi-currency plugin

The store that turned on a currency nobody tested

A shop enabled a multi-currency plugin to show prices in the shopper's local currency at checkout. Most currencies worked fine, but one regional currency was never actually approved on the Stripe account. Every order in that currency failed silently, and support could not find a pattern for weeks.

The checker flagged the pattern on its first run: every failed order shared the same unenabled currency. The store enabled it in the Stripe Dashboard the same day and the failures stopped.

B2B channel

The wholesale orders that never went through

A wholesale channel started quoting invoices in a currency used by one regional distributor. The Stripe account behind the storefront had never been approved for it, so every manual order created in that currency sat on Pending with no useful error.

Running the script in dry run surfaced all nine affected orders in one pass, each with a note explaining the exact currency at fault, instead of nine separate support tickets.

What good looks like

After this runs on a schedule, a currency mismatch is caught within the hour instead of showing up as a pile of confusing support tickets. The shop manager gets a note that names the exact currency and the exact fix, so enabling it in Stripe or rebilling the buyer takes minutes, not a debugging session.

FAQ

Why does my WooCommerce order fail with a currency error on Stripe?

Stripe accounts only accept charges in a fixed list of currencies approved for the account's country. If a store adds a new store currency, a multi-currency plugin, or a manual order in a currency Stripe was never approved for, the charge is rejected with an error such as currency_not_enabled, and the order stays unpaid.

How do I know which currencies my Stripe account can accept?

Stripe exposes the accepted settlement currencies for your account's country through the country spec endpoint. A script can fetch that list once and compare it against every order's currency instead of guessing from a failed charge alone.

Can I fix an order that already failed for this reason?

The script does not silently change the currency or resubmit the charge, since that decision belongs to a human. Instead it flags the order with a clear note so the shop manager can enable the currency in Stripe, or refund and rebill the buyer in a supported currency.

Related field notes

Citations

On the problem:

  1. Stripe docs: supported currencies and how an account's country limits which currencies it can settle in. docs.stripe.com/currencies
  2. Stripe error codes reference: currency_not_enabled and related decline codes. docs.stripe.com/error-codes
  3. WooCommerce docs: multi-currency support and its limits with the Stripe gateway. woocommerce.com/document/stripe

On the solution:

  1. Stripe API: retrieve a country spec, including its supported payment currencies. docs.stripe.com/api/country_specs
  2. Stripe API: retrieve a PaymentIntent, including the last_payment_error field. docs.stripe.com/api/payment_intents/retrieve
  3. WooCommerce REST API: update an order and add an order note. woocommerce.github.io/woocommerce-rest-api-docs

Stuck on a tricky one?

If you have a bug in WooCommerce, WooCommerce Subscriptions, or the WooCommerce Stripe gateway 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 currency problem for you?

If this saved you a pile of confusing support tickets or a lot of guesswork, 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 WooCommerce and Stripe field notes