Repair Alternative payment methods
Stripe Link becomes manual
A customer breezes through checkout with Stripe Link, one click, no typing a card number, and the first payment goes through fine. Weeks later the renewal never charges. WooCommerce Subscriptions has quietly switched the subscription to manual renewal, so it emails an invoice and waits for the customer to pay it by hand, and most never do. Here is why Link checkouts can skip the step that saves a reusable payment method, and a small script that finds the subscriptions this happened to and switches the ones it safely can back to automatic.
Stripe Link can complete a payment without leaving a reusable payment method attached to the Stripe customer, so WooCommerce Subscriptions has nothing to charge next cycle and falls back to manual renewal. Run a small Python or Node.js job on a schedule that reads each manual subscription's last PaymentIntent (from order meta _stripe_intent_id or transaction_id), checks whether Stripe now has a reusable payment method on that customer, and switches the subscription back to automatic only when one genuinely exists. Full code, tests, and a dry run guard are below.
The problem in plain words
Stripe Link lets a returning shopper pay with one click using a phone number and a code, no card form, no re-typing anything. It is great for conversion. WooCommerce Subscriptions likes it too, until it needs to charge that customer again in thirty days.
To bill a subscription automatically, WooCommerce Subscriptions needs a payment method token saved against the Stripe customer, something it can hand back to Stripe next month and say "charge this one again." A normal card checkout attaches that token as part of the flow. Some Link checkouts finish the PaymentIntent successfully but do not attach an off-session reusable payment method the way WooCommerce expects, because Link can settle the charge through a wallet-style path rather than a saved card object. WooCommerce sees a subscription with a successful first payment and no usable token, and does the safe thing: it flips the subscription to manual renewal so the store is never left holding an uncharged order.
Nobody configured this. No setting was changed. It is a side effect of how the payment happened, and unless someone is watching the subscriptions list, it looks identical to any other subscription until the missed renewal email starts to arrive.
Why it happens
Stripe's own documentation notes that Link's saved payment details are tied to the Link identity, not always exposed to a merchant as a plain reusable payment method the way a saved card is. WooCommerce Subscriptions, through the WooCommerce Stripe gateway, looks for a specific kind of saved token on the customer before it will attempt an off-session renewal. A few things can go wrong at that boundary:
- The Link checkout completes with a payment method that Stripe treats as single-use for that context, so nothing reusable is left on the Stripe customer record.
- The order was placed as a guest, or the WordPress user was not properly matched to the Stripe customer, so even a valid saved method is not linked to the right account.
- An older version of the WooCommerce Stripe gateway did not fully support saving a Link-originated payment method, a gap that has been reported and patched over several plugin releases.
- The shopper genuinely removed their saved payment method from their Stripe Link wallet after the first payment, which is a real manual case and should not be auto-repaired.
This is a known friction point between one-click wallet checkouts and subscription billing, and it is reported in the WooCommerce Stripe gateway's issue tracker as subscriptions silently losing automatic renewal after specific payment method types. See the citations at the end for the exact references.
"Manual renewal" is not always a customer choice, and it is not always wrong either. The only safe move is to check Stripe directly. If the Stripe customer now has a genuine reusable payment method attached, and it was not there when the subscription was created, the subscription can be switched back. If Stripe has nothing reusable, leave it alone and let the customer pay the invoice or add a card.
The fix, as a flow
We do not touch checkout or the Link flow itself. We add a job that runs once a day, looks at every subscription currently on manual renewal, and asks Stripe whether the customer behind that subscription has a real reusable payment method on file today. If they do, and WooCommerce still thinks the subscription needs manual renewal, we set the subscription's payment method back to the gateway and save the token, the same way a normal card renewal would have.
Build it step by step
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 and subscriptions. Create the WooCommerce key under WooCommerce, Settings, Advanced, REST API. Keep every value in environment variables, never in the file.
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 DRY_RUN="true" # start safe, change to false to write
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 DRY_RUN="true" // start safe, change to false to write
List subscriptions on manual renewal
The WooCommerce Subscriptions REST API extends the same /wp-json/wc/v3/ namespace with a subscriptions endpoint. We page through everything with requires_manual_renewal set, since that flag is exactly what marks a subscription as needing the invoice-and-wait path instead of an automatic charge.
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 manual_subscriptions():
page = 1
while True:
r = requests.get(
f"{WOO_URL}/wp-json/wc/v3/subscriptions",
params={"status": "active", "per_page": 50, "page": page},
auth=AUTH, timeout=30,
)
r.raise_for_status()
batch = r.json()
if not batch:
return
for sub in batch:
if sub.get("requires_manual_renewal"):
yield sub
page += 1
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.status === 404) return null;
if (!res.ok) throw new Error(`Woo ${path} returned ${res.status}`);
return res.json();
}
async function* manualSubscriptions() {
let page = 1;
while (true) {
const batch = await woo(`/subscriptions?status=active&per_page=50&page=${page}`);
if (!batch || !batch.length) return;
for (const sub of batch) {
if (sub.requires_manual_renewal) yield sub;
}
page++;
}
}
Read the PaymentIntent and find the Stripe customer
Each subscription has a parent order with the original checkout on it. We read the saved PaymentIntent id from order meta _stripe_intent_id, falling back to transaction_id when it looks like a PaymentIntent id, then ask Stripe for that intent to get its customer.
import stripe
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 get_intent(intent_id):
if not intent_id:
return None
try:
return stripe.PaymentIntent.retrieve(intent_id)
except stripe.error.InvalidRequestError:
return None
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;
}
async function getIntent(stripe, intentId) {
if (!intentId) return null;
try {
return await stripe.paymentIntents.retrieve(intentId);
} catch {
return null;
}
}
Decide, with one pure function
Keep the decision in its own function that takes the subscription and the Stripe customer's default payment method (or null) and returns an action. A pure function like this is easy to read and easy to test, which we do later. The rule: only repair a subscription that is on manual renewal, whose parent order actually paid through the Stripe gateway, and where Stripe now shows a genuine reusable card or bank type payment method. Everything else is left alone.
REUSABLE_TYPES = {"card", "us_bank_account", "sepa_debit"}
def is_reusable(payment_method):
"""A payment method Stripe will let us charge again off session."""
if not payment_method:
return False
return payment_method.get("type") in REUSABLE_TYPES
def decide(subscription, payment_method):
if not subscription.get("requires_manual_renewal"):
return ("skip", "subscription already automatic")
if subscription.get("payment_method") not in ("stripe", ""):
return ("skip", "not billed through the Stripe gateway")
if not is_reusable(payment_method):
return ("keep_manual", "no reusable payment method on the Stripe customer")
return ("repair", "reusable payment method found, safe to re-enable automatic renewal")
const REUSABLE_TYPES = new Set(["card", "us_bank_account", "sepa_debit"]);
export function isReusable(paymentMethod) {
if (!paymentMethod) return false;
return REUSABLE_TYPES.has(paymentMethod.type);
}
export function decide(subscription, paymentMethod) {
if (!subscription.requires_manual_renewal) return ["skip", "subscription already automatic"];
if (!["stripe", ""].includes(subscription.payment_method)) {
return ["skip", "not billed through the Stripe gateway"];
}
if (!isReusable(paymentMethod)) {
return ["keep_manual", "no reusable payment method on the Stripe customer"];
}
return ["repair", "reusable payment method found, safe to re-enable automatic renewal"];
}
Switch the subscription back to automatic
When the action is repair, update the subscription so requires_manual_renewal is false and the payment method stays stripe, then add an order note on the parent order so the shop manager can see exactly why it changed and which Stripe payment method made it safe.
def re_enable_automatic(subscription_id, parent_order_id, payment_method):
requests.put(
f"{WOO_URL}/wp-json/wc/v3/subscriptions/{subscription_id}",
json={"requires_manual_renewal": False, "payment_method": "stripe"},
auth=AUTH, timeout=30,
).raise_for_status()
requests.post(
f"{WOO_URL}/wp-json/wc/v3/orders/{parent_order_id}/notes",
json={"note": f"Automatic renewal restored. Stripe customer now has a reusable "
f"{payment_method['type']} payment method on file, so the Link "
f"checkout fallback to manual renewal was cleared by the repair job."},
auth=AUTH, timeout=30,
).raise_for_status()
async function reEnableAutomatic(subscriptionId, parentOrderId, paymentMethod) {
await woo(`/subscriptions/${subscriptionId}`, {
method: "PUT",
body: JSON.stringify({ requires_manual_renewal: false, payment_method: "stripe" }),
});
await woo(`/orders/${parentOrderId}/notes`, {
method: "POST",
body: JSON.stringify({
note: `Automatic renewal restored. Stripe customer now has a reusable ` +
`${paymentMethod.type} payment method on file, so the Link checkout ` +
`fallback to manual renewal was cleared by the repair job.`,
}),
});
}
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 do. Read the output, trust it, then switch it off to let it write. Run it once a day with cron, there is no need to check more often than that.
Always start with DRY_RUN=true. A repair job writes to real subscriptions, so you want to see its plan before it acts. Once the report looks right for a few days, turn it off.
The full code
Here is the complete repair job in one file for each language. It reads settings from the environment, logs what it does, respects the dry run flag, and is safe to run again and again because it only ever touches a subscription that is on manual renewal with a genuinely reusable payment method behind it.
View this code on GitHub Full runnable folder with tests in the woocommerce-fixes repo.
"""Restore automatic renewal on WooCommerce Subscriptions that a Stripe Link
checkout left on manual renewal, but only when Stripe now shows a genuine
reusable payment method for that customer. Run on a schedule. 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("restore_automatic_renewal")
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"])
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"
REUSABLE_TYPES = {"card", "us_bank_account", "sepa_debit"}
def manual_subscriptions():
page = 1
while True:
r = requests.get(
f"{WOO_URL}/wp-json/wc/v3/subscriptions",
params={"status": "active", "per_page": 50, "page": page},
auth=AUTH, timeout=30,
)
r.raise_for_status()
batch = r.json()
if not batch:
return
for sub in batch:
if sub.get("requires_manual_renewal"):
yield sub
page += 1
def get_order(order_id):
r = requests.get(f"{WOO_URL}/wp-json/wc/v3/orders/{order_id}", auth=AUTH, timeout=30)
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()
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 get_intent(intent_id):
if not intent_id:
return None
try:
return stripe.PaymentIntent.retrieve(intent_id)
except stripe.error.InvalidRequestError:
return None
def default_reusable_payment_method(customer_id):
"""The Stripe customer's default payment method, if it is a reusable type."""
if not customer_id:
return None
try:
customer = stripe.Customer.retrieve(customer_id)
except stripe.error.InvalidRequestError:
return None
pm_id = (customer.get("invoice_settings") or {}).get("default_payment_method")
if not pm_id:
methods = stripe.PaymentMethod.list(customer=customer_id, limit=1)
if not methods.data:
return None
pm_id = methods.data[0].id
try:
return stripe.PaymentMethod.retrieve(pm_id)
except stripe.error.InvalidRequestError:
return None
def is_reusable(payment_method):
if not payment_method:
return False
return payment_method.get("type") in REUSABLE_TYPES
def decide(subscription, payment_method):
if not subscription.get("requires_manual_renewal"):
return ("skip", "subscription already automatic")
if subscription.get("payment_method") not in ("stripe", ""):
return ("skip", "not billed through the Stripe gateway")
if not is_reusable(payment_method):
return ("keep_manual", "no reusable payment method on the Stripe customer")
return ("repair", "reusable payment method found, safe to re-enable automatic renewal")
def re_enable_automatic(subscription_id, parent_order_id, payment_method):
requests.put(
f"{WOO_URL}/wp-json/wc/v3/subscriptions/{subscription_id}",
json={"requires_manual_renewal": False, "payment_method": "stripe"},
auth=AUTH, timeout=30,
).raise_for_status()
requests.post(
f"{WOO_URL}/wp-json/wc/v3/orders/{parent_order_id}/notes",
json={"note": f"Automatic renewal restored. Stripe customer now has a reusable "
f"{payment_method['type']} payment method on file, so the Link "
f"checkout fallback to manual renewal was cleared by the repair job."},
auth=AUTH, timeout=30,
).raise_for_status()
def run():
repaired = 0
for sub in manual_subscriptions():
parent_order_id = sub.get("parent_id")
order = get_order(parent_order_id) if parent_order_id else None
payment_method = None
if order:
intent = get_intent(intent_id_of(order))
customer_id = intent.get("customer") if intent else None
payment_method = default_reusable_payment_method(customer_id)
action, reason = decide(sub, payment_method)
if action != "repair":
if action == "keep_manual":
log.info("Subscription %s: %s", sub["id"], reason)
continue
log.info("Subscription %s: %s. %s", sub["id"], reason, "would repair" if DRY_RUN else "repairing")
if not DRY_RUN:
re_enable_automatic(sub["id"], parent_order_id, payment_method)
repaired += 1
log.info("Done. %d subscription(s) %s.", repaired, "to repair" if DRY_RUN else "repaired")
if __name__ == "__main__":
run()
/**
* Restore automatic renewal on WooCommerce Subscriptions that a Stripe Link
* checkout left on manual renewal, but only when Stripe now shows a genuine
* reusable payment method for that customer. Run on a schedule. Safe to run
* again and again.
*/
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
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 DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";
const REUSABLE_TYPES = new Set(["card", "us_bank_account", "sepa_debit"]);
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.status === 404) return null;
if (!res.ok) throw new Error(`Woo ${path} returned ${res.status}`);
return res.json();
}
async function* manualSubscriptions() {
let page = 1;
while (true) {
const batch = await woo(`/subscriptions?status=active&per_page=50&page=${page}`);
if (!batch || !batch.length) return;
for (const sub of batch) {
if (sub.requires_manual_renewal) yield sub;
}
page++;
}
}
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;
}
async function getIntent(intentId) {
if (!intentId) return null;
try {
return await stripe.paymentIntents.retrieve(intentId);
} catch {
return null;
}
}
async function defaultReusablePaymentMethod(customerId) {
if (!customerId) return null;
let customer;
try {
customer = await stripe.customers.retrieve(customerId);
} catch {
return null;
}
let pmId = customer.invoice_settings && customer.invoice_settings.default_payment_method;
if (!pmId) {
const methods = await stripe.paymentMethods.list({ customer: customerId, limit: 1 });
if (!methods.data.length) return null;
pmId = methods.data[0].id;
}
try {
return await stripe.paymentMethods.retrieve(pmId);
} catch {
return null;
}
}
export function isReusable(paymentMethod) {
if (!paymentMethod) return false;
return REUSABLE_TYPES.has(paymentMethod.type);
}
export function decide(subscription, paymentMethod) {
if (!subscription.requires_manual_renewal) return ["skip", "subscription already automatic"];
if (!["stripe", ""].includes(subscription.payment_method)) {
return ["skip", "not billed through the Stripe gateway"];
}
if (!isReusable(paymentMethod)) {
return ["keep_manual", "no reusable payment method on the Stripe customer"];
}
return ["repair", "reusable payment method found, safe to re-enable automatic renewal"];
}
async function reEnableAutomatic(subscriptionId, parentOrderId, paymentMethod) {
await woo(`/subscriptions/${subscriptionId}`, {
method: "PUT",
body: JSON.stringify({ requires_manual_renewal: false, payment_method: "stripe" }),
});
await woo(`/orders/${parentOrderId}/notes`, {
method: "POST",
body: JSON.stringify({
note: `Automatic renewal restored. Stripe customer now has a reusable ` +
`${paymentMethod.type} payment method on file, so the Link checkout ` +
`fallback to manual renewal was cleared by the repair job.`,
}),
});
}
async function run() {
let repaired = 0;
for await (const sub of manualSubscriptions()) {
const parentOrderId = sub.parent_id;
const order = parentOrderId ? await woo(`/orders/${parentOrderId}`) : null;
let paymentMethod = null;
if (order) {
const intent = await getIntent(intentIdOf(order));
const customerId = intent && intent.customer;
paymentMethod = await defaultReusablePaymentMethod(customerId);
}
const [action, reason] = decide(sub, paymentMethod);
if (action !== "repair") {
if (action === "keep_manual") console.log(`Subscription ${sub.id}: ${reason}`);
continue;
}
console.log(`Subscription ${sub.id}: ${reason}. ${DRY_RUN ? "would repair" : "repairing"}`);
if (!DRY_RUN) await reEnableAutomatic(sub.id, parentOrderId, paymentMethod);
repaired++;
}
console.log(`Done. ${repaired} subscription(s) ${DRY_RUN ? "to repair" : "repaired"}.`);
}
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 subscriptions get switched back to real billing. Because we kept decide pure, the test needs no network and no Stripe account. It just feeds in plain objects and checks the action.
from restore_automatic_renewal import decide
def pm(**over):
base = {"type": "card"}
base.update(over)
return base
def test_repair_when_manual_and_reusable_method_found():
sub = {"requires_manual_renewal": True, "payment_method": "stripe"}
assert decide(sub, pm())[0] == "repair"
def test_skip_when_already_automatic():
sub = {"requires_manual_renewal": False, "payment_method": "stripe"}
assert decide(sub, pm())[0] == "skip"
def test_skip_when_not_stripe_gateway():
sub = {"requires_manual_renewal": True, "payment_method": "cheque"}
assert decide(sub, pm())[0] == "skip"
def test_keep_manual_when_no_payment_method():
sub = {"requires_manual_renewal": True, "payment_method": "stripe"}
assert decide(sub, None)[0] == "keep_manual"
import { test } from "node:test";
import assert from "node:assert/strict";
import { decide } from "./restore-automatic-renewal.js";
const pm = (over = {}) => ({ type: "card", ...over });
test("repair when manual and reusable method found", () => {
assert.equal(decide({ requires_manual_renewal: true, payment_method: "stripe" }, pm())[0], "repair");
});
test("skip when already automatic", () => {
assert.equal(decide({ requires_manual_renewal: false, payment_method: "stripe" }, pm())[0], "skip");
});
test("skip when not stripe gateway", () => {
assert.equal(decide({ requires_manual_renewal: true, payment_method: "cheque" }, pm())[0], "skip");
});
test("keep manual when no payment method", () => {
assert.equal(decide({ requires_manual_renewal: true, payment_method: "stripe" }, null)[0], "keep_manual");
});
Case studies
The one-click signups that never renewed
A membership site added Stripe Link to speed up signups and saw a jump in completed checkouts. Two months later, renewal revenue was flat despite more members. Every single Link-only signup had quietly landed on manual renewal, and the invoice emails were going straight to spam.
The repair job found forty three subscriptions in that state. Thirty six had a real reusable card sitting on the Stripe customer from a later purchase, and were switched back to automatic in one dry run followed by one real run.
The customer who really did remove their card
One flagged subscription looked identical to the others until the job checked Stripe. The customer had paid once with Link, then deleted their Link wallet entirely. There was nothing reusable to find.
The decision function correctly returned keep_manual, so the job left that subscription untouched and let the normal manual invoice flow keep asking the customer to pay, which is exactly what should happen.
After this runs on a schedule, a Stripe Link checkout stops being a quiet tax on renewal revenue. Subscriptions that can be billed automatically go back to being billed automatically, and the ones that genuinely have no saved payment method stay on manual, which is the correct outcome for them. Keep it running even after enabling any gateway update, because new Link checkouts will keep happening.
FAQ
Why does a subscription paid with Stripe Link switch to manual renewal?
WooCommerce Subscriptions needs a reusable, tokenized payment method attached to the customer to bill later. Some Stripe Link checkouts complete the PaymentIntent without a payment method that WooCommerce recognizes as reusable, so the subscription is created with no saved token and falls back to manual renewal, meaning WooCommerce emails an invoice instead of charging automatically.
Is it safe to switch a subscription back to automatic renewal with a script?
Yes, when the script confirms Stripe actually has a reusable payment method attached to the customer and the subscription is on manual renewal only because the token is missing, not because the customer chose manual on purpose. Start in dry run mode so you can review the exact list before anything changes.
How often should this repair job run?
Once a day is enough. New subscriptions from Link checkouts are the ones at risk, and there is no urgency to fix a manual renewal within minutes, so a daily cron job that reports and then repairs is the right pace.
Related field notes
Citations
On the problem:
- Stripe docs: Link and how its saved payment details relate to reusable payment methods for a customer. docs.stripe.com/payments/link
- WooCommerce Subscriptions docs: renewal payments and why a subscription needs a saved token to bill automatically. woocommerce.com/document/subscriptions/renewal-process
- WooCommerce Stripe gateway issue tracker: subscriptions losing automatic renewal tied to specific payment method types. github.com/woocommerce/woocommerce-gateway-stripe/issues
On the solution:
- Stripe API: retrieve a PaymentIntent and read its
customerfield. docs.stripe.com/api/payment_intents/retrieve - Stripe API: list a customer's payment methods and read the default from invoice settings. docs.stripe.com/api/payment_methods/list
- WooCommerce Subscriptions REST API: read and update a subscription, including
requires_manual_renewal. woocommerce.github.io/subscriptions-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.
Did this fix your stuck renewals?
If this saved you a pile of failed renewals or a support queue full of "why was I not charged," 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