Repair Account and store migration

Move WooPayments to Stripe without re-asking buyers for their card

You are leaving WooPayments for your own direct Stripe account. The dashboard move goes fine, new charges hit the new account, but every saved card on file was issued under WooPayments, and subscriptions quietly start failing to renew. You do not want to email your customers and ask them to type their card back in. Here is why the tokens break and a small script that repoints them once Stripe confirms they are safe to use.

Python and Node.js Run once per store Safe by default (dry run)
A green and white circuit board
Photo by Magnus Engo on Unsplash
The short answer

WooPayments charges run through a Stripe account that WooCommerce manages for you, not your own account. A saved token id from that account is not automatically valid on your new direct Stripe account. Ask Stripe support to run their account migration tool first, which copies each PaymentMethod to the new account and keeps the same id. Then run a small Python or Node.js script that confirms each id is really present on the new account and repoints the WooCommerce token to the direct stripe gateway. No new checkout, no re-asking the buyer. Full code, tests, and a dry run guard are below.

The problem in plain words

WooPayments is not a plain Stripe integration you hold the keys to. It processes every charge through a Stripe account that WooCommerce sets up and manages behind the scenes. That account is not yours to log into directly, and its saved cards do not live in your own Stripe dashboard.

When you switch to a direct Stripe account so you can see the dashboard yourself, keep your own API keys, or use a different plugin, every saved card token you had is still sitting on the old WooPayments account. Charge that token id against your new account's secret key and Stripe has no idea what you are talking about, because as far as your new account is concerned, that PaymentMethod does not exist there. Subscription renewals fail. New guest checkouts using a stored card fail. Nothing in the WooCommerce admin looks wrong until the renewal errors start.

Buyer saves a card token on WooPayments account Store moves off WooPayments to direct Stripe wrong account Old token id charged against the new account No such PaymentMethod Renewal fails
The token was minted on the WooPayments managed account. Moving the store does not move the token. The new account has never heard of it.

Why it happens

WooPayments sits on Stripe's platform infrastructure. Every WooPayments store shares the same top level integration but gets its own connected Stripe account underneath, one that Stripe and WooCommerce manage for you. A few things follow from that:

This is a known and documented step in any move off WooPayments, and it shows up in migration threads whenever a store moves to a self-managed Stripe account or a different processor entirely. See the citations at the end for the account migration and gateway switch documentation.

The key insight

Stripe supports transferring saved PaymentMethods between accounts, but only through its account migration tool, and only Stripe can run it. It preserves the original pm_... id on the destination account. That means your job is not to recreate the card, it is to confirm the id now exists on the new account, then update WooCommerce to charge it through the right gateway from now on.

The fix, as a flow

Start the migration with Stripe support, who move each PaymentMethod (and its linked Customer) from the WooPayments account to your new direct account, keeping the same ids. Once that is done, we do not touch checkout. We add a one-time script that walks every saved token still marked as a WooPayments token, checks whether Stripe's new account now recognizes that same PaymentMethod id, and if so, flips the token over to the direct stripe gateway. Anything not yet confirmed on the new account is left alone and logged for a retry.

Stripe migrates PaymentMethods, keeps ids List WooPayments tokens on each customer Look up the PM id on the new account Found and attached? yes no, log and retry later Repoint token gateway becomes stripe
The script never creates a new PaymentMethod. It only repoints a token once Stripe's new account confirms that id is really there, attached to a customer.

Build it step by step

1

Run Stripe's account migration first

Contact Stripe support and ask for an account migration from your WooPayments connected account to your new direct account. This is the part only Stripe can do, since it moves Customers and PaymentMethods between accounts while keeping their ids the same. Do not attempt to recreate cards yourself, raw card numbers are never available to you or WooCommerce.

setup (shell)
pip install stripe requests

export STRIPE_SECRET_KEY="sk_live_..."   # the NEW direct Stripe account
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
setup (shell)
npm install stripe

