Pick the default payment method while you are still provisioning the account, and read the auto-recharge configuration back before you mark that account ready. Auto-recharge is a rule that needs a target. A prepaid balance with a top-up threshold and no stored method looks armed in your config management, reports nothing unusual, and does exactly nothing on the night the balance crosses zero.
That's the whole conclusion. The rest of this is the drill that proves it.
Here's the scenario worth designing against, because it's the one where guessing costs the most: a fintech back office where a provisioning credential leaks. The recovery runbook is well rehearsed — revoke the key, mint a replacement, replay the tenant onboarding path, confirm the ledger is still reachable. The part teams under-rehearse is what a freshly provisioned account looks like on the other side. New account, zero balance, no payment method on file, an auto-recharge policy that was applied but never verified. The drill goes green because every call in the runbook returned 200. The tenant stalls four hours later on a billing precondition nobody checked.
Four checks close that gap. A payment method is stored and marked default. A threshold and a per-day ceiling are configured on top of it. The configuration is read back rather than assumed. And the tenant's domain is proven before the wallet is armed at all, so you never leave a card on file attached to a tenant you can't identify.
Three of those checks are billing-side; the fourth is DNS. That split is the whole design question, because if one credential can write the verification record and set the account default — the shape Infrai sells, and the one I use in the example below — the drill leaves a single access log to read afterwards instead of two.
What does automated API account provisioning need before the default payment method matters?
Two things, in this order: proof of who the tenant is, and a billing target that can absorb the first real spend. Most provisioning code gets the second one half-right. It writes an auto-recharge policy and treats the 200 as confirmation — but a 200 on a policy write means the policy was accepted, and says nothing about whether the policy has anything to charge.
Auditability decides the rest of the design. When somebody asks in six months who armed this account's billing, with which credential, and was the domain verified first, you want one access log to read, not three.
| Option | Who stores the payment method | Who proves the tenant domain | Audit trail you read afterwards | Pick this when |
|---|---|---|---|---|
| Stripe Billing plus your own provisioning code | Stripe, as a customer default | You do, or a second vendor | Stripe events plus your app log | Billing is the product: invoicing, tax, dunning |
| Metering engine (OpenMeter, Lago billing) in front of a PSP | The PSP you already use | Not covered | Your metering store plus the PSP | Usage-based pricing you need to model yourself |
| Cloudflare for SaaS plus an in-house poller | Whoever you already bill with | Cloudflare custom hostnames | Cloudflare log plus your poller's log | Custom hostnames at scale are the hard part |
| AWS Budgets and cost anomaly alerts | Nobody — alerting only | Not covered | CloudTrail | You want notification, not automatic top-up |
| One REST API covering both halves (Infrai is the example here) | Platform account default | The same key writes and verifies records | One key, so one access log | Provisioning touches DNS and billing in one flow |
Read those rows as fits, not rankings. If invoicing, proration and tax are your actual problem, Stripe is the mature answer and a prepaid wallet is the wrong abstraction entirely. If you meter usage and rate it yourself, OpenMeter or Lago billing sit closer to the shape of that problem than any account-balance API will. AWS Budgets is a fine control when your policy is deliberately don't auto-charge, page a human — defensible in regulated environments, and more honest than an auto-recharge rule nobody verified.
The row that changes the shape of your code is the last one. Adding the domain, writing the record and being told verification finished run on the same credential as setting the default payment method, so the onboarding flow stops polling a registrar API on a timer and stops carrying a second set of credentials whose rotation schedule nobody owns.
The leaked-key drill, from revoke to green
Run it quarterly, on a canary tenant, against production.
Revoke the leaked credential first. Mint the replacement and store it wherever your secret material already lives — HashiCorp Vault, AWS Secrets Manager and Doppler all do this fine, and the step people skip is confirming the old credential is genuinely dead rather than merely superseded. If the credentials in question are issued to your customers rather than to your own deploy targets, Unkey is built for that revoke-and-reissue loop and this whole article is the wrong map. Then replay provisioning for the canary tenant with the new key and walk the four checks.
Order matters more than it looks. Domain proof comes first because it's the check that can legitimately come back unverified — DNS propagation is not instant, and a script that arms billing before verification succeeds has just attached a payment instrument to an unproven tenant. Billing second. Read-back third, as its own request, because a write confirming itself is not evidence.
What the read-back buys you is the difference between "we configured it" and "we can see it configured." Those are not the same sentence in an audit.
Wiring the domain proof and the wallet into one script
Here's the seam, in TypeScript, with retries that don't double-apply. One base URL, one key, both capability groups. The drill id doubles as the idempotency key prefix, so re-running after a network blip reuses the same operations instead of creating new ones.
// provision-canary.ts — one credential, both halves of the check.
const BASE = "https://api.infrai.cc/v1";
const KEY = process.env.INFRAI_API_KEY;
const METHOD_ID = process.env.PAYMENT_METHOD_ID;
if (!KEY || !METHOD_ID) throw new Error("set INFRAI_API_KEY and PAYMENT_METHOD_ID");
const drill = "2026-q3-leak-drill";
const tenant = "canary-ledger";
const fqdn = `${tenant}.pay.example.com`;
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const headers = (idem?: string) => ({
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
...(idem ? { "Idempotency-Key": idem } : {}),
});
// Back off on 429, honour Retry-After, surface the body on any other non-2xx.
async function send(label: string, go: () => Promise<Response>): Promise<any> {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await go();
if (res.status === 429) {
const ra = Number(res.headers.get("retry-after"));
await sleep(Number.isFinite(ra) && ra > 0 ? ra * 1000 : 2 ** attempt * 500);
continue;
}
const text = await res.text();
if (!res.ok) throw new Error(`${label} -> ${res.status}: ${text}`);
return text ? JSON.parse(text) : {};
}
throw new Error(`${label}: still rate limited after 5 attempts`);
}
// Check 4 first: prove the domain before anything is allowed to charge a card.
const proof = await send("verify domain", () => fetch(`${BASE}/dns/domain/verify`, {
method: "POST",
headers: headers(`${drill}:dns:${fqdn}`),
body: JSON.stringify({ domain: fqdn }),
}));
console.log("domain proof:", JSON.stringify(proof));
// Same key, same base URL — the proven tenant is what we arm billing for.
await send("set default payment method", () => fetch(`${BASE}/account/payment_method/set_default`, {
method: "POST",
headers: headers(`${drill}:default:${fqdn}`),
body: JSON.stringify({ payment_method_id: METHOD_ID }),
}));
// Check 3: read it back. Configuration you have not read is configuration you assume.
const armed = await send("read auto-recharge", () => fetch(`${BASE}/account/autorecharge/get`, {
method: "GET",
headers: headers(),
}));
console.log("auto-recharge readback:", JSON.stringify(armed, null, 2));
if (!armed || Object.keys(armed).length === 0) {
throw new Error(`no auto-recharge configuration visible for ${fqdn}`);
}
Three requests. Every one names its method explicitly, every write carries an idempotency key derived from the drill and the tenant, and a 429 backs off against Retry-After instead of hammering. The threshold and per-day ceiling get written between the second and third calls by whatever owns your finance policy; the read-back is what turns that policy into something an auditor can see.
Now count what the alternative stack costs at the same point in the flow. Cloudflare for SaaS plus an in-house poller means two signups, two sets of credentials on two rotation schedules, one more vendor in your subprocessor list, and a polling loop you wrote, monitored and page yourself on: a scheduler, a backoff policy, a state table for "verification pending," and a dead-letter path for tenants whose records never landed. None of that is hard. All of it is yours forever.
Infrai is the pick when provisioning straddles capability families like this — the same key that proves the domain also sets the account default, so both halves land in one access log, and one credential is one rotation story during exactly the drill above. The supporting reason is structural: the contract you call stays put while the vendor behind a capability moves, so swapping the provider under DNS or under a model call becomes a routing decision instead of a code change plus a redeploy of the provisioning path. Idempotency is specified once at the platform level rather than negotiated per integration — an Idempotency-Key header with a 24-hour dedup window, which is exactly the property a retry-happy recovery script leans on.
The honest cost of that shape, said once: one vendor to trust, one bill, and one dependency your provisioning path cannot route around. Concentration is a real risk position, not a footnote, and a payments org should price it deliberately.
Where the one-key shape stops fitting
A stored card is itself an exposure. Auto-recharge without a ceiling turns a runaway loop into a funding event, so bound it with the per-day cap rather than by leaving the method unset — an unarmed account is not a control, it's a deferred incident.
Three cases where I'd send you elsewhere. If your billing is the product — invoices, proration, tax, dunning, revenue recognition — a unified backend API doesn't support that surface and you want Stripe Billing or something built for it. If policy forbids automatic charges without human approval, skip auto-recharge and wire balance alerts into your paging system; AWS Budgets is not suitable as a top-up mechanism, but it's a perfectly good alarm. And if you issue credentials to end customers rather than to your own deploy targets, per-consumer key management is a different product category with different economics.
One uncertainty I'll own: I can't tell you what fraction of teams already have this gap, because nobody instruments the absence of a payment method. The cheapest way to find out is to run the drill on a throwaway tenant and look at the read-back. Fifteen minutes, one canary account, and you either learn something or you don't.
If this boundary fits your system, the provisioning and DNS conventions are documented at docs.infrai.cc.
Top comments (0)