Repair Cancellation sync

WooCommerce Subscriptions still try to bill after WooPayments is turned off

You disabled WooPayments, maybe to switch to a different processor or to pause the store, and assumed billing stopped with it. It did not. WooCommerce Subscriptions keeps its own renewal schedule, and every subscription still points at the payment method it was created with. The scheduled action fires anyway, tries to charge through a gateway that no longer exists, and fails quietly. Here is why that happens and a small script that finds every subscription still wired to the disabled gateway and moves it to manual renewal so it stops trying.

Python and Node.js One sweep, then a short schedule Safe by default (dry run)
A close up of a calendar on a table
Photo by Road Ahead on Unsplash
The short answer

Turning off WooPayments does not touch existing subscriptions. Each one still has payment_method: woocommerce_payments saved on it, and WooCommerce Subscriptions will keep trying to run automatic renewals through that gateway even though it is disabled, so every attempt fails. Run a small Python or Node.js script that lists active and on-hold subscriptions through the WooCommerce REST API, finds the ones still set to the disabled gateway, and sets requires_manual_renewal to true so the automatic charge attempt stops. Full code, tests, and a dry run guard are below.

The problem in plain words

WooPayments is a payment gateway plugin. WooCommerce Subscriptions is a separate plugin that manages renewal dates and billing schedules. They work together, but one does not control the other. When you disable WooPayments in WooCommerce, Settings, Payments, you have only removed it from the list of gateways a customer can choose at checkout. You have not told any existing subscription to stop using it.

Every subscription that was created while WooPayments was active still has that gateway saved as its payment_method. WooCommerce Subscriptions runs a scheduled action for each subscription's next payment date regardless of which gateways are currently enabled. When that action fires, it asks the saved gateway to process the renewal. Since the gateway is disabled, the charge cannot go through, and the renewal fails, over and over, on every future due date, usually with no clear alert to anyone until a customer notices they were never billed, or worse, notices a failed payment retry that they did not expect.

WooPayments disabled in settings Subscription record still saved: woopayments Renewal date fires on schedule Renewal fails gateway not active
Disabling the gateway only removes it from checkout. The subscription still points at it, so the scheduled renewal keeps trying and keeps failing.

Why it happens

The WooCommerce Subscriptions documentation is direct about this: automatic renewals depend on the payment gateway saved on the subscription supporting scheduled payments, and if that gateway is deactivated, renewals will fail. A few ways stores end up here:

WooCommerce Subscriptions does have a setting, requires_manual_renewal, that is meant for exactly this situation. It tells the scheduler "do not attempt an automatic charge, wait for the customer or the shop manager to pay manually." Nothing sets that flag automatically when a gateway is disabled, which is the gap this fix closes.

The key insight

A subscription's payment method is a fact stored on the subscription, not a live check against your current settings. Disabling a gateway changes what a customer can pick tomorrow. It does nothing to a subscription that already picked it last year. The fix is not to reconnect the old gateway, it is to tell every affected subscription to stop trying automatically until a person or a new payment method takes over.

The fix, as a flow

We do not touch pricing, the next payment date, or line items. We add a script that lists subscriptions in a state where a renewal could still fire, checks whether the saved payment method is the disabled gateway, and if so, flips requires_manual_renewal to true. That one flag is what stops WooCommerce Subscriptions from trying to auto charge. If the most recent renewal order already tried and failed, we also read its Stripe PaymentIntent so the report shows exactly what happened, without changing that order.

Run the script once, then a short schedule List subscriptions status active, on-hold Read payment_method and manual renewal flag Method is the disabled gateway? yes no, skip Set manual renewal + add note
The script only flips a subscription to manual renewal when its saved payment method matches a gateway you have named as disabled. Price, next payment date, and everything else stay exactly as they were.

Build it step by step

1

Get access and name the disabled gateway

You need a WooCommerce REST API key pair (consumer key and secret) with read and write access to subscriptions, plus a Stripe secret key if you want the report to show what a failed renewal attempt actually returned. List the gateway ids you disabled, usually just woocommerce_payments, in an environment variable so the script knows what to look for.

setup (shell)
pip install stripe requests

export WOO_STORE_URL="https://yourstore.com"
export WOO_CONSUMER_KEY="ck_..."
export WOO_CONSUMER_SECRET="cs_..."
export STRIPE_SECRET_KEY="sk_live_..."
export DISABLED_GATEWAYS="woocommerce_payments"
export DRY_RUN="true"   # start safe, change to false to write
setup (shell)
npm install stripe