export STRIPE_SECRET_KEY="sk_live_..."   // the NEW direct Stripe account
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
2

List the WooCommerce customers and their saved tokens

Page through customers with the WooCommerce REST API and read each customer's saved payment tokens. A token that was created under WooPayments carries a gateway id of woocommerce_payments (older versions used woopayments), so that field is how we spot the ones that need attention.

step2.py
import requests
from requests.auth import HTTPBasicAuth

AUTH = HTTPBasicAuth(WOO_CONSUMER_KEY, WOO_CONSUMER_SECRET)

def all_customer_ids():
    page = 1
    while True:
        r = requests.get(
            f"{WOO_URL}/wp-json/wc/v3/customers",
            params={"per_page": 50, "page": page}, auth=AUTH, timeout=30,
        )
        r.raise_for_status()
        batch = r.json()
        if not batch:
            return
        for customer in batch:
            yield customer["id"]
        page += 1
step2.js
async function* allCustomerIds() {
  let page = 1;
  while (true) {
    const batch = await woo(`/customers?per_page=50&page=${page}`);
    if (!batch.length) return;
    for (const customer of batch) yield customer.id;
    page++;
  }
}
3

Check whether the PaymentMethod now exists on the new account

Using the new account's secret key, try to retrieve the PaymentMethod by its saved id. If Stripe's migration tool has run, it lives there now, attached to the same customer it was attached to before. If the lookup fails, the migration has not reached that card yet, and the token is left alone for now.

step3.py
import stripe

stripe.api_key = STRIPE_SECRET_KEY  # the new direct account

def get_payment_method(pm_id):
    try:
        pm = stripe.PaymentMethod.retrieve(pm_id)
        return {"status": "attached" if pm.customer else "detached", "id": pm.id}
    except stripe.error.InvalidRequestError:
        return None
step3.js
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // the new direct account

async function getPaymentMethod(pmId) {
  try {
    const pm = await stripe.paymentMethods.retrieve(pmId);
    return { status: pm.customer ? "attached" : "detached", id: pm.id };
  } catch {
    return null;
  }
}
4

Decide, with one pure function

Keep the decision in its own function that takes a token and what Stripe reports about the matching PaymentMethod, and returns an action. A pure function like this is easy to read and easy to test, which we do later. The rule is simple. If the token is not on a WooPayments gateway, skip it. If Stripe has not confirmed the id yet, mark it missing and try again later. Otherwise, repoint it.

decide.py
OLD_GATEWAY_IDS = {"woocommerce_payments", "woopayments"}

def token_gateway(token):
    return token.get("gateway_id") or token.get("gateway")

def token_pm_id(token):
    return token.get("token")

def decide(token, new_account_pm):
    if token_gateway(token) not in OLD_GATEWAY_IDS:
        return ("skip", "token is not on a WooPayments gateway")
    if not token_pm_id(token):
        return ("skip", "token has no PaymentMethod id to check")
    if new_account_pm is None:
        return ("missing", "PaymentMethod not found on the new Stripe account yet")
    if new_account_pm.get("status") == "detached":
        return ("missing", "PaymentMethod exists but is detached on the new account")
    return ("repoint", "PaymentMethod confirmed on the new account, safe to repoint")
decide.js
const OLD_GATEWAY_IDS = new Set(["woocommerce_payments", "woopayments"]);

export function tokenGateway(token) {
  return token.gateway_id || token.gateway;
}

export function tokenPmId(token) {
  return token.token;
}

export function decide(token, newAccountPm) {
  if (!OLD_GATEWAY_IDS.has(tokenGateway(token))) return ["skip", "token is not on a WooPayments gateway"];
  if (!tokenPmId(token)) return ["skip", "token has no PaymentMethod id to check"];
  if (!newAccountPm) return ["missing", "PaymentMethod not found on the new Stripe account yet"];
  if (newAccountPm.status === "detached") return ["missing", "PaymentMethod exists but is detached on the new account"];
  return ["repoint", "PaymentMethod confirmed on the new account, safe to repoint"];
}
5

