Dashboard

How to Add a Trial Period to an AI-Built App

How to add a trial period to an AI-built app, including the four expiry edge cases agent-written billing code misses and the settings that fix them.

Steve Jefferson
Steve Jefferson
Developer Advocate
13 October 20261 min read

Adding a trial period to an AI-built app is four lines of happy path and six edge cases. The happy path is a date in the future and a check against it. The edge cases are what happens when the date arrives and nobody has entered a card, when the first real invoice covers a strange period, when the reminder fires, and when a determined user signs up again. Agent-generated billing code usually gets the first part right and leaves the rest.

Work the edge cases first and the happy path writes itself.

Decide the trial's shape before you write anything

Three decisions, in this order, because each one constrains the next.

  1. Card up front, or not. Requiring a card cuts signups and raises conversion among the people who remain. Not requiring one gives you more users and a harder conversion moment. Both are defensible. Choosing by accident is not.

  2. Time or usage. A fourteen-day trial is easy to explain and easy to waste. A trial capped at a number of runs matches value to consumption and is fairer for AI features where each run costs you money. Usage caps need the metering described in capping AI usage per customer.

  3. What happens at the end. Read-only, locked, or deleted. Read-only is almost always right: people come back, and data they can still see is a reason to return.

How to add a trial period to an AI-built app: the server-side check

The check itself is trivial:

sql
-- on the account row
trial_ends_at timestamptz,
plan text not null default 'trial'
python
def can_use_feature(account):
    if account.plan == "paid":
        return True
    return account.trial_ends_at is not None and account.trial_ends_at > now()

The mistake is where this runs. An agent asked to "add a trial" will often gate the user interface, hiding the button when the trial expires. That is presentation, not enforcement. The check has to run on the server, in the endpoint that performs the work, because the expensive part of an AI feature is the API call behind the button and anyone can call the endpoint directly.

Make the trial check part of the same function that already checks authentication, so a new endpoint cannot forget it.

The four edge cases that decide whether this works

No payment method when the trial ends

This is the big one, and it has a real setting rather than an opinion. Stripe's trials documentation describes trial_settings.end_behavior.missing_payment_method with three options: create_invoice, the default, which raises an invoice and moves the subscription to past_due if it cannot be paid; cancel, which cancels the subscription; and pause, which pauses it until you resume.

Pick to match your product rather than taking the default. For a tool holding the customer's work, pause keeps the account recoverable. For a one-off utility, cancel is cleaner than a trail of unpaid invoices. The default, create_invoice, is the one that generates dunning email to people who never gave you a card.

The first invoice covers an odd period

When a trial ends, the billing cycle anchor moves by default. The same page states that billing_cycle_anchor resets to the moment the trial completes, generating a full invoice with no proration, while unchanged keeps the original anchor and prorates the gap. The worked example is a subscription created on 1 January with a seven-day trial: the trial ends on 8 January, the anchor resets to 8 January, and the invoice covers 8 January to 8 February at full price.

If your customer expects to be billed on the first of the month, you want unchanged and a prorated first invoice. If you want a clean full month from the day they convert, take the default. Either is fine. A support ticket asking why the first charge is 11.40 is what happens when nobody chose.

The reminder

Stripe sends customer.subscription.trial_will_end according to the Dashboard's trial ending setting, which the documentation says defaults to three days before the trial ends, and fires immediately if the trial is shorter than that interval. It also notes that Stripe-hosted reminder emails go out seven days before.

Three days is the right trigger for your own message. Send one email that says what happens on which date and what they will be charged. Do not send three.

Signing up again for another trial

Some abuse is tolerable and chasing it is not free. The cheap mitigations, in order of effort: unique index on email, block the plus-addressing trick if it matters to you, and require a card for a second trial on the same payment fingerprint. Blocking by IP address punishes offices and shared networks, so avoid it.

Status and what the customer sees

If a subscription contains only trial items priced at zero, Stripe reports its status as trialing, which is what a no-card-required flow looks like. With a paid trial or a regular priced item, the status is active, incomplete or past_due as normal. That distinction decides what your webhook handler branches on, so read the status rather than inferring from the price.

One integration note worth knowing before you choose an approach: the documentation says Stripe Checkout does not support the newer trial offers and directs you to legacy free trials with trial_end there, so the hosted checkout path and the API path are not interchangeable.

Whatever the backend says, the account page should show the trial end date as a date, not as "7 days left". People plan around dates. A countdown that reads 0 days left is ambiguous in both directions.

The webhook handler

Event

What it means

What to do

customer.subscription.trial_will_end

Reminder window reached

Send your one email with the date and the amount

customer.subscription.updated

Trial transitioned to the real price

Update the stored plan and the expiry

customer.subscription.paused

Trial ended with no payment method, pause behaviour

Revoke access, keep the data

customer.subscription.deleted

Subscription ended

Revoke access, start your retention clock

Write the plan state to your own database from these events rather than asking the payment provider on every request. Your app should keep working when their API is slow, and a cached plan with a webhook-driven update is how that happens.

The payment plumbing underneath all this, including webhook verification and idempotency, is covered in adding payments to an AI-built app. If you are also running promotional pricing, keep it separate from trial logic, for reasons set out in adding discount codes safely.

Before you ship it

  • Set a trial end date in the past on a test account and confirm the feature is blocked at the endpoint, not just hidden in the interface

  • Let one trial run to its real end in test mode and read the resulting invoice

  • Confirm the reminder email contains a date and an amount

  • Check what an expired user sees, and whether they can still read their own data

  • Make sure your own AI costs during a trial are capped, because an unlimited trial on a usage-priced model is a bill with no ceiling

Whether to offer a free tier alongside the trial is a different question with different consequences, and it is worth answering before you build either. Does an AI app need a free tier covers that decision, and the broader build sequence is in the guide to building an app with AI.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.