Free Trial to Paid Subscription Billing: Technical Implementation
I spent the first few months of building our subscription product treating the free trial as a marketing feature, not a billing feature. That was a mistake. The trial itself is easy — anyone can gate a dashboard behind a signup form and a countdown. What's hard, and what I didn't appreciate until our first batch of trial users actually reached day 14, is the moment the trial has to turn into a real charge without anyone clicking anything. That single transition — authorize now, charge later, unless cancelled — is where most of the support tickets, chargebacks, and churn complaints in a subscription business originate. This article walks through how we actually built it for a Philippines-facing SaaS product on top of SMARTPAY, and the parts of the flow that are easy to get subtly wrong.
Why the Trial-to-Paid Moment Is Where Subscription Billing Actually Breaks
A normal recurring charge is low-drama. The customer already knows they're a paying subscriber, they've seen a few charges go through, and if something fails they expect a retry or a dunning email. A trial conversion is different because it's the first real charge the customer experiences, and it happens automatically, days or weeks after they stopped thinking about your product. If the timing, the amount, or the reminder is off, the customer's first real interaction with your billing system is a surprise debit — not a renewal.
That gap between "signed up for free" and "got charged without doing anything" is also exactly where support load concentrates. In our case, the first cohort of trial users produced a disproportionate share of "why was I charged" tickets relative to every other billing event combined, simply because we hadn't built the reminder and cancellation paths with the same care we'd put into the checkout page. It's a narrower problem than general SaaS subscription billing architecture for the Philippines market — this is specifically about the one conversion event, not the ongoing monthly cycle.
The Pattern: Authorize First, Charge Later
The mechanic that makes trial-to-paid billing work is the same one behind any token-based subscription system: you collect a payment method once, verify it's valid, and store a reusable reference to it instead of the raw card or e-wallet details. We cover the general version of this — recurring charges driven by a stored authorization token — in a separate piece, because that mechanism also powers ordinary month-to-month billing once a customer is already paying. What's specific to trial conversion is the timing: the authorization happens at signup, the actual capture happens on a schedule, and the two events can be separated by weeks with nothing visibly happening in between.
Three things have to be true for this to work cleanly:
- The payment method is verified as usable at signup, without moving money.
- The token survives long enough — and is still valid — when the trial window closes.
- Nothing fires the real charge if the customer cancelled before the trial ended.
Because this pattern involves holding a payment token and initiating a charge without the customer present at that moment, it only works end to end through a processor actually licensed for it. SMARTPAY operates under BSP OPS and EMI authorization, which is the specific reason we were comfortable building an unattended trial-conversion charge on top of it rather than a workaround.
Authorizing the Payment Method at Signup
At signup, we don't want to move any money — we want proof the payment method is real and capturable later. Most processors support an authorize-only mode for exactly this: the card or e-wallet is checked and a token is issued, but no funds leave the customer's account. Here's roughly what our signup handler looks like:
// Step 1: authorize the payment method at trial signup.
// Verifies the card / e-wallet and returns a reusable token.
// No funds move at this step.
const axios = require('axios');
async function authorizeTrialSignup(customer, paymentMethod) {
const payload = {
merchant_id: process.env.SMARTPAY_MERCHANT_ID,
customer_ref: customer.id,
payment_method: paymentMethod, // token/handle from the checkout SDK
mode: 'authorize_only', // verify, do not capture
amount: 0,
plan_id: customer.planId,
trial_ends_at: customer.trialEndsAt, // ISO timestamp, used for scheduling
sign: generateSignature(customer, paymentMethod)
};
const res = await axios.post(
'https://apix.smartpay.example/api/authorize',
payload
);
if (res.data.status !== 'authorized') {
throw new Error(`Authorization failed: ${res.data.reason}`);
}
// Persist the token - never the underlying card or account number.
await db.subscriptions.update(customer.id, {
payment_token: res.data.token,
token_status: 'active',
trial_ends_at: customer.trialEndsAt,
status: 'trialing'
});
return res.data.token;
}
Two implementation details matter more than they look. First, the token has to be bound to the customer, not just stored as a loose string — if your database is ever exposed, a token that can't be replayed against a different merchant account is far less dangerous than one that can. Second, we deliberately store trial_ends_at on the subscription record at authorization time rather than computing it later, because trial length changes (promotions, extensions, support goodwill) should never silently shift the conversion date for customers who already signed up under the original terms.
Triggering the First Real Charge When the Trial Ends
The conversion charge should never be triggered by anything the customer does — it has to come from a scheduled job that owns the subscription lifecycle. We run this as a daily sweep over subscriptions whose trial window has closed, rather than a single timer per customer, because a sweep is far easier to monitor, re-run, and audit than thousands of independent timers.
// Step 2: convert a trial to a paid subscription.
// Runs from a scheduled job, never from a user-facing request.
async function convertTrialToPaid(subscriptionId) {
const sub = await db.subscriptions.get(subscriptionId);
if (sub.status === 'cancelled') {
console.log(`Subscription ${subscriptionId} cancelled before conversion - skipping`);
return;
}
if (sub.token_status !== 'active') {
await notifyBillingTeam(sub, 'token_invalid_at_conversion');
return;
}
const payload = {
merchant_id: process.env.SMARTPAY_MERCHANT_ID,
customer_ref: sub.customer_id,
payment_token: sub.payment_token,
mode: 'capture',
amount: sub.plan_price,
order_ref: `trial-convert-${subscriptionId}-${Date.now()}`,
sign: generateSignature(sub)
};
const res = await axios.post(
'https://apix.smartpay.example/api/charge',
payload
);
if (res.data.status === 'success') {
await db.subscriptions.update(subscriptionId, { status: 'active_paid' });
await sendReceipt(sub.customer_id, res.data.transaction_id);
} else {
await handleFailedConversion(sub, res.data.reason);
}
}
Notice the cancellation check happens first, before anything touches the token. That ordering is the single most important line in the function. We also treat a failed conversion charge as its own small workflow — retry logic modeled on webhook retry design works well here, since a declined conversion attempt and a failed webhook delivery have the same shape: retry on a backoff schedule, give up after a bounded number of attempts, and hand off to a human path (usually a "update your payment method" email) rather than retrying forever.
Trial-End Reminders: The Difference Between a Renewal and a Chargeback
I underestimated how much weight the reminder email carries until I saw what happened without one. A customer who forgot they signed up, sees an unexpected debit, and doesn't recognize the line item on their statement does one of three things: emails support, disputes the charge with their bank, or both. Any of those outcomes costs more than the subscription is worth for that month, and repeated disputes affect your standing with your payment processor regardless of whether the charge was legitimate.
The fix is mechanically simple but easy to skip under deadline pressure: send a reminder a few days before the trial ends, stating the exact plan, the exact charge date, and a direct link to cancel — not a link to "manage subscription" that requires three more clicks to find the cancel button. We send a second, shorter reminder on the day of conversion itself, timed to land before the charge fires rather than after, since a reminder that arrives alongside or after the receipt reads as an afterthought rather than a notice. Framing the reminder as useful information rather than a retention nudge ("your trial ends on this date and here's what happens next") also measurably reduces how defensive the follow-up support conversations feel, because the customer feels informed rather than caught.
Cancellation Has to Be at Least as Easy as Signup
If signup is a single form and cancellation is a support ticket, that asymmetry is the thing customers remember and complain about publicly, independent of your actual pricing or product quality. We made cancellation self-serve, reachable from the same account page as the upgrade button, and designed to work right up to the edge of the trial window — not just "any time before the trial ends" in the abstract, but specifically handling the case where a cancellation request arrives in the same window the conversion job is running.
That race condition is the part people miss. If a customer clicks cancel at nearly the same moment your scheduled job starts processing their conversion, you need a deterministic answer for which one wins — and it should always be the cancellation. We handle this by having the cancel endpoint set status = 'cancelled' inside the same transaction that checks the current status, and having the conversion job re-read the status immediately before calling the charge API rather than relying on a snapshot taken earlier in the sweep. If a charge does slip through because of a timing edge case, we treat it as an immediate, no-argument refund rather than a case-by-case review — the cost of occasionally refunding a charge that shouldn't have happened is far lower than the cost of a customer who feels billed against their will. The same discipline applies to disputes that do land with the bank instead of with you directly; our approach to handling disputes and chargebacks treats a trial-boundary mistake the same way, as a fast refund rather than a fight.
Trial-to-Paid Flow at a Glance
Here's the sequence we ended up with, including the failure points we specifically designed around:
| Step | What Happens | When | What Can Go Wrong |
|---|---|---|---|
| 1. Signup | Customer enters payment method; method is verified via authorize-only call | Trial day 0 | Invalid or declined method lets a trial start with no usable token |
| 2. Token storage | Reusable token and trial-end date saved against the subscription record | Immediately after authorization | Token stored without expiry tracking; goes stale before conversion |
| 3. Trial usage | Customer uses the product; no billing activity occurs | Trial day 0 through day N-1 | Customer forgets they're on a trial at all |
| 4. Pre-end reminder | Email states plan, charge date, and a direct cancel link | A few days before trial end | Reminder sent too late, or buried link increases support contacts |
| 5. Trial boundary | Cancellation requests and the conversion job can arrive almost simultaneously | Trial end date/time | Race condition charges a customer who cancelled moments earlier |
| 6. Conversion charge | Scheduled job re-checks status, then captures against the stored token | Trial end, via batch job | Token invalid, insufficient funds, or method expired |
| 7. Failure handling | Failed capture triggers retry schedule and a "update payment method" notice | After a failed conversion attempt | Retrying indefinitely instead of escalating to the customer |
| 8. Cancellation at the boundary | Any charge that slipped through a race condition is refunded without review | As soon as detected | Treating it as a dispute instead of an immediate refund damages trust |
Frequently Asked Questions
Do I need to charge something small during the trial just to confirm the card works?
Not if your processor supports a real authorize-only mode. A zero-amount authorization verifies the method is valid and capturable without creating a line item the customer has to mentally reconcile later. Reserve an actual small charge for methods where authorize-only isn't supported, and refund it automatically rather than leaving it to expire on its own.
How many reminders should go out before conversion, and when?
We settled on two: one several days ahead, giving the customer time to act, and one right at the trial boundary confirming what's about to happen. A single early reminder tends to get forgotten; reminders stacked right before the charge with no earlier notice feel like pressure rather than information.
What happens if a cancellation request arrives at the exact moment the trial ends?
Design for this explicitly rather than hoping it's rare. The cancel action and the conversion job both need to check the subscription's live status immediately before acting, and cancellation should always take priority. Treat any charge that slips through as an automatic refund, not a dispute to be argued.
Can the same stored token be reused later for upgrades or plan changes?
Yes — that's the advantage of token-based billing over re-collecting payment details every time. The trial-conversion charge and any later upgrade, downgrade, or regular renewal charge all reference the same stored token unless the customer replaces their payment method.
What if the authorization has expired by the time the trial ends?
This happens more than teams expect, especially with longer trial periods or expired cards. Check token validity ahead of the conversion date, not just on the day itself, so you can prompt the customer to refresh their payment method before the charge attempt rather than after it fails.
Does the flow differ for e-wallets versus cards?
The authorize-then-capture pattern is the same at the architecture level, but the user experience for re-authorizing an expired or revoked e-wallet link often requires the customer to actively reopen the app, which cards don't. Build your failure-handling messaging with that difference in mind rather than sending identical instructions for both.
Should trial-to-paid billing be built differently for a Philippines-facing SaaS product versus a global one?
The core mechanism is identical. What changes locally is which payment methods you're authorizing against — GCash and Maya alongside cards — and making sure the processor underneath actually has the standing to hold a token and settle an unattended charge. That's a licensing question as much as an engineering one, which is part of why we built this on a BSP-regulated processor rather than treating it as a pure integration detail.
If you're building a trial-to-paid flow for a subscription product targeting the Philippines market and want to compare notes on the token lifecycle, reminder timing, or cancellation edge cases, reach out directly — @LouisSMARTPAY or [email protected].