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/transferFrompattern, 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.
The non-custodial recurring model: link-per-cycle instead of card-on-file
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:
| Field | Example | Notes |
|---|---|---|
customer_id | cust_1284 | your own identifier |
cadence | monthly | interval you enforce in your scheduler |
amount | 49.00 USDT | fixed per cycle unless you version pricing |
next_due_date | 2026-08-08 | advances only after reconciliation |
last_request_id | req_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.
Delivering the renewal link and setting a sane expiry window
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-Signatureon 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.