export WOO_STORE_URL="https://yourstore.com"
export WOO_CONSUMER_KEY="ck_..."
export WOO_CONSUMER_SECRET="cs_..."
export STRIPE_SECRET_KEY="sk_live_..."
export DISABLED_GATEWAYS="woocommerce_payments"
export DRY_RUN="true"   // start safe, change to false to write
2

List subscriptions that could still try to bill

Only active and on-hold subscriptions run automatic renewals, so those are the only statuses worth checking. We page through the WooCommerce REST API rather than querying the database directly, which keeps the script working the same way whether the store uses classic order storage or High Performance Order Storage (HPOS), since WooCommerce Subscriptions is built on top of the same order tables.

step2.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 billable_subscriptions():
    page = 1
    while True:
        r = requests.get(
            f"{WOO_URL}/wp-json/wc/v3/subscriptions",
            params={"status": "active,on-hold", "per_page": 50, "page": page},
            auth=AUTH, timeout=30,
        )
        r.raise_for_status()
        batch = r.json()
        if not batch:
            return
        for sub in batch:
            yield sub
        page += 1
step2.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* billableSubscriptions() {
  let page = 1;
  while (true) {
    const batch = await woo(`/subscriptions?status=active,on-hold&per_page=50&page=${page}`);
    if (!batch.length) return;
    for (const sub of batch) yield sub;
    page++;
  }
}
3

Read the last renewal attempt from Stripe, if there was one

If the most recent renewal order already tried to charge, its PaymentIntent id is usually saved in order meta as _stripe_intent_id, or as the order's transaction_id when it starts with pi_. Reading it back from Stripe tells us whether the failure was the gateway being gone entirely, versus a normal decline, which matters for the note we leave on the subscription.

step3.py
import stripe

def intent_id_of(order):
    for meta in (order or {}).get("meta_data") or []:
        if meta.get("key") == "_stripe_intent_id" and meta.get("value"):
            return meta["value"]
    tid = (order or {}).get("transaction_id")
    return tid if tid and tid.startswith("pi_") else None

def get_intent(intent_id):
    if not intent_id:
        return None
    try:
        return stripe.PaymentIntent.retrieve(intent_id)
    except stripe.error.InvalidRequestError:
        return None
step3.js
export function intentIdOf(order) {
  for (const meta of (order && order.meta_data) || []) {
    if (meta.key === "_stripe_intent_id" && meta.value) return meta.value;
  }
  const tid = order && order.transaction_id;
  return tid && tid.startsWith("pi_") ? tid : null;
}

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

Decide, with one pure function

The decision only needs the subscription and the list of disabled gateway ids. It never needs the network, so it is easy to test on its own. Skip subscriptions that already require manual renewal, skip ones on a gateway that is still enabled, and mark the rest for repair.

decide.py
BILLABLE_STATUSES = {"active", "on-hold"}

def is_manual(subscription):
    value = subscription.get("requires_manual_renewal")
    return value in (True, "true", 1, "1")

def decide(subscription, disabled_gateways):
    if subscription["status"] not in BILLABLE_STATUSES:
        return ("skip", "subscription is not billable")
    if is_manual(subscription):
        return ("skip", "already set to manual renewal")
    method = subscription.get("payment_method") or ""
    if method not in disabled_gateways:
        return ("skip", "payment method is not a disabled gateway")
    return ("repair", f"payment method '{method}' is disabled, would auto bill and fail")
decide.js
const BILLABLE_STATUSES = new Set(["active", "on-hold"]);

export function isManual(subscription) {
  const value = subscription.requires_manual_renewal;
  return value === true || value === "true" || value === 1 || value === "1";
}

export function decide(subscription, disabledGateways) {
  if (!BILLABLE_STATUSES.has(subscription.status)) return ["skip", "subscription is not billable"];
  if (isManual(subscription)) return ["skip", "already set to manual renewal"];
  const method = subscription.payment_method || "";
  if (!disabledGateways.includes(method)) return ["skip", "payment method is not a disabled gateway"];
  return ["repair", `payment method '${method}' is disabled, would auto bill and fail`];
}
5

Flip the flag and leave a note

When the action is repair, update the subscription with requires_manual_renewal: true through the REST API. Nothing else on the subscription changes, not the price, not the next payment date, not the line items. Add a note explaining why, so the shop manager or the customer support team sees the reason if they open the subscription later.

