Diagnostic Stripe
cardholder requirements.past_due keeps every card inactive
The card was issued in the morning, handed to an employee at lunch, and declined at a card reader by two. The dashboard shows the card as inactive. Activating it does nothing. Nothing about the card object explains why, because the reason is not on the card — it is a list of two missing strings on the cardholder the card belongs to, one of which is an IP address.
Read the cardholder, not the card. GET /v1/issuing/cardholders?limit=100 and flag any with requirements.past_due non-empty or requirements.disabled_reason set. While those are outstanding, Stripe blocks activation, so every card attached to that cardholder stays inactive and every authorization declines.
Sort the missing fields before you write the ticket. A past_due list containing only individual.card_issuing.user_terms_acceptance.ip and .date is not a verification problem at all — it is a terms acceptance that was never captured, and the fix is a checkbox plus the IP and timestamp from the moment they accepted. Confirm the impact with GET /v1/issuing/authorizations and read request_history[].reason, which is where card_inactive actually appears.
The problem in plain words
The confusion is structural: the object that fails is not the object that is wrong. Someone is holding a card that does not work, so they look at the card, and the card says inactive with no explanation attached. Setting it to active either errors or silently stays inactive. Three objects are involved — the authorization that declined, the card that is inactive, and the cardholder that is blocking it — and only the third one carries the reason.
The declines make it worse by being instant and total. There is no partial degradation, no retry that succeeds, no pattern to narrow down. Every transaction on every card belonging to that cardholder fails at the terminal, which reads like a broken card program rather than a missing form field, and gets escalated accordingly.
Why it happens
Cards default to inactive, so nothing looks unusual at first. A newly issued card is inactive by design and is meant to be activated by a call. The failure is that the activation does not take, and there is no error surfaced on the card object explaining that a different object is holding it.
The commonest missing fields are not identity documents. individual.card_issuing.user_terms_acceptance.ip and .date record that the cardholder accepted the Authorized User Terms, and they have to be captured at the moment of acceptance. An integration that collects a name, a date of birth and an address, but never presents the terms, produces cardholders that look fully verified and are still blocked.
The decline reason is in a nested array. An authorization carries approved: false, and the actual reason — card_inactive, cardholder_inactive, verification_failed, insufficient_funds, spending_controls, webhook_timeout — lives in request_history[].reason. Anything reading only the top-level fields sees that it declined and not why, and those six reasons have six unrelated repairs.
A clean cardholder with inactive cards is a different bug. If nothing is past due and the cards are still inactive, nobody ever called activation. That is a gap in your issuing flow, not a Stripe block, and it needs saying separately or the two get conflated forever.
The fix, as a flow
The script starts at the cardholder rather than the card, counts the inactive cards behind each one, and tallies the decline reasons, because those three facts together say whether the block is what you think it is.
How to fix it
Start from the cardholder list, not from the complaint
GET /v1/issuing/cardholders?limit=100, reading status, requirements.past_due and requirements.disabled_reason. Doing this as a sweep rather than per-complaint matters, because cardholders created by the same onboarding flow share the same missing field, and finding one usually means finding all of them.
Separate a terms gap from an identity gap
If every entry in past_due sits under individual.card_issuing.user_terms_acceptance, no document is missing and nothing needs verifying: the cardholder was never shown the Authorized User Terms. That is a UI change and a stored IP and timestamp, which is a completely different piece of work from collecting a date of birth or an address.
Correlate the inactive cards
GET /v1/issuing/cards?status=inactive&limit=100, grouped by cardholder.id. One blocked cardholder with eleven cards behind it is a bigger number than eleven separate tickets suggests, and the count is what gets the fix prioritised.
Read request_history on the declines
GET /v1/issuing/authorizations?limit=100, keep approved: false, and tally request_history[].reason. If the reasons are card_inactive you have confirmed the chain end to end. If they are insufficient_funds or spending_controls, the cardholder requirements are a real finding but they are not what declined this transaction, and fixing them will not stop the declines.
Collect the fields, then activate the cards
Update the cardholder with everything in past_due, including the terms acceptance IP and date captured at acceptance time, then set each card to active. Activation before the requirements clear does not stick, so the order is not optional.
How to check it worked
Re-run the script. No cardholder should have past-due requirements, and any card still inactive should be one nobody has issued to a person yet.
python3 stripe_issuing_cardholder_requirements.py
# 24 cardholder(s), 31 inactive card(s): 0 blocked, 0 dormant
The full code
Three GETs: cardholders, inactive cards grouped by cardholder, and recent authorizations for the decline reasons. Two pure functions carry the judgement — one that sorts a cardholder by what is actually missing, and one that turns a decline reason into the repair it implies — because a terms checkbox and a passport scan are the same shape of finding and completely different work.
"""Report Issuing cardholders whose requirements keep their cards inactive.
Read only. Three GET requests and no writes: give this a RESTRICTED key with
read access to Issuing cardholders, Issuing cards and Issuing authorizations.
The repair is printed, never performed, because this script holds a credential
to a live payments account.
"""
import argparse
import logging
import os
import sys
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("stripe_issuing_cardholder_requirements")
API = "https://api.stripe.com/v1"
# Fields under this prefix record that the cardholder accepted the Authorized
# User Terms. They are past due far more often than any identity field, and they
# are not a verification problem: nothing needs checking, the terms were never
# shown.
TERMS_PREFIX = "individual.card_issuing.user_terms_acceptance"
# What each decline reason actually implies. Six reasons, six unrelated repairs,
# and all six arrive as approved: false with nothing else to tell them apart.
DECLINE_HINTS = {
"card_inactive":
"the card itself is not active. Activation is blocked while the cardholder "
"has past-due requirements, so check the cardholder first.",
"cardholder_inactive":
"the cardholder is not active. Its own status, not the card's, is the block.",
"verification_failed":
"the cardholder's identity verification did not pass. Collecting the same "
"details again will not change it; read the requirements for what failed.",
"insufficient_funds":
"the Issuing balance is empty, which has nothing to do with requirements. "
"Read balance.issuing.available and top it up.",
"spending_controls":
"a spending control on the card or cardholder rejected this. The card is "
"working exactly as configured.",
"webhook_timeout":
"your real-time authorization endpoint did not answer in time, so Stripe "
"applied the default. This is your latency, not a cardholder problem.",
}
def explain_decline(reason):
"""Turn an authorization decline reason into its repair. Pure.
Unknown reasons come back named rather than swallowed: the enum grows, and a
decline reported as "unknown reason" is still more useful than one silently
dropped from the tally.
"""
if reason in DECLINE_HINTS:
return DECLINE_HINTS[reason]
return "unrecognised reason %r: read the authorization's request_history" % (reason,)
def verdict(cardholder, inactive_cards):
"""Classify one cardholder. Pure. Returns (state, detail).
`inactive_cards` is how many of its cards are sitting inactive. The states
separate three different jobs: capture a terms acceptance, collect identity
fields, or go and find out why your own code never called activation.
"""
reqs = cardholder.get("requirements") or {}
past_due = [f for f in (reqs.get("past_due") or []) if f]
reason = reqs.get("disabled_reason")
cards = " (%d inactive card(s) behind it)" % inactive_cards if inactive_cards else ""
if past_due:
if all(f.startswith(TERMS_PREFIX) for f in past_due):
return ("blocked-terms",
"past_due is only terms acceptance: %s%s. Nothing needs "
"verifying. Capture the IP and the timestamp at the moment the "
"cardholder accepts the Authorized User Terms."
% (", ".join(past_due), cards))
return ("blocked-identity",
"%d field(s) past due: %s%s. Activation stays blocked until every "
"one is supplied." % (len(past_due), ", ".join(past_due[:4]), cards))
if reason:
return ("disabled",
"disabled_reason %s with nothing in past_due%s: read the "
"requirements hash before collecting anything" % (reason, cards))
if cardholder.get("status") != "active":
return ("inactive-cardholder",
"status %r with no outstanding requirements%s: this was set "
"deliberately, so find out by whom"
% (cardholder.get("status"), cards))
if inactive_cards:
return ("dormant",
"cardholder is clean and %d card(s) are still inactive: nothing is "
"blocking activation, so nobody ever called it" % inactive_cards)
return ("healthy", "active, nothing past due")
def get(session, path, **params):
r = session.get(API + path, params=params, timeout=30)
if r.status_code == 401:
raise SystemExit("401 from Stripe: the key is wrong, or is for the other mode")
r.raise_for_status()
return r.json()
def paginate(session, path, cap, **params):
"""Yield every object from a list endpoint, up to `cap`."""
seen = 0
params = dict(params, limit=100)
while True:
page = get(session, path, **params)
data = page.get("data", [])
for item in data:
yield item
seen += 1
if seen >= cap:
return
if not data or not page.get("has_more"):
return
params["starting_after"] = data[-1]["id"]
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--max-authorizations", type=int, default=1000,
help="how many recent authorizations to read for decline reasons")
args = ap.parse_args()
key = os.environ.get("STRIPE_API_KEY")
if not key:
log.error("set STRIPE_API_KEY (use a restricted, read-only key)")
return 2
s = requests.Session()
s.headers.update({"Authorization": "Bearer " + key})
inactive = {}
total_inactive = 0
for card in paginate(s, "/issuing/cards", 5000, status="inactive"):
total_inactive += 1
holder = (card.get("cardholder") or {}).get("id")
if holder:
inactive[holder] = inactive.get(holder, 0) + 1
counts = {}
cardholders = 0
for holder in paginate(s, "/issuing/cardholders", 5000):
cardholders += 1
state, detail = verdict(holder, inactive.get(holder.get("id"), 0))
counts[state] = counts.get(state, 0) + 1
if state == "healthy":
continue
log.warning("%s %-18s %s", holder.get("id", "ich_?"), state, detail)
reasons = {}
for auth in paginate(s, "/issuing/authorizations", args.max_authorizations):
if auth.get("approved"):
continue
for attempt in auth.get("request_history") or []:
reason = attempt.get("reason")
reasons[reason] = reasons.get(reason, 0) + 1
blocked = counts.get("blocked-terms", 0) + counts.get("blocked-identity", 0)
log.info("%d cardholder(s), %d inactive card(s): %d blocked, %d dormant",
cardholders, total_inactive, blocked, counts.get("dormant", 0))
for reason, count in sorted(reasons.items(), key=lambda kv: -kv[1]):
log.warning(" %d decline(s) with reason %s: %s",
count, reason, explain_decline(reason))
if counts.get("blocked-terms"):
log.warning(" repair: POST %s/issuing/cardholders/{ich_id} with", API)
log.warning(" individual[card_issuing][user_terms_acceptance][date] and [ip], "
"captured when the cardholder accepted the terms")
if counts.get("blocked-identity"):
log.warning(" repair: POST %s/issuing/cardholders/{ich_id} supplying every "
"field listed in requirements.past_due", API)
if blocked or counts.get("dormant"):
log.warning(" then: POST %s/issuing/cards/{ic_id} with status=active. "
"Activation before the requirements clear does not stick.", API)
return 1 if (blocked or counts.get("dormant") or counts.get("disabled")
or counts.get("inactive-cardholder")) else 0
if __name__ == "__main__":
sys.exit(main())
/**
* Report Issuing cardholders whose requirements keep their cards inactive.
*
* Read only. Three GET requests and no writes: give this a RESTRICTED key with
* read access to Issuing cardholders, Issuing cards and Issuing authorizations.
* The repair is printed, never performed.
*/
const API = 'https://api.stripe.com/v1';
// Fields under this prefix record that the cardholder accepted the Authorized
// User Terms. They are past due far more often than any identity field, and they
// are not a verification problem: nothing needs checking, the terms were never
// shown.
const TERMS_PREFIX = 'individual.card_issuing.user_terms_acceptance';
// What each decline reason actually implies. Six reasons, six unrelated repairs.
const DECLINE_HINTS = {
card_inactive:
'the card itself is not active. Activation is blocked while the cardholder ' +
'has past-due requirements, so check the cardholder first.',
cardholder_inactive:
"the cardholder is not active. Its own status, not the card's, is the block.",
verification_failed:
"the cardholder's identity verification did not pass. Collecting the same " +
'details again will not change it; read the requirements for what failed.',
insufficient_funds:
'the Issuing balance is empty, which has nothing to do with requirements. ' +
'Read balance.issuing.available and top it up.',
spending_controls:
'a spending control on the card or cardholder rejected this. The card is ' +
'working exactly as configured.',
webhook_timeout:
'your real-time authorization endpoint did not answer in time, so Stripe ' +
'applied the default. This is your latency, not a cardholder problem.',
};
/** Turn an authorization decline reason into its repair. Pure. */
export function explainDecline(reason) {
if (Object.prototype.hasOwnProperty.call(DECLINE_HINTS, reason)) {
return DECLINE_HINTS[reason];
}
return `unrecognised reason ${JSON.stringify(reason)}: read the authorization's ` +
'request_history';
}
/**
* Classify one cardholder. Pure. Returns [state, detail].
* The states separate three different jobs: capture a terms acceptance, collect
* identity fields, or find out why your own code never called activation.
*/
export function verdict(cardholder, inactiveCards) {
const reqs = cardholder.requirements ?? {};
const pastDue = (reqs.past_due ?? []).filter(Boolean);
const reason = reqs.disabled_reason ?? null;
const cards = inactiveCards ? ` (${inactiveCards} inactive card(s) behind it)` : '';
if (pastDue.length) {
if (pastDue.every((f) => f.startsWith(TERMS_PREFIX))) {
return ['blocked-terms',
`past_due is only terms acceptance: ${pastDue.join(', ')}${cards}. Nothing ` +
'needs verifying. Capture the IP and the timestamp at the moment the ' +
'cardholder accepts the Authorized User Terms.'];
}
return ['blocked-identity',
`${pastDue.length} field(s) past due: ${pastDue.slice(0, 4).join(', ')}${cards}. ` +
'Activation stays blocked until every one is supplied.'];
}
if (reason) {
return ['disabled',
`disabled_reason ${reason} with nothing in past_due${cards}: read the ` +
'requirements hash before collecting anything'];
}
if (cardholder.status !== 'active') {
return ['inactive-cardholder',
`status ${JSON.stringify(cardholder.status)} with no outstanding ` +
`requirements${cards}: this was set deliberately, so find out by whom`];
}
if (inactiveCards) {
return ['dormant',
`cardholder is clean and ${inactiveCards} card(s) are still inactive: nothing ` +
'is blocking activation, so nobody ever called it'];
}
return ['healthy', 'active, nothing past due'];
}
async function get(key, path, params = {}) {
const url = new URL(API + path);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
if (res.status === 401) {
throw new Error('401 from Stripe: the key is wrong, or is for the other mode');
}
if (!res.ok) throw new Error(`${res.status} from ${url.pathname}`);
return res.json();
}
async function* paginate(key, path, cap, extra = {}) {
let seen = 0;
const params = { ...extra, limit: 100 };
for (;;) {
const page = await get(key, path, params);
const data = page.data ?? [];
for (const item of data) {
yield item;
seen += 1;
if (seen >= cap) return;
}
if (data.length === 0 || !page.has_more) return;
params.starting_after = data[data.length - 1].id;
}
}
async function main() {
const key = process.env.STRIPE_API_KEY;
if (!key) {
console.error('set STRIPE_API_KEY (use a restricted, read-only key)');
process.exitCode = 2;
return;
}
const inactive = new Map();
let totalInactive = 0;
for await (const card of paginate(key, '/issuing/cards', 5000, { status: 'inactive' })) {
totalInactive += 1;
const holder = (card.cardholder ?? {}).id;
if (holder) inactive.set(holder, (inactive.get(holder) ?? 0) + 1);
}
const counts = new Map();
let cardholders = 0;
for await (const holder of paginate(key, '/issuing/cardholders', 5000)) {
cardholders += 1;
const [state, detail] = verdict(holder, inactive.get(holder.id) ?? 0);
counts.set(state, (counts.get(state) ?? 0) + 1);
if (state === 'healthy') continue;
console.warn(`${holder.id ?? 'ich_?'} ${state.padEnd(18)} ${detail}`);
}
const reasons = new Map();
for await (const auth of paginate(key, '/issuing/authorizations', 1000)) {
if (auth.approved) continue;
for (const attempt of auth.request_history ?? []) {
reasons.set(attempt.reason, (reasons.get(attempt.reason) ?? 0) + 1);
}
}
const blocked = (counts.get('blocked-terms') ?? 0) + (counts.get('blocked-identity') ?? 0);
console.log(`${cardholders} cardholder(s), ${totalInactive} inactive card(s): ` +
`${blocked} blocked, ${counts.get('dormant') ?? 0} dormant`);
for (const [reason, count] of [...reasons].sort((a, b) => b[1] - a[1])) {
console.warn(` ${count} decline(s) with reason ${reason}: ${explainDecline(reason)}`);
}
if (counts.get('blocked-terms')) {
console.warn(` repair: POST ${API}/issuing/cardholders/{ich_id} with`);
console.warn(' individual[card_issuing][user_terms_acceptance][date] and [ip], ' +
'captured when the cardholder accepted the terms');
}
if (counts.get('blocked-identity')) {
console.warn(` repair: POST ${API}/issuing/cardholders/{ich_id} supplying every ` +
'field listed in requirements.past_due');
}
if (blocked || counts.get('dormant')) {
console.warn(` then: POST ${API}/issuing/cards/{ic_id} with status=active. ` +
'Activation before the requirements clear does not stick.');
}
if (blocked || counts.get('dormant') || counts.get('disabled')
|| counts.get('inactive-cardholder')) {
process.exitCode = 1;
}
}
// Only run when invoked directly. The test file imports this module, and without
// the guard main() would run there too, fail on the missing key, and set a
// non-zero exit code that fails the whole test file even as every test passes.
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => { console.error(err.message); process.exitCode = 2; });
}
Add a test
The split that matters is terms acceptance against everything else, so that is what most of these assert. A cardholder blocked only on user_terms_acceptance needs a checkbox and two stored values; one blocked on identity fields needs a person to send documents. Reporting them identically is how a two-hour fix waits three weeks behind a KYC queue.
from stripe_issuing_cardholder_requirements import explain_decline, verdict
TERMS = ["individual.card_issuing.user_terms_acceptance.ip",
"individual.card_issuing.user_terms_acceptance.date"]
def cardholder(past_due=(), reason=None, status="active"):
return {"id": "ich_1", "status": status,
"requirements": {"past_due": list(past_due), "disabled_reason": reason}}
def test_a_clean_active_cardholder_with_no_inactive_cards_is_healthy():
assert verdict(cardholder(), 0)[0] == "healthy"
def test_terms_only_is_not_a_verification_problem():
state, detail = verdict(cardholder(TERMS), 3)
assert state == "blocked-terms"
assert "Nothing needs verifying" in detail
assert "3 inactive card(s)" in detail
def test_one_identity_field_alongside_terms_is_an_identity_block():
# The distinction is all-or-nothing on purpose: a passport scan in the list
# means somebody has to send documents, whatever else is in there with it.
state, _ = verdict(cardholder(TERMS + ["individual.dob.day"]), 1)
assert state == "blocked-identity"
def test_identity_fields_are_named_in_the_detail():
state, detail = verdict(
cardholder(["individual.first_name", "individual.last_name"]), 0)
assert state == "blocked-identity"
assert "individual.first_name" in detail
def test_a_clean_cardholder_with_inactive_cards_is_a_gap_in_your_own_flow():
state, detail = verdict(cardholder(), 4)
assert state == "dormant"
assert "nobody ever called it" in detail
def test_a_disabled_reason_without_past_due_is_reported_separately():
state, _ = verdict(cardholder(reason="listed"), 2)
assert state == "disabled"
def test_an_inactive_cardholder_with_nothing_outstanding_says_so():
state, detail = verdict(cardholder(status="inactive"), 1)
assert state == "inactive-cardholder"
assert "deliberately" in detail
def test_every_known_decline_reason_gets_its_own_repair():
hints = {r: explain_decline(r) for r in
("card_inactive", "cardholder_inactive", "verification_failed",
"insufficient_funds", "spending_controls", "webhook_timeout")}
assert len(set(hints.values())) == 6
assert "top it up" in hints["insufficient_funds"]
assert "latency" in hints["webhook_timeout"]
def test_an_unknown_decline_reason_is_named_not_swallowed():
assert "some_new_reason" in explain_decline("some_new_reason")
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { explainDecline, verdict } from './stripe-issuing-cardholder-requirements.mjs';
const TERMS = ['individual.card_issuing.user_terms_acceptance.ip',
'individual.card_issuing.user_terms_acceptance.date'];
const cardholder = (pastDue = [], reason = null, status = 'active') => ({
id: 'ich_1', status, requirements: { past_due: pastDue, disabled_reason: reason },
});
test('a clean active cardholder with no inactive cards is healthy', () => {
assert.equal(verdict(cardholder(), 0)[0], 'healthy');
});
test('terms only is not a verification problem', () => {
const [state, detail] = verdict(cardholder(TERMS), 3);
assert.equal(state, 'blocked-terms');
assert.match(detail, /Nothing needs verifying/);
assert.match(detail, /3 inactive card\(s\)/);
});
test('one identity field alongside terms is an identity block', () => {
// All-or-nothing on purpose: a passport scan in the list means somebody has to
// send documents, whatever else is in there with it.
assert.equal(verdict(cardholder([...TERMS, 'individual.dob.day']), 1)[0],
'blocked-identity');
});
test('identity fields are named in the detail', () => {
const [state, detail] = verdict(
cardholder(['individual.first_name', 'individual.last_name']), 0);
assert.equal(state, 'blocked-identity');
assert.match(detail, /individual.first_name/);
});
test('a clean cardholder with inactive cards is a gap in your own flow', () => {
const [state, detail] = verdict(cardholder(), 4);
assert.equal(state, 'dormant');
assert.match(detail, /nobody ever called it/);
});
test('a disabled reason without past due is reported separately', () => {
assert.equal(verdict(cardholder([], 'listed'), 2)[0], 'disabled');
});
test('an inactive cardholder with nothing outstanding says so', () => {
const [state, detail] = verdict(cardholder([], null, 'inactive'), 1);
assert.equal(state, 'inactive-cardholder');
assert.match(detail, /deliberately/);
});
test('every known decline reason gets its own repair', () => {
const reasons = ['card_inactive', 'cardholder_inactive', 'verification_failed',
'insufficient_funds', 'spending_controls', 'webhook_timeout'];
const hints = reasons.map(explainDecline);
assert.equal(new Set(hints).size, 6);
assert.match(explainDecline('insufficient_funds'), /top it up/);
assert.match(explainDecline('webhook_timeout'), /latency/);
});
test('an unknown decline reason is named not swallowed', () => {
assert.match(explainDecline('some_new_reason'), /some_new_reason/);
});
FAQ
Why does setting the card to active not work?
Because the block is on the cardholder. While requirements.past_due has anything in it, Stripe will not let cards belonging to that cardholder activate, so the call either errors or the card comes back inactive. Supply the missing fields on the cardholder first, then activate; the order is what makes it stick.
What are user_terms_acceptance.ip and .date?
They record that the cardholder accepted the Authorized User Terms, and they have to be the real IP address and timestamp from the moment of acceptance rather than values filled in later. An integration that collects identity details but never presents the terms produces cardholders that look complete and cannot hold an active card.
Where do I actually see why an authorization declined?
In request_history on the authorization. The top level tells you approved is false; the nested entries carry reason, which is where card_inactive, cardholder_inactive, verification_failed, insufficient_funds, spending_controls and webhook_timeout appear. Those six have six unrelated repairs, so the tally of reasons is the useful output rather than the count of declines.
The cardholder is clean but the cards are still inactive. What now?
That is a gap in your own issuing flow rather than a Stripe block: nothing is preventing activation and nobody called it. It is worth reporting separately, because otherwise it hides inside the same list as the blocked cardholders and gets fixed by collecting fields that were never missing.
Does this need a live secret key?
No. A restricted key with read access to Issuing cardholders, cards and authorizations is enough. The script reads three lists and prints what to update; it cannot update a cardholder or activate a card, which is the right level of privilege for something that runs on a schedule.
Related field notes
- requirements.past_due has already disabled the payouts
- A person on the account has outstanding requirements
- Terminal readers sit offline and take no payments
Sources
Every figure in this note is traced to one of these. Prices are list rates and change — check them for your own region before acting.
- The Cardholder object — Stripe API reference
- The Card object — Stripe API reference
- The Authorization object — Stripe API reference
- Issuing cards — Stripe Docs
If your setup is misbehaving in a way this note does not cover, message me on LinkedIn with what you are seeing.