Use case

Crypto subscription payments without a card on file

A card subscription renews itself: the processor pulls next month's fee from a stored credential without anyone signing anything. Crypto has no stored credential — a wallet signs one transaction at a time, for the exact amount shown, and you cannot reach back in next month to pull the next period's fee. So a crypto subscription is not an auto-charge you set up once. It is a sequence: each cycle becomes its own fiat-anchored invoice with a locked rate, the customer pays it from their wallet, and the verified invoice.paid webhook is what grants the next period of access. This page is about getting that loop right.

01

Model each cycle as one fiat-anchored invoice

The whole subscription reduces to a single repeated action: when a period comes due, create one invoice for what the customer owes that period. Anchor it in fiat — the price your plan is stated in — and let halfin quote the equivalent in the assets you accept and lock that rate at activation. The customer is shown a concrete token amount and a payment window; they never settle a number that drifted while they read it.

Anchor in fiat, not in a token. A subscription priced at USD 49 is a fiat-anchored invoice — you send the fiat amount and currency, and halfin pins the payable asset amount. The alternative, a fixed-asset invoice, names a crypto amount and currency directly (for example 0.01 BTC), which is what you want for crypto-denominated pricing but not for a plan you sell and book in dollars. For subscriptions the fiat anchor is the point: your pricing page, your MRR, and your revenue reporting stay in fiat while the customer pays in USDT, USDC, or a native asset.

There is no recurring endpoint and no saved-card object to manage. You call the invoicing primitive once per period. The only thing that makes the call belong to a subscription rather than a one-off sale is how you schedule it and how you key it: one invoice per account per cycle, created by your billing job when the period rolls over.

  • One billing cycle = one fiat-anchored invoice, created when the period comes due.
  • Send amount_fiat + fiat_currency; halfin quotes the asset amount and locks the rate at activation.
  • Fixed-asset invoices (amount + crypto currency) are for crypto-denominated pricing, not fiat-stated plans.
  • No recurring endpoint, no stored credential — the invoicing primitive called once per period is the whole mechanism.
02

Grant entitlement off the verified invoice.paid webhook

The hard rule of a crypto subscription is what flips an account to active. It is not the customer landing back on a success page, and it is not a polling loop guessing at confirmations. It is the invoice.paid webhook — the platform's authoritative statement that the billed amount settled on-chain at the chain's confirmation threshold, reorg-aware. That event is the only thing that should extend the subscription period and set the next renewal date.

Verify the HMAC signature before you act. Your webhook URL is public the moment you register it, so anyone can POST JSON shaped like an invoice.paid event. The signature is what separates a real settlement from a forged one. Recompute the HMAC over the raw request bytes, compare in constant time, and only then read the event and grant access. Granting a paid period off an unverified payload is the difference between a paying customer and a free one.

Tie the grant back to the cycle, not just the account. The event carries the invoice you created, so resolve which account and which billing period it settles, mark that period paid, extend access, and schedule the next invoice. Make the grant idempotent on the invoice identity: webhook delivery is at-least-once, so the same invoice.paid can arrive twice, and a second delivery must not hand out a second month.

  • invoice.paid — and only invoice.paid — extends the subscription and sets the next renewal date.
  • Verify the HMAC signature over the raw body before reading the event; a redirect or a poll is not authoritative.
  • Resolve the event back to account + billing period; mark that period paid and schedule the next cycle.
  • Make the grant idempotent on the invoice — at-least-once delivery means the event can repeat.
03

Map every state to a subscription decision

A card subscription has two outcomes you care about — charged or declined. A crypto subscription surfaces a few more, because the customer is sending real funds from a wallet you do not control. Each canonical invoice event maps to one decision in your billing state machine. Wire them once and the loop runs itself.

The table is the contract. Treat any event you have not mapped as a reason to hold access, not to grant it.

Webhook eventWhat happened this cycleSubscription decision
invoice.confirmingThe cycle's invoice is live, rate locked, awaiting payment.Show the amount, address, and countdown; do not change access yet.
invoice.paidThe billed amount settled at the chain's confirmation threshold.Extend the period, set the next renewal date, schedule the next invoice.
invoice.underpaidA real payment arrived but fell short of the billed amount.Do not grant the full period; request a top-up or settle per your policy.
invoice.overpaidThe customer sent more than the billed amount.Grant the period; record the excess as credit or refund per your policy.
invoice.expiredThe payment window elapsed with no sufficient payment.Treat as a non-renewal; hand off to dunning — reminder, grace, or downgrade.
04