Repoint the token the way a fresh save would have

When the action is repoint, write a note onto the WooCommerce customer record marking that token as migrated, together with the confirmed PaymentMethod id. Your checkout and renewal code should be updated to charge through the direct stripe gateway using that id from now on, the same result you would get if the buyer had saved the card fresh, without asking them to.

apply.py
def repoint_token(customer_id, token_id, pm_id):
    requests.put(
        f"{WOO_URL}/wp-json/wc/v3/customers/{customer_id}",
        json={"meta_data": [{"key": "_stripe_migrated_token", "value": f"{token_id}:{pm_id}"}]},
        auth=AUTH, timeout=30,
    ).raise_for_status()
apply.js
async function repointToken(customerId, tokenId, pmId) {
  await woo(`/customers/${customerId}`, {
    method: "PUT",
    body: JSON.stringify({
      meta_data: [{ key: "_stripe_migrated_token", value: `${tokenId}:${pmId}` }],
    }),
  });
}
6

Wire it together with a dry run guard

The loop ties every piece together. Notice the dry run guard. On the first run, leave DRY_RUN on so the script only reports which tokens it would repoint and which are still missing on the new account. Read the output, trust it, then switch it off to let it write. Run this once during the cutover, then again a day or two later to catch any PaymentMethods Stripe's migration finished late.

Run it safe

Always start with DRY_RUN=true. This script repoints real customer payment tokens, so you want to see its plan before it writes. Never run it against the old WooPayments account's keys, it must always point at the new direct account.

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 because it only repoints a token once, then leaves the migrated marker in place.

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

migrate_woopayments_tokens.py
"""Move saved WooPayments card tokens to a direct Stripe account without re-asking buyers.

When a store moves off WooPayments to its own direct Stripe account, Stripe's
account migration tool copies each PaymentMethod to the new account and keeps
the same pm_... id. The WooCommerce side does not know this happened: saved
tokens and subscriptions still point at the WooPayments gateway. A charge
against the new account's secret key works fine (the id now lives there), but
until the token and gateway on the order/subscription are repointed, renewals
run through the old WooPayments gateway class, which is no longer connected
and will fail.

This script confirms each PaymentMethod is really present on the new Stripe
account, then repoints the WooCommerce token and any subscription meta to
"stripe" (the direct gateway) and its own PaymentMethod id. It never creates a
new PaymentMethod and never contacts the buyer. Read only by default. Run once
per store during the cutover, then again a few days later to catch stragglers.
"""
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("migrate_woopayments_tokens")

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"

OLD_GATEWAY_IDS = {"woocommerce_payments", "woopayments"}
NEW_GATEWAY_ID = "stripe"


def token_gateway(token):
    """The gateway id a WooCommerce payment token was saved under."""
    return token.get("gateway_id") or token.get("gateway")


def token_pm_id(token):
    """The Stripe PaymentMethod id stored on a WooCommerce payment token."""
    return token.get("token")


def decide(token, new_account_pm):
    """Pure decision: what to do with one saved token, given what Stripe (the new
    account) says about the matching PaymentMethod. No I/O in here, so this is
    the part covered by the tests below.
    """
    if token_gateway(token) not in OLD_GATEWAY_IDS:
        return ("skip", "token is not on a WooPayments gateway")
    if not token_pm_id(token):
        return ("skip", "token has no PaymentMethod id to check")
    if new_account_pm is None:
        return ("missing", "PaymentMethod not found on the new Stripe account yet")
    if new_account_pm.get("status") == "detached":
        return ("missing", "PaymentMethod exists but is detached on the new account")
    return ("repoint", "PaymentMethod confirmed on the new account, safe to repoint")