apply.py
def set_manual_renewal(subscription_id, method):
    requests.put(
        f"{WOO_URL}/wp-json/wc/v3/subscriptions/{subscription_id}",
        json={"requires_manual_renewal": True},
        auth=AUTH, timeout=30,
    ).raise_for_status()
    requests.post(
        f"{WOO_URL}/wp-json/wc/v3/subscriptions/{subscription_id}/notes",
        json={"note": f"Moved to manual renewal. Payment method '{method}' is disabled, "
                      f"so automatic renewal would keep failing. Price and next payment "
                      f"date were not changed."},
        auth=AUTH, timeout=30,
    ).raise_for_status()
apply.js
async function setManualRenewal(subscriptionId, method) {
  await woo(`/subscriptions/${subscriptionId}`, {
    method: "PUT",
    body: JSON.stringify({ requires_manual_renewal: true }),
  });
  await woo(`/subscriptions/${subscriptionId}/notes`, {
    method: "POST",
    body: JSON.stringify({
      note: `Moved to manual renewal. Payment method '${method}' is disabled, ` +
            `so automatic renewal would keep failing. Price and next payment date were not changed.`,
    }),
  });
}
6

Wire it together with a dry run guard

The loop ties every piece together. Leave DRY_RUN on for the first pass so the script only reports what it would repair. Read the list, confirm the payment method and status look right, then switch it off. Run it once right after disabling the gateway, then again daily for a week or two in case a renewal was already queued.

Run it safe

Always start with DRY_RUN=true. This script changes a real billing setting on live subscriptions, so you want to see the exact list before it writes anything.

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, since it skips any subscription that is already on manual renewal or already on a gateway that is still enabled.

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

stop_billing_on_disabled_gateway.py
"""Stop WooCommerce Subscriptions from trying to auto bill through a gateway
you have disabled, such as WooPayments. Moves affected subscriptions to
manual renewal without changing price, next payment date, or line items.
Safe to run again and again.
"""
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("stop_billing_on_disabled_gateway")

stripe.api_key = os.environ.get("STRIPE_SECRET_KEY", "")
WOO_URL = os.environ["WOO_STORE_URL"].rstrip("/")
AUTH = HTTPBasicAuth(os.environ["WOO_CONSUMER_KEY"], os.environ["WOO_CONSUMER_SECRET"])
DISABLED_GATEWAYS = [g.strip() for g in os.environ.get("DISABLED_GATEWAYS", "woocommerce_payments").split(",") if g.strip()]
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"

BILLABLE_STATUSES = {"active", "on-hold"}


def is_manual(subscription):
    value = subscription.get("requires_manual_renewal")
    return value in (True, "true", 1, "1")


def decide(subscription, disabled_gateways):
    if subscription["status"] not in BILLABLE_STATUSES:
        return ("skip", "subscription is not billable")
    if is_manual(subscription):
        return ("skip", "already set to manual renewal")
    method = subscription.get("payment_method") or ""
    if method not in disabled_gateways:
        return ("skip", "payment method is not a disabled gateway")
    return ("repair", f"payment method '{method}' is disabled, would auto bill and fail")


def intent_id_of(order):
    for meta in (order or {}).get("meta_data") or []:
        if meta.get("key") == "_stripe_intent_id" and meta.get("value"):
            return meta["value"]
    tid = (order or {}).get("transaction_id")
    return tid if tid and tid.startswith("pi_") else None


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


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


def set_manual_renewal(subscription_id, method):
    requests.put(
        f"{WOO_URL}/wp-json/wc/v3/subscriptions/{subscription_id}",
        json={"requires_manual_renewal": True},
        auth=AUTH, timeout=30,
    ).raise_for_status()
    requests.post(
        f"{WOO_URL}/wp-json/wc/v3/subscriptions/{subscription_id}/notes",
        json={"note": f"Moved to manual renewal. Payment method '{method}' is disabled, "
                      f"so automatic renewal would keep failing. Price and next payment "
                      f"date were not changed."},
        auth=AUTH, timeout=30,
    ).raise_for_status()


