How to Bill Recurring Crypto Payments, Non-Custodially

Crypto has no auto-charge. Learn the non-custodial way to bill recurring customers: automated payment links, webhooks, and reconciliation — no custody.

Voxel-style circular arrows linking glowing payment tiles in emerald and gold on a dark background

Crypto has no equivalent of a stored card number — no auto-charge button, because the only two ways to make a recurring payment happen automatically are a processor holding custody of the funds, or an on-chain allowance the customer explicitly grants (and that only works for token standards built to support it, not BTC or XMR). Every crypto subscription you’ve ever paid for is quietly built on one of those two mechanisms. This guide covers the third option: a fully non-custodial “link-per-cycle” architecture that automates payment-request creation and reconciliation on top of the SwapZilla Pay API, with no standing authorization over your customer’s wallet.

Why crypto can’t do card-style auto-charge

A stored card number works because Visa and Mastercard built rails specifically for a merchant to pull funds on a schedule, with the card network as the enforcement layer if something goes wrong. Bitcoin, Monero, and most base-layer crypto assets have nothing analogous. A wallet address is not a permission token — sending BTC to an address once does not give anyone the right to charge that address again next month.

There are exactly two ways anyone has engineered around this gap, and both change the trust model:

  • Custodial recurring billing. A processor holds the customer’s funds, or a pre-funded balance, and initiates the charge itself on your schedule. This is how most “crypto subscription” products actually work under the branding — the recurring part is a custodial ledger entry, not an on-chain event.
  • On-chain allowance. Certain token standards — ERC-20 being the best-known — support an approve/transferFrom pattern, where a customer grants a smart contract standing permission to pull up to a set amount. This only exists for tokens built with that interface; it has no equivalent for BTC or XMR, and it means the customer is granting an ongoing allowance rather than approving each payment individually.

Every crypto subscription you’ve paid for is quietly built on one of two mechanisms: a custodian holding your funds, or a smart-contract allowance you granted once and forgot about.

Neither is wrong, but both trade something for the convenience of auto-charge. Custody means a third party controls settlement and can freeze or delay it. An allowance means the customer hands over standing permission that persists until explicitly revoked — a real attack surface if the contract or the party holding the allowance is ever compromised. Worth flagging directly: SwapZilla Pay does not implement an on-chain allowance model; the approve/transferFrom pattern above is included as a contrast, not as something the API does.

That leaves a third option most billing guides skip: making the customer send a discrete payment each cycle, but automating everything else around it so it doesn’t feel manual.

Replace “card on file” with “link per cycle.” Instead of storing a payment method and pulling from it, you generate a fresh payment request for each billing period, and the customer completes it the same way they would a one-off invoice — a single on-chain transaction from a wallet they control.

This preserves every non-custodial property covered in how to accept crypto payments without a merchant account: no third party holds the settlement balance, the customer’s funds go straight to your address via the aggregator’s routing, and there’s no standing authorization sitting on a contract or a processor’s ledger between cycles.

It’s worth being precise here: SwapZilla Pay does not have a native subscription object. There’s no “create subscription” endpoint, no built-in cadence, and no dunning engine. What Pay gives you is the same payment-request API documented at /pay/docs that powers one-off invoices — the recurring behavior is something you build on top of it, in your own application, by calling that API on a schedule you control.

That’s the trade you’re making versus a custodial subscription product: more integration work upfront, in exchange for never holding customer funds and never asking for standing on-chain permission.

Automating payment-request creation on a schedule with the Pay API

The billing cycle lives in your database, not in Pay. Model it as a plain record per customer: customer_id, cadence (monthly, weekly, and so on), amount, receive_asset, and next_due_date. Pay has no concept of a subscription, so this state is entirely yours to keep and update.

A scheduler — a cron job, a serverless function on a timer, a queue worker — walks that table daily and, for every row where next_due_date is within your lead window (a few days out is typical), calls the Pay API to create a new payment request for that cycle’s amount and receive asset. Store the returned request id against the customer’s row immediately, before you send anything to the customer — you’ll need it to reconcile the webhook later.

For exactly how API access is provisioned — key format, authentication, rate limits — treat /pay/docs as the source of truth rather than any summary here. Provisioning details can change, and the docs are what your integration should follow at build time.

A minimal cadence record looks like this in practice:

FieldExampleNotes
customer_idcust_1284your own identifier
cadencemonthlyinterval you enforce in your scheduler
amount49.00 USDTfixed per cycle unless you version pricing
next_due_date2026-08-08advances only after reconciliation
last_request_idreq_9f2a…the Pay request tied to the current cycle

Keep pricing changes explicit — if you raise a plan’s price, update the stored amount on your side and let the next generated request reflect it. There is no live-pricing hook inside a payment request once it’s created.

Once the request exists, send the link with enough runway before it expires — email, in-app notification, or both. A renewal link that expires the same day it’s created gives a customer with a slow wallet, a busy weekend, or a missed notification almost no room to pay on time.

A short expiry window is still the right instinct: it limits how long a quoted rate needs to hold, and it caps how long a stale, unpaid link stays live and confusing in your system. Most merchants land on a window measured in a small number of days, not weeks — long enough for a customer to notice and act, short enough that you’re not chasing a link from three billing cycles ago.

Two habits help here. First, send the link a few days before next_due_date, not on the due date itself — that leaves room for a failed first attempt. Second, treat every renewal link as disposable: if it expires unpaid, generate a new one rather than trying to revive the old request. Pay requests don’t have a re-open action; a fresh request with a fresh expiry is the correct next step.

Reconciling renewals with HMAC-verified webhooks

This is the step that turns a link-per-cycle flow from manual invoice chasing into something that actually automates. Pay emits three webhook events across the life of a payment request: payment.created when the request is generated, payment.updated on intermediate state changes as the swap progresses, and payment.completed once the customer’s payment has settled.

Your integration should only ever advance a subscription’s next_due_date and grant the next cycle of access on a verified payment.completed event — never on payment.created, and never on the customer simply landing back on your redirect URL. Every webhook delivery carries an X-SwapZillaPay-Signature header, an HMAC over the payload; verify it server-side against your webhook secret before trusting anything in the body.

Never mark a cycle paid because the customer returned to your app. Mark it paid because payment.completed arrived with a signature you verified.

An unverified webhook endpoint is an open invitation: anyone who guesses your endpoint URL can POST a fake payment.completed and grant themselves access. Verify first, act second, every time — no exceptions for “trusted” traffic.

Handling missed payments and dunning without an auto-retry button

Because there’s no card to retry and no standing allowance to pull from — unless you’ve built something custodial, which this architecture deliberately avoids — a missed renewal is a genuinely missed payment, not a temporary decline the network will quietly retry for you. Your dunning logic has to replace that missing retry entirely.

A workable pattern: send a reminder before the due date, send a second reminder if the link expires unpaid, and generate a fresh request rather than reusing the expired one. Give the customer a short grace period of continued access after the due date passes — long enough to cover “I meant to pay yesterday,” short enough that non-payers don’t ride free for a full extra cycle. If the grace period lapses without a verified payment.completed, downgrade or suspend access on your side; that state change lives entirely in your application, since Pay has no awareness of what “access” means for your product.

None of this can auto-resolve itself the way a card network’s retry logic can. Budget for the support-inbox reality: customers who forget, customers whose wallet balance came up short, and customers who need a fresh link because the old one expired mid-transfer.

Common mistakes and when this approach isn’t the right fit

The mistakes that show up most often when teams build this for the first time:

  • Assuming Pay has a subscription object. It doesn’t — there is no subscriptions endpoint, no built-in cadence, no dunning engine. All of that state and logic is yours to build, per the payment-request API documented at /pay/docs.
  • Skipping signature verification “just for now.” This is the single most common way link-per-cycle billing gets exploited. Verify X-SwapZillaPay-Signature on every webhook, in every environment, from day one.
  • Setting an expiry window so short that legitimate customers get chased for a link that already died. Give real payers real runway.
  • Treating a fixed invoice amount as immune to rate movement. If you price in a volatile pay-in coin, a floating-rate request can settle for a different value than you expected due to market movement between quote and payment. Decide up front whether you price in a stable receive asset or account for variance, and see how the aggregator’s routing and rate types work before you lock in an approach.
  • Building the whole reconciliation loop before testing the scheduler in isolation. Get request creation right on its own first — webhook handling and dunning logic are easier to debug against a system you already trust to create requests correctly.

This architecture is a good fit for SaaS, subscription content, and recurring service billing where customers are already crypto-native and comfortable completing a short payment flow every cycle. It’s a poor fit if you need an invisible, zero-friction auto-renew experience indistinguishable from a stored card — that’s a custodial trade-off this guide deliberately avoids, not a gap you can engineer around non-custodially. It’s also not the right fit for usage-based billing with highly variable, tiny per-cycle amounts, where the overhead of generating and chasing a link per charge outweighs the value of each charge.

