Charging a card every month is a solved problem and your processor solves it. What is not solved for you is everything around it: when exactly the charge lands, what your product does between a failed payment and a cancellation, and which of the eight states a subscription can be in should still let someone log in.
This is about that second category.
Creating the subscription
The prerequisite is a customer with an attached payment method — a freshly tokenized card is single-use and expires in about fifteen minutes, so it cannot back a subscription. That sequence is covered in storing payment methods safely.
POST /v1/subscriptions
{
"customer_id": "cus_yP8jSLsFD16WmiH1mW6rl",
"payment_method_id": "pm_7qOHcBHz0zBtIYrS37sOT",
"currency": "usd",
"price": 2500,
"interval_unit": "month",
"interval_count": 1,
"billing_cycle_anchor": "2026-10-01T09:00:00-04:00"
}
-> 201 { "id": "sub_dDRoYZIbPAFvTmRgmfxym",
"status": "pending",
"next_payment_at": "2026-10-01T13:00:00.000Z" }The timezone trap in the cycle anchor
This one is worth its own section because it is silent, it affects every customer, and it recurs every cycle rather than once.
A date-only anchor like "2026-10-01" is interpreted as midnight UTC. Midnight UTC on 1 October is 8PM Eastern on 30 September. Your customer is charged the day before the date on their invoice, every month, and the only signal is occasional confusion in support.
| You send | It means (UTC) | Eastern time | Result |
|---|---|---|---|
| 2026-10-01 | 2026-10-01 00:00Z | 30 Sep, 8PM | Charged a day early, every cycle |
| 2026-10-01T00:00:00-04:00 | 2026-10-01 04:00Z | 1 Oct, midnight | Correct, customer-local midnight |
| 2026-10-01T09:00:00-04:00 | 2026-10-01 13:00Z | 1 Oct, 9AM | Correct, and during business hours |
The states you will forget
Most implementations start with two states — active and cancelled — and grow the rest painfully, one production incident at a time. The full set is small enough to write down at the start.
| State | Means | Access? |
|---|---|---|
| pending | Created, waiting for its first billing date | Depends on whether you offer a trial |
| trialing | In a free trial, no payment taken yet | Yes |
| active | Paid and current | Yes |
| past_due | A payment failed, retries still in progress | Yes — this is the grace period |
| unpaid | Retries exhausted, still not paid | No, but keep the data |
| canceled | Ended, by the customer or by you | No, at period end |
| paused | Deliberately suspended, intended to resume | No, and do not bill |
| incomplete | Created but never successfully set up | No |
The row that matters most is past_due. Treating it as cancelled is the single most expensive mistake in this list: it takes a customer whose card expired and turns them into a churned account, usually while they are still happily using the product and entirely willing to pay.
Keep billing state and entitlement state apart
There are two questions here that look like one: has this customer paid? and what is this customer allowed to do? The processor owns the first. You own the second, and you should own it in your own database.
// Driven by webhooks, so your product never calls the payments API on a
// request path and never goes down because someone else does.
const ACCESS = {
pending: false,
trialing: true,
active: true,
past_due: true, // the grace period — the expensive row to get wrong
unpaid: false,
canceled: false,
paused: false,
incomplete: false,
};
export async function canAccess(userId) {
const sub = await db.subscriptions.findByUser(userId);
if (!sub) return false;
// Trust the mirrored state, but never past its own period end: a missed
// cancellation webhook should fail closed rather than grant forever.
if (sub.currentPeriodEnd && sub.currentPeriodEnd < new Date()) {
return sub.status === "past_due"; // still retrying, still inside grace
}
return ACCESS[sub.status] ?? false;
}The period-end check is the part worth keeping. Mirrored state is only as fresh as the last webhook you successfully processed, and the failure mode of a missed cancellation is granting access indefinitely. Expiring on the stored period end makes the mirror fail closed.
Failed payments are most of the work
A meaningful share of subscription cancellations are not decisions. The card expired, or was reissued after fraud, or the balance was short on the day. The customer wants the product; the payment stopped working. Recovering those is called dunning, and it usually returns more revenue than any pricing change you could ship in the same week.
- Space the retries out. Three or four attempts over one to two weeks. The commonest cause is a temporary balance problem, so calendar spacing recovers far more than rapid retries.
- Tell the customer, in plain terms. One clear email naming the card and the amount, with a direct link to update it. Not a dunning sequence of five.
- Keep access during the grace period. Someone who can still use the product has a reason to fix the card.
- Distinguish the decline reasons. An expired card needs a new card; insufficient funds needs a later retry. Sending the same message to both wastes half of them.
- Stop at a clear point. Retrying indefinitely annoys people and can attract issuer scrutiny. Move to
unpaid, revoke access, and keep the data so returning is one click.
Retries have no idempotency net
The API has no idempotency-key header, so a retried POST /v1/subscriptions after a timeout creates a second subscription and bills the customer twice. For a one-off charge that is a refund and an apology; for a subscription it is a recurring double charge that continues until somebody notices.
Record your own reference before the call and check it before retrying — the pattern is set out in idempotency keys explained. Of everywhere that guard matters, subscription creation is the place it matters most.
A build order that works
- Store a card and attach it properly. Everything else depends on this.
- Create a subscription with a full timestamp anchor, and check the first charge lands on the day you expected in the customer timezone.
- Mirror state from webhooks into your own table, with the period-end guard.
- Write the entitlement function, and test
past_duegrants access. - Add the retry guard around creation.
- Build dunning: expiry warnings first, then failed-payment recovery.
- Only then worry about proration, plan changes and upgrades.
Proration is last deliberately. It is the part that feels hardest because it involves arithmetic, and it is the part that costs least when it is briefly wrong — a few cents on an invoice, against an entitlement bug that either gives the product away or locks out paying customers.
GOOD QUESTIONS
Frequently asked
Should I build subscription billing myself or use the processor?+
Use the processor for the billing loop — the scheduled charge, the retry schedule, the proration maths. Build your own layer for entitlements: what the customer can actually do in your product. The two answer different questions, and conflating them is what makes billing code hard to change later.
Why does my subscription bill a day early?+
Almost always a date-only billing cycle anchor. A value like 2026-10-01 is interpreted as midnight UTC, which is the evening of 30 September in North American timezones. Send a full ISO 8601 timestamp with an offset and the problem disappears.
What does a pending subscription status mean?+
On the Paydude API, pending is the healthy result of creating a subscription — it means the subscription exists and is waiting for its first billing date. It does not need confirming. Only a subscription in requires_confirmation should be confirmed, and calling confirm on a pending one returns an error saying so.
How many times should I retry a failed subscription payment?+
Three or four attempts spread over one to two weeks recovers most recoverable failures without irritating anyone. The spacing matters more than the count, because the most common cause is a temporary balance problem that resolves on payday. Retrying four times in an hour recovers almost nothing.
Should a failed payment immediately revoke access?+
No. A first failed payment is usually a card problem, not a decision to leave. Keep access during a grace period while you retry and contact the customer; revoking on the first decline converts a fixable billing event into a cancellation.
Do I need to store subscription state in my own database?+
Yes, at least the parts your product reads. Checking entitlement by calling the payments API on every request couples your product availability to theirs and is slow. Mirror the state you need via webhooks, and treat the processor as the source of truth for money and your own store as the cache for access.