The renewal job, end to end

A renewal is short to describe and entirely API-driven. Your scheduler wakes up, finds accounts whose period is ending, and creates one invoice each. The idempotency key is what keeps a retried job — or two workers racing — from billing the same account twice for the same cycle: scope the key to the account and the period, and a repeat call returns the existing invoice instead of issuing a second one.

From there the customer pays on hosted checkout or a checkout you render yourself, halfin confirms on-chain, and your only job is to wait for the signed webhook and act on it per the table above. An expired invoice is your non-renewal signal — re-point whatever your product already does for a failed card (reminder email, grace period, downgrade to free) at the expiry event.

The request below creates one cycle's invoice. The same call backs a cron renewal, a usage-rollup worker, or a self-serve upgrade — there is no separate recurring path, just this primitive called once per period with a cycle-scoped idempotency key.

  • Scheduler finds ending periods and creates one fiat-anchored invoice per account.
  • Idempotency key scoped to account + cycle makes the create call safe to retry and race-safe.
  • Customer pays via hosted or self-hosted checkout; halfin confirms reorg-aware.
  • Verified invoice.paid extends the period; invoice.expired feeds dunning.
# Called once per billing cycle by your renewal scheduler.
# The idempotency_key is scoped to account + period, so a retried
# job (or a racing worker) returns the existing invoice rather
# than billing the customer twice for the same cycle.
curl https://api.thehalfin.com/api/v1/invoices \
  -H "X-API-Key: $HALFIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_fiat": "49.00",
    "fiat_currency": "USD",
    "deferred": true,
    "description": "Pro plan — acct_42 — 2026-07",
    "idempotency_key": "sub-acct_42-2026-07"
  }'

# halfin locks the rate at activation and returns an invoice you
# present to the customer. Grant the next period only on the verified
# invoice.paid webhook. See docs.thehalfin.com for the response schema.
05

Plan changes, proration, and the non-payment path

Because each cycle is a discrete invoice rather than a standing mandate, plan changes are simpler than they are with a stored card. An upgrade mid-cycle is just the next invoice computed at the new price — or, if you prorate, a one-off invoice for the difference you calculate in your own billing logic. There is nothing to re-authorize and no saved instrument to update; you decide the amount and issue an invoice for it.

A cancellation is the absence of a next invoice. When a customer cancels, stop scheduling renewals for that account; access runs out naturally at the end of the period they already paid for, because you never granted beyond the last verified invoice.paid. There is no recurring charge to revoke, which means no accidental charge after a cancellation either.

Non-payment is the case crypto makes more honest. A card declines and your processor retries on its own cadence; a crypto invoice simply expires when its window passes, and you own the follow-up entirely. Treat invoice.expired as a non-renewal event, then run your dunning flow on your terms — re-issue at the current rate if they come back late, grant a grace period, or downgrade to a free tier. The customer is never silently charged a stale rate, and you are never reconciling a renewal you did not initiate.

  • Upgrade or downgrade = the next cycle's invoice at the new price (or a prorated one-off you compute).
  • Cancellation = stop scheduling the next invoice; access lapses after the last paid period.
  • Non-payment = invoice.expired, owned by your dunning flow — re-issue, grace, or downgrade.
  • No saved instrument means no charge can fire after a customer cancels.
06

Honest recurring billing, and where to go next

Be precise with customers about what recurring means here: it is invoice-per-cycle billing, not a saved-card auto-charge. Every period, the customer actively signs a payment from their wallet for an amount you re-quote at the live rate. That is a feature in many markets — payers who do not trust a merchant with a standing pull, or who hold value in stablecoins rather than a bank, get a clean per-cycle bill they approve each time — but it is a different mental model from a card subscription, and your billing copy should say so rather than imply an unattended renewal.

Start with the invoicing primitive, since the entire loop is that primitive called once per cycle with a rate lock. Wire the webhook before anything else that grants access — verifying its signature is the line between extending a subscription correctly and extending it on a forged request. The parent SaaS use case covers the surrounding jobs — usage-based billing, API-first checkout, developer payouts, and treasury conversion — that share the same balance and event stream as this recurring path.