If you’d rather self-host the entire stack instead of building on an API, BTCPay Server’s Greenfield API supports a similar build-your-own-cadence approach on infrastructure you run yourself — the trade-off there is operational overhead in exchange for full sovereignty over every request in the flow.

FAQ

Can crypto payments be set up to auto-renew like a card subscription?
Not natively, and no non-custodial rail can fully replicate the invisible auto-renew a stored card gives you, because there's no crypto equivalent of a card network pulling funds on your behalf. What you can build is close: a scheduler that creates a fresh SwapZilla Pay payment request a few days before each due date and sends the customer a link, so renewal feels automatic on your side even though the customer completes a real transaction each cycle. If you want zero customer action per cycle, the only paths are custodial billing (a processor holds funds and settles internally) or an on-chain allowance for token standards that support one — both are different trust trade-offs, not features of this non-custodial model.
Is there a way to charge a customer's crypto wallet automatically every month?
Not without giving something up. BTC and XMR wallets have no allowance mechanism at all — there is no way to charge one without the holder initiating the transaction themselves. Some token standards, ERC-20 among them, support an approve/transferFrom pattern where a customer grants a contract standing permission to pull funds up to a limit, but that requires the customer to actively grant an ongoing allowance, only works for compatible tokens, and carries its own risk if the contract or the party holding the allowance is ever compromised. SwapZilla Pay does not use this model — it's a non-custodial, link-per-cycle system where the customer completes each payment directly, and no standing pull permission ever exists.
What's the real difference between recurring crypto billing and a Stripe-style subscription?
A Stripe subscription pulls funds from a stored card on your schedule with no customer action required after the first sign-up. A non-custodial crypto version flips that: your system creates a new payment request each cycle, and the customer completes a real transaction every time, because there's no stored payment method to pull from. The upside is that no processor or contract ever holds standing authorization over the customer's funds. The trade-off is integration effort on your side — building the scheduler, the reconciliation logic, and the dunning flow Stripe provides out of the box — plus a small amount of active participation from the customer each cycle.
Can an ERC-20 allowance (approve/transferFrom) let me auto-charge stablecoins without custody?
It's the closest thing crypto has to a card-on-file model without a custodian holding funds, but it isn't free of trade-offs. The customer signs one approve transaction granting a contract permission to pull up to a set amount, and your contract calls transferFrom on your schedule. It works only for token standards built with that interface — it has no equivalent for BTC or XMR — and the customer is trusting the contract, and whoever controls it, with a standing allowance until they revoke it. This is a genuinely different architecture from SwapZilla Pay's link-per-cycle model, which asks for no standing permission at all; it's mentioned here as a contrast, not as something Pay implements.
Does SwapZilla Pay have a native subscriptions or recurring-billing feature?
No, and it's worth being direct about that. Pay's API is built around single payment requests: you create one, the customer pays it, you get a webhook when it settles. There is no subscriptions endpoint, no built-in cadence, no dunning engine, and no stored-customer object with a recurring amount. The recurring architecture described in this guide is something you build yourself on top of the payment-request API — a scheduler that generates a new request each cycle, plus your own database tracking cadence and due dates. See [/pay/docs](/pay/docs) for the exact request and webhook shapes to build against.
How do I handle a customer who misses a renewal payment?
Treat it as a genuinely missed payment, not a temporary decline — there's no card network quietly retrying it for you. A workable pattern is a reminder before the due date, a second reminder if the link expires unpaid, a short grace period of continued access after the due date, and a fresh payment request (never a reused expired one) if the customer wants to catch up. Once the grace period lapses without a verified payment.completed webhook, downgrade or suspend access on your side — that logic lives entirely in your application, since Pay has no concept of what access means for your product.
What happens if the exchange rate moves between billing cycles on a fixed invoice amount?
The receive amount you set when you create the payment request is what's meant to land in your wallet; the customer's pay-in amount is computed at the rate available when they pay, not when you created the link. If you price your plan in a stable receive asset, such as a stablecoin, rate movement mostly affects what the customer sends, not what you receive. If you price in a volatile receive asset, the value you actually get can differ cycle to cycle purely from market movement — that's a pricing decision to make deliberately, not something the payment-request flow smooths out for you. See [how rate types work in the aggregator](/how-it-works/) before deciding.