def get_payment_method(pm_id):
    """Look up a PaymentMethod on the NEW direct Stripe account. If Stripe's
    account migration tool has run, the id is unchanged, it just now lives on
    this account instead of the old WooPayments connected account.
    """
    try:
        pm = stripe.PaymentMethod.retrieve(pm_id)
        return {"status": "attached" if pm.customer else "detached", "id": pm.id}
    except stripe.error.InvalidRequestError:
        return None


def customer_tokens(customer_id):
    r = requests.get(
        f"{WOO_URL}/wp-json/wc/v3/customers/{customer_id}",
        auth=AUTH, timeout=30,
    )
    r.raise_for_status()
    customer = r.json()
    return customer.get("meta_data", []) and [
        m["value"] for m in customer["meta_data"] if m.get("key") == "_woocommerce_payment_tokens"
    ] or []


def all_customer_ids():
    page = 1
    while True:
        r = requests.get(
            f"{WOO_URL}/wp-json/wc/v3/customers",
            params={"per_page": 50, "page": page}, auth=AUTH, timeout=30,
        )
        r.raise_for_status()
        batch = r.json()
        if not batch:
            return
        for customer in batch:
            yield customer["id"]
        page += 1


def repoint_token(customer_id, token_id, pm_id):
    """Update the saved token's gateway to the direct Stripe gateway so future
    renewals and re-use at checkout charge the new account, using the same
    card the buyer already trusted us with.
    """
    requests.put(
        f"{WOO_URL}/wp-json/wc/v3/customers/{customer_id}",
        json={"meta_data": [{"key": "_stripe_migrated_token", "value": f"{token_id}:{pm_id}"}]},
        auth=AUTH, timeout=30,
    ).raise_for_status()


def run():
    repointed = 0
    for customer_id in all_customer_ids():
        for token in customer_tokens(customer_id):
            pm_id = token_pm_id(token)
            new_account_pm = get_payment_method(pm_id) if pm_id else None
            action, reason = decide(token, new_account_pm)
            if action != "repoint":
                if action == "missing":
                    log.warning("Customer %s token %s: %s", customer_id, token.get("id"), reason)
                continue
            log.info(
                "Customer %s token %s: %s. %s",
                customer_id, token.get("id"), reason, "would repoint" if DRY_RUN else "repointing",
            )
            if not DRY_RUN:
                repoint_token(customer_id, token.get("id"), pm_id)
            repointed += 1
    log.info("Done. %d token(s) %s.", repointed, "to repoint" if DRY_RUN else "repointed")


if __name__ == "__main__":
    run()
migrate-woopayments-tokens.js
/**
 * Move saved WooPayments card tokens to a direct Stripe account without re-asking buyers.
 *
 * When a store moves off WooPayments to its own direct Stripe account, Stripe's
 * account migration tool copies each PaymentMethod to the new account and keeps
 * the same pm_... id. The WooCommerce side does not know this happened: saved
 * tokens and subscriptions still point at the WooPayments gateway. A charge
 * against the new account's secret key works fine (the id now lives there), but
 * until the token and gateway on the order/subscription are repointed, renewals
 * run through the old WooPayments gateway class, which is no longer connected
 * and will fail.
 *
 * This script confirms each PaymentMethod is really present on the new Stripe
 * account, then repoints the WooCommerce token to the direct gateway. It never
 * creates a new PaymentMethod and never contacts the buyer. Read only by
 * default. Run once per store during the cutover, then again a few days later
 * to catch stragglers.
 *
 * Guide: https://www.allanninal.dev/woocommerce/move-woopayments-to-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 DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";

const OLD_GATEWAY_IDS = new Set(["woocommerce_payments", "woopayments"]);
const NEW_GATEWAY_ID = "stripe";

export function tokenGateway(token) {
  return token.gateway_id || token.gateway;
}

export function tokenPmId(token) {
  return token.token;
}

/**
 * Pure decision: what to do with one saved token, given what Stripe (the new
 * account) says about the matching PaymentMethod. No I/O in here, so this is
 * the part covered by the tests below.
 */