def run():
    repaired = 0
    for subscription in billable_subscriptions():
        action, reason = decide(subscription, DISABLED_GATEWAYS)
        if action != "repair":
            continue
        last_order = subscription.get("last_order")
        intent = get_intent(intent_id_of(last_order)) if isinstance(last_order, dict) else None
        detail = f" Last attempt on Stripe: {intent['status']}." if intent else ""
        log.warning("Subscription %s: %s.%s %s", subscription["id"], reason, detail,
                    "would repair" if DRY_RUN else "repairing")
        if not DRY_RUN:
            set_manual_renewal(subscription["id"], subscription.get("payment_method") or "")
        repaired += 1
    log.info("Done. %d subscription(s) %s.", repaired, "to repair" if DRY_RUN else "repaired")


if __name__ == "__main__":
    run()
stop-billing-on-disabled-gateway.js
/**
 * Stop WooCommerce Subscriptions from trying to auto bill through a gateway
 * you have disabled, such as WooPayments. Moves affected subscriptions to
 * manual renewal without changing price, next payment date, or line items.
 * Safe to run again and again.
 */
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.replace(/\/$/, "");
const AUTH = "Basic " + Buffer.from(
  `${process.env.WOO_CONSUMER_KEY}:${process.env.WOO_CONSUMER_SECRET}`
).toString("base64");
const DISABLED_GATEWAYS = (process.env.DISABLED_GATEWAYS || "woocommerce_payments")
  .split(",").map((g) => g.trim()).filter(Boolean);
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";

const BILLABLE_STATUSES = new Set(["active", "on-hold"]);

export function isManual(subscription) {
  const value = subscription.requires_manual_renewal;
  return value === true || value === "true" || value === 1 || value === "1";
}

export function decide(subscription, disabledGateways) {
  if (!BILLABLE_STATUSES.has(subscription.status)) return ["skip", "subscription is not billable"];
  if (isManual(subscription)) return ["skip", "already set to manual renewal"];
  const method = subscription.payment_method || "";
  if (!disabledGateways.includes(method)) return ["skip", "payment method is not a disabled gateway"];
  return ["repair", `payment method '${method}' is disabled, would auto bill and fail`];
}

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

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 getIntent(intentId) {
  if (!intentId) return null;
  try {
    return await stripe.paymentIntents.retrieve(intentId);
  } catch {
    return null;
  }
}

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

async function setManualRenewal(subscriptionId, method) {
  await woo(`/subscriptions/${subscriptionId}`, {
    method: "PUT",
    body: JSON.stringify({ requires_manual_renewal: true }),
  });
  await woo(`/subscriptions/${subscriptionId}/notes`, {
    method: "POST",
    body: JSON.stringify({
      note: `Moved to manual renewal. Payment method '${method}' is disabled, ` +
            `so automatic renewal would keep failing. Price and next payment date were not changed.`,
    }),
  });
}

export async function run() {
  let repaired = 0;
  for await (const subscription of billableSubscriptions()) {
    const [action, reason] = decide(subscription, DISABLED_GATEWAYS);
    if (action !== "repair") continue;
    const lastOrder = subscription.last_order;
    const intent = lastOrder && typeof lastOrder === "object" ? await getIntent(intentIdOf(lastOrder)) : null;
    const detail = intent ? ` Last attempt on Stripe: ${intent.status}.` : "";
    console.warn(`Subscription ${subscription.id}: ${reason}.${detail} ${DRY_RUN ? "would repair" : "repairing"}`);
    if (!DRY_RUN) await setManualRenewal(subscription.id, subscription.payment_method || "");
    repaired++;
  }
  console.log(`Done. ${repaired} subscription(s) ${DRY_RUN ? "to repair" : "repaired"}.`);
}

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 worth testing, since it decides which live subscriptions get changed. Because decide is pure, no Stripe account and no WooCommerce store are needed. It only takes plain objects and checks the action it returns.

test_billing_decide.py
from stop_billing_on_disabled_gateway import decide, is_manual, intent_id_of


def sub(**over):
    base = {"status": "active", "payment_method": "woocommerce_payments", "requires_manual_renewal": False}
    base.update(over)
    return base


def test_repair_when_active_on_disabled_gateway():
    assert decide(sub(), ["woocommerce_payments"])[0] == "repair"


def test_repair_when_on_hold_on_disabled_gateway():
    assert decide(sub(status="on-hold"), ["woocommerce_payments"])[0] == "repair"


def test_skip_when_already_manual():
    assert decide(sub(requires_manual_renewal=True), ["woocommerce_payments"])[0] == "skip"


def test_skip_when_gateway_not_disabled():
    assert decide(sub(payment_method="stripe"), ["woocommerce_payments"])[0] == "skip"


def test_skip_when_not_billable_status():
    assert decide(sub(status="cancelled"), ["woocommerce_payments"])[0] == "skip"


def test_is_manual_accepts_string_true():
    assert is_manual({"requires_manual_renewal": "true"}) is True


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"
billing-decide.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { decide, isManual, intentIdOf } from "./stop-billing-on-disabled-gateway.js";

const sub = (over = {}) => ({
  status: "active", payment_method: "woocommerce_payments", requires_manual_renewal: false, ...over,
});

test("repair when active on disabled gateway", () => {
  assert.equal(decide(sub(), ["woocommerce_payments"])[0], "repair");
});

test("repair when on-hold on disabled gateway", () => {
  assert.equal(decide(sub({ status: "on-hold" }), ["woocommerce_payments"])[0], "repair");
});

test("skip when already manual", () => {
  assert.equal(decide(sub({ requires_manual_renewal: true }), ["woocommerce_payments"])[0], "skip");
});

test("skip when gateway not disabled", () => {
  assert.equal(decide(sub({ payment_method: "stripe" }), ["woocommerce_payments"])[0], "skip");
});

test("skip when not a billable status", () => {
  assert.equal(decide(sub({ status: "cancelled" }), ["woocommerce_payments"])[0], "skip");
});

test("isManual accepts string true", () => {
  assert.equal(isManual({ requires_manual_renewal: "true" }), true);
});

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

Case studies

Processor migration

The store that switched processors and forgot the old subscriptions

A store moved from WooPayments to a different processor over a weekend. New checkouts worked fine. Three days later, forty renewal attempts had failed silently, all on subscriptions that predated the switch and still pointed at WooPayments.

Running the script in dry run listed all forty in seconds. Once switched to manual renewal, the team emailed each customer a payment link instead of leaving them to guess why a card was never charged.

Account suspension

The account that was suspended without warning

A WooPayments account was placed under review by the payments provider, which disabled the gateway from the store's side without any obvious banner in wp-admin. Renewals kept firing and failing for a week before support noticed the pattern in the logs.

The script's daily schedule caught new failures as they appeared and moved each affected subscription to manual renewal, so no further silent retries happened while the account review was in progress.

What good looks like

After this runs, a disabled gateway stops being an invisible source of failed renewals. Every subscription that was still wired to it is either already migrated to a working gateway or clearly marked as manual, with a note explaining why. Keep the script around for a week or two after any gateway change, since a renewal can already be queued before you disable anything.

FAQ

Why do subscriptions still try to bill after I turned off WooPayments?

WooCommerce Subscriptions schedules renewals independently of which gateways are active. Each subscription still remembers the payment method it was set up with, so the scheduled action fires and tries to charge through a gateway that is no longer enabled, which fails every time until someone updates the subscription.

Is it safe to switch these subscriptions to manual renewal with a script?

Yes, when the script only touches active or on-hold subscriptions whose payment method is the disabled gateway and it never changes the subscription price, next payment date, or line items. Setting requires_manual_renewal to true just stops the automatic charge attempt, it does not cancel or alter anything else.

Should I run this once or on a schedule?

Run it once right after disabling WooPayments to sweep every existing subscription, then leave it on a daily schedule for a week or two in case a renewal was already queued before the gateway was turned off.

Related field notes

Citations

On the problem:

  1. WooCommerce Subscriptions docs: automatic renewals depend on the saved payment gateway being active and supporting scheduled payments. woocommerce.com/document/subscriptions/renewal-process
  2. WooCommerce Subscriptions docs: manual renewal payments and when a subscription is set to require them. woocommerce.com/document/subscriptions/manual-renewal-payments
  3. WooPayments docs: disabling or deactivating the WooPayments gateway from the store. woocommerce.com/document/woopayments/startup-guide

On the solution:

  1. WooCommerce Subscriptions REST API: read and update a subscription, including requires_manual_renewal. woocommerce.github.io/subscriptions-rest-api-docs
  2. Stripe API: retrieve a PaymentIntent by id to inspect the outcome of a past charge attempt. docs.stripe.com/api/payment_intents/retrieve
  3. WooCommerce REST API: add an order or subscription note for an audit trail. 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 stop your silent renewal failures?

If this saved you a pile of confused customers or a stretch of missed billing, 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