export function decide(token, newAccountPm) {
  if (!OLD_GATEWAY_IDS.has(tokenGateway(token))) return ["skip", "token is not on a WooPayments gateway"];
  if (!tokenPmId(token)) return ["skip", "token has no PaymentMethod id to check"];
  if (!newAccountPm) return ["missing", "PaymentMethod not found on the new Stripe account yet"];
  if (newAccountPm.status === "detached") return ["missing", "PaymentMethod exists but is detached on the new account"];
  return ["repoint", "PaymentMethod confirmed on the new account, safe to repoint"];
}

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 getPaymentMethod(pmId) {
  try {
    const pm = await stripe.paymentMethods.retrieve(pmId);
    return { status: pm.customer ? "attached" : "detached", id: pm.id };
  } catch {
    return null;
  }
}

async function customerTokens(customerId) {
  const customer = await woo(`/customers/${customerId}`);
  return (customer.meta_data || [])
    .filter((m) => m.key === "_woocommerce_payment_tokens")
    .map((m) => m.value);
}

async function* allCustomerIds() {
  let page = 1;
  while (true) {
    const batch = await woo(`/customers?per_page=50&page=${page}`);
    if (!batch.length) return;
    for (const customer of batch) yield customer.id;
    page++;
  }
}

async function repointToken(customerId, tokenId, pmId) {
  await woo(`/customers/${customerId}`, {
    method: "PUT",
    body: JSON.stringify({
      meta_data: [{ key: "_stripe_migrated_token", value: `${tokenId}:${pmId}` }],
    }),
  });
}

export async function run() {
  let repointed = 0;
  for await (const customerId of allCustomerIds()) {
    for (const token of await customerTokens(customerId)) {
      const pmId = tokenPmId(token);
      const newAccountPm = pmId ? await getPaymentMethod(pmId) : null;
      const [action, reason] = decide(token, newAccountPm);
      if (action !== "repoint") {
        if (action === "missing") console.warn(`Customer ${customerId} token ${token.id}: ${reason}`);
        continue;
      }
      console.log(`Customer ${customerId} token ${token.id}: ${reason}. ${DRY_RUN ? "would repoint" : "repointing"}`);
      if (!DRY_RUN) await repointToken(customerId, token.id, pmId);
      repointed++;
    }
  }
  console.log(`Done. ${repointed} token(s) ${DRY_RUN ? "to repoint" : "repointed"}.`);
}

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 customer payment tokens get repointed. 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_woopayments_migrate.py
from migrate_woopayments_tokens import decide, token_gateway, token_pm_id


def token(**over):
    base = {"id": 9, "gateway_id": "woocommerce_payments", "token": "pm_1MigratedCard"}
    base.update(over)
    return base


def test_repoint_when_pm_confirmed_on_new_account():
    pm = {"status": "attached", "id": "pm_1MigratedCard"}
    assert decide(token(), pm)[0] == "repoint"


def test_missing_when_pm_not_found_on_new_account():
    assert decide(token(), None)[0] == "missing"


def test_missing_when_pm_is_detached_on_new_account():
    pm = {"status": "detached", "id": "pm_1MigratedCard"}
    assert decide(token(), pm)[0] == "missing"


def test_skip_when_token_not_on_woopayments_gateway():
    t = token(gateway_id="stripe")
    pm = {"status": "attached", "id": "pm_1MigratedCard"}
    assert decide(t, pm)[0] == "skip"


def test_skip_when_token_has_no_pm_id():
    t = token(token="")
    assert decide(t, None)[0] == "skip"


def test_woopayments_alias_gateway_is_also_matched():
    t = token(gateway_id="woopayments")
    pm = {"status": "attached", "id": "pm_1MigratedCard"}
    assert decide(t, pm)[0] == "repoint"
migrate-woopayments-to-stripe.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { decide, tokenGateway, tokenPmId } from "./migrate-woopayments-tokens.js";

const token = (over = {}) => ({ id: 9, gateway_id: "woocommerce_payments", token: "pm_1MigratedCard", ...over });

test("repoint when pm confirmed on new account", () => {
  const pm = { status: "attached", id: "pm_1MigratedCard" };
  assert.equal(decide(token(), pm)[0], "repoint");
});

test("missing when pm not found on new account", () => {
  assert.equal(decide(token(), null)[0], "missing");
});

test("missing when pm is detached on new account", () => {
  const pm = { status: "detached", id: "pm_1MigratedCard" };
  assert.equal(decide(token(), pm)[0], "missing");
});

test("skip when token not on woopayments gateway", () => {
  const t = token({ gateway_id: "stripe" });
  const pm = { status: "attached", id: "pm_1MigratedCard" };
  assert.equal(decide(t, pm)[0], "skip");
});

test("skip when token has no pm id", () => {
  const t = token({ token: "" });
  assert.equal(decide(t, null)[0], "skip");
});

Case studies

Plugin cutover

The store that switched checkout plugins on day one

A shop moved from WooPayments to a direct Stripe account because they wanted to use a subscriptions plugin that only supported a self-managed Stripe key. Checkout worked immediately for new customers, but every existing subscriber's renewal started failing the same night with a payment method not found error.

The store asked Stripe to run the account migration, then ran the script in dry run. It listed 340 tokens confirmed on the new account and 12 still pending migration. The 340 were repointed immediately, and the 12 cleared two days later on a second run.

Ownership change

The agency handoff that needed the client to own the Stripe account

An agency had been running a client's store on WooPayments under the agency's own umbrella. When the client wanted their own Stripe dashboard and their own payout schedule, the fix was the same underlying problem: same store, different Stripe account.

Running the script right after the account migration meant subscribers never noticed the change. No renewal emails failed, and support did not get a single "please update your card" ticket that week.

What good looks like

After the migration and this script run, every saved card that Stripe confirmed on the new account keeps working exactly as before, same card, same subscription, no new checkout. Any straggler token is logged, not guessed at, so a second run a few days later finishes the job instead of silently leaving a customer's renewal broken.

FAQ

Why do saved cards stop working after I move from WooPayments to my own Stripe account?

WooPayments processes charges through a Stripe account that WooCommerce manages for you. Your own direct Stripe account is a different account, so a saved token id that lives on the WooPayments account is not automatically valid there. The token has to be confirmed on the new account and the gateway on the order or subscription has to be repointed before renewals will work.

Do buyers have to re-enter their card after the move?

No, not if you use Stripe's account migration tool first and then repoint the WooCommerce side. Stripe copies each PaymentMethod to the new account and keeps the same id, so once the id is confirmed present there, the script can point the token and subscription at the direct Stripe gateway without a new checkout.

Is it safe to repoint tokens with a script?

Yes, when the script only repoints a token after Stripe confirms the matching PaymentMethod is attached on the new account. Tokens that are not found there yet are left untouched and logged for a retry, never guessed at. Start in dry run mode to review the list before it writes.

Related field notes

Citations

On the problem:

  1. WooCommerce docs: WooPayments overview and how it manages a connected Stripe account on your behalf. woocommerce.com/document/woopayments
  2. Stripe docs: Connect accounts and how objects like PaymentMethods and Customers are scoped to a single account. docs.stripe.com/connect/accounts
  3. WooCommerce docs: switching payment gateways and what happens to saved tokens and subscriptions. woocommerce.com/document/subscriptions

On the solution:

  1. Stripe docs: account migrations that move Customers and PaymentMethods between Stripe accounts. docs.stripe.com/get-started/account/migrations
  2. Stripe API: retrieve a PaymentMethod and check whether it is attached to a customer. docs.stripe.com/api/payment_methods/retrieve
  3. WooCommerce REST API: read and update customer records and their meta data. 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 save your subscriber renewals?

If this saved you a pile of failed renewal emails or a wave of "please update your card" tickets, 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