---
title: "Free trials"
description: "Give a plan a free trial, start trials with or without a card, change or end a trial early, and know exactly what happens to the subscription when it ends."
canonical_url: "https://twominutebilling.com/help/developers/recipes/trials"
---
# Free trials

> Give a plan a free trial, start trials with or without a card, change or end a trial early, and know exactly what happens to the subscription when it ends.

A trial lets a customer use a plan before you charge them. You set a default trial on the price,
override it per subscription when you need to, and tmbilling converts, invoices or cancels the
subscription when the trial ends.

## Before you begin

You need a test API key and your base URL, set up as in the [Quickstart](https://twominutebilling.com/help/developers/getting-started/quickstart).

```bash
export TMBILLING_URL=https://your-api-host
export TMBILLING_API_KEY=sk_test_…
```

```ts [setup.ts]
import { Tmbilling } from "@two-minute-billing/sdk";

const tmb = new Tmbilling({
  apiKey: process.env.TMBILLING_API_KEY!,
  baseUrl: process.env.TMBILLING_URL,
});

// The client does not wrap the price trial fields or the trial-end call yet,
// so those steps call the API directly with this helper.
async function post<T>(path: string, body: unknown): Promise<T> {
  const res = await fetch(`${process.env.TMBILLING_URL}${path}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.TMBILLING_API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return (await res.json()) as T;
}
```

## Give a plan a default trial

Set the trial on the price. Every subscription to that price starts in trial unless you say
otherwise. `trial_end_action` decides what happens when the trial ends:
`activate_subscription` (the default) converts it to a paid subscription, and
`cancel_subscription` ends it.

<code-group>

```ts [TypeScript]
const biz = await tmb.businesses.create({ name: "Fernhill Analytics", default_currency: "USD" });
const product = await tmb.products.create(biz.id, { name: "Pro" });

const price = await post<{ id: string }>(`/v1/businesses/${biz.id}/prices`, {
  product: product.id,
  name: "Pro monthly",
  model: "flat",
  currency: "USD",
  amount_minor: 2900,
  interval: "month",
  trial_period: 14,
  trial_period_unit: "day",
  trial_end_action: "activate_subscription",
});
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Fernhill Analytics", "default_currency": "USD" }'

export BIZ=biz_…

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/products \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Pro" }'

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/prices \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "pro",
    "name": "Pro monthly",
    "model": "flat",
    "currency": "USD",
    "amount_minor": 2900,
    "interval": "month",
    "trial_period": 14,
    "trial_period_unit": "day",
    "trial_end_action": "activate_subscription"
  }'
```

</code-group>

```json [Response]
{
  "object": "price",
  "id": "pro-monthly",
  "product": "pro",
  "name": "Pro monthly",
  "model": "flat",
  "currency": "USD",
  "amount_minor": 2900,
  "interval": "month",
  "trial_period": 14,
  "trial_period_unit": "day",
  "trial_end_action": "activate_subscription"
}
```

## Start a trial without a card

Create the customer and subscribe them. You do not need a payment method to start a trial:
nothing is charged until it ends.

<code-group>

```ts [TypeScript]
const customer = await tmb.customers.create(biz.id, {
  email: "maya@example.com",
  first_name: "Maya",
  last_name: "Castillo",
  currency: "USD",
});

const trial = await tmb.subscriptions.create(biz.id, customer.id, {
  items: [{ price: price.id }],
});
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "maya@example.com",
    "first_name": "Maya",
    "last_name": "Castillo",
    "currency": "USD"
  }'

export CUS=cus_…

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/subscriptions \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "items": [{ "price": "pro-monthly" }] }'

export SUB=sub_…
```

</code-group>

```json [Response]
{
  "object": "subscription",
  "id": "sub_XXXXXXXXXXXXXXXXXXXXXX01",
  "status": "in_trial",
  "customer": "cus_XXXXXXXXXXXXXXXXXXXXXX01",
  "trial_start": "2026-10-20T19:08:21.726Z",
  "trial_end": "2026-11-03T19:08:21.726Z",
  "current_period_start": null,
  "current_period_end": null,
  "mrr": 0
}
```

The subscription is `in_trial`, with no invoice. `trial_end` is when it converts.

<warning title="Collect a card before the trial ends">

With auto-collection on, a trial that ends with no payment method on file does not convert:
tmbilling cancels it with `cancel_reason: "no_card"`, and raises no invoice. Send your customer to
[hosted checkout or the portal](https://twominutebilling.com/help/developers/recipes/checkout-and-portal) during the trial.

</warning>

## Save the customer's card

In test mode, save a test card. In live mode the card comes from your checkout or portal through
your payment gateway.

<code-group>

```ts [TypeScript]
await tmb.paymentMethods.create(biz.id, customer.id, { token: "4242424242424242" });
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/payment_methods \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "token": "4242424242424242" }'
```

</code-group>

The trial keeps running. tmbilling charges this card when it ends.

## End a trial early

When a customer wants to upgrade now, end the trial. tmbilling activates the subscription, raises
the first invoice and collects it, in the same call.

<code-group>

```ts [TypeScript]
const ended = await post<{ status: string }>(
  `/v1/businesses/${biz.id}/subscriptions/${trial.id}/edit_trial_end`,
  { immediately: true },
);
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/subscriptions/$SUB/edit_trial_end \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "immediately": true }'
```

</code-group>

```json [Response]
{
  "object": "subscription",
  "id": "sub_XXXXXXXXXXXXXXXXXXXXXX01",
  "status": "active",
  "trial_start": "2026-10-20T19:08:21.726Z",
  "trial_end": "2026-10-20T19:08:22.204Z",
  "current_period_start": "2026-10-20T19:08:22.204Z",
  "current_period_end": "2026-11-20T19:08:22.204Z",
  "mrr": 2900
}
```

The response is the subscription. List the customer's invoices to see the first invoice, `paid`.

To move the end date instead, send `{ "trial_end": "<ISO 8601 date-time>" }` to the same
endpoint. The new end must be in the future. A later date extends the trial, an earlier one
shortens it.

## Override the trial per subscription

The price's trial is a default. Change it when you create the subscription:

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        trial_days: 30
      </code>
    </td>
    
    <td>
      A 30-day trial in place of the price's trial.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        trial_days: 0
      </code>
    </td>
    
    <td>
      No trial: tmbilling invoices and charges now. Needs a payment method when auto-collection is on.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        trial_end
      </code>
    </td>
    
    <td>
      An exact trial end date-time, in place of <code>
        trial_days
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      neither
    </td>
    
    <td>
      The price's <code>
        trial_period
      </code>
      
      , if it has one.
    </td>
  </tr>
</tbody>
</table>

Here a returning customer, who already has a card on file, skips the trial:

<code-group>

```ts [TypeScript]
const paid = await tmb.subscriptions.create(biz.id, customer.id, {
  items: [{ price: price.id }],
  trial_days: 0,
});
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/subscriptions \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "items": [{ "price": "pro-monthly" }], "trial_days": 0 }'
```

</code-group>

```json [Response]
{
  "object": "subscription",
  "id": "sub_XXXXXXXXXXXXXXXXXXXXXX02",
  "status": "active",
  "customer": "cus_XXXXXXXXXXXXXXXXXXXXXX01",
  "trial_start": null,
  "trial_end": null,
  "current_period_start": "2026-10-20T19:08:22.622Z",
  "current_period_end": "2026-11-20T19:08:22.622Z",
  "mrr": 2900,
  "latest_invoice": {
    "id": "inv_XXXXXXXXXXXXXXXXXXXXXX01",
    "status": "paid",
    "total_minor": 2900
  }
}
```

## Know what happens when a trial ends

The billing clock ends the trial at `trial_end`. What it does depends on the price and on the
customer:

<table>
<thead>
  <tr>
    <th>
      Situation
    </th>
    
    <th>
      Result
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        trial_end_action: activate_subscription
      </code>
      
      , card on file
    </td>
    
    <td>
      The subscription becomes <code>
        active
      </code>
      
      . tmbilling raises the first invoice and charges the card.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        trial_end_action: activate_subscription
      </code>
      
      , auto-collection off
    </td>
    
    <td>
      The subscription becomes <code>
        active
      </code>
      
      . tmbilling raises the first invoice and sends it, with your payment terms.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        trial_end_action: activate_subscription
      </code>
      
      , auto-collection on, no payment method
    </td>
    
    <td>
      The subscription is <code>
        cancelled
      </code>
      
       with <code>
        cancel_reason: "no_card"
      </code>
      
      . No invoice.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        trial_end_action: cancel_subscription
      </code>
    </td>
    
    <td>
      The subscription is <code>
        cancelled
      </code>
      
       with <code>
        cancel_reason: "trial_ended"
      </code>
      
      , card or no card.
    </td>
  </tr>
</tbody>
</table>

A failed first charge follows your [dunning](https://twominutebilling.com/help/developers/recipes/failed-payments) settings,
like any other invoice.

Listen for these [events](https://twominutebilling.com/help/developers/webhooks/events):

<table>
<thead>
  <tr>
    <th>
      Event
    </th>
    
    <th>
      When
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        subscription_trial_ending
      </code>
    </td>
    
    <td>
      3 days before <code>
        trial_end
      </code>
      
      . Remind the customer, or ask for a card.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        subscription_activated
      </code>
    </td>
    
    <td>
      The trial converted.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        invoice_generated
      </code>
      
      , <code>
        invoice_paid
      </code>
    </td>
    
    <td>
      The first invoice was raised and paid.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        subscription_canceled
      </code>
    </td>
    
    <td>
      The trial ended without converting. Read <code>
        cancel_reason
      </code>
      
      .
    </td>
  </tr>
</tbody>
</table>

## Test the end of a trial

You cannot wait 14 days in a test. Use Time Travel in the dashboard: open your test business,
start **Time Travel**, and move the clock past `trial_end`. tmbilling runs everything the billing
clock would do in that time. See [Test with Time Travel](https://twominutebilling.com/help/guides/test-with-time-travel).

Time Travel is a dashboard tool. An integration's `sk_test_` key cannot move the clock.

## Check it worked

- `GET /v1/businesses/{biz}/subscriptions/{sub}` shows `status: "in_trial"` and the `trial_end`
you expect.
- No invoice exists for the subscription until the trial ends or you end it.
- After the trial, the first invoice is `paid` (card on file) or `posted` and waiting for
payment (auto-collection off).

## Common errors

<table>
<thead>
  <tr>
    <th>
      Status
    </th>
    
    <th>
      Message
    </th>
    
    <th>
      Fix
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      400
    </td>
    
    <td>
      <code>
        You can change the trial end only on an in-trial subscription. This subscription is active.
      </code>
    </td>
    
    <td>
      The trial already ended. Change the term instead.
    </td>
  </tr>
  
  <tr>
    <td>
      400
    </td>
    
    <td>
      <code>
        We cannot end the trial. This subscription auto-collects, and the customer has no payment method on file. Add a payment method, or disable auto-collection to send the first invoice instead.
      </code>
    </td>
    
    <td>
      Save a card first.
    </td>
  </tr>
  
  <tr>
    <td>
      400
    </td>
    
    <td>
      <code>
        Provide exactly one of trial_end or immediately
      </code>
    </td>
    
    <td>
      Send one of the two fields, not both.
    </td>
  </tr>
  
  <tr>
    <td>
      400
    </td>
    
    <td>
      <code>
        trial_end must be in the future
      </code>
    </td>
    
    <td>
      Send a later date-time.
    </td>
  </tr>
  
  <tr>
    <td>
      400
    </td>
    
    <td>
      <code>
        Auto-collection is on, but the customer has no usable payment method. Add a card first, or create with auto_collection=off.
      </code>
    </td>
    
    <td>
      You created a subscription with no trial (<code>
        trial_days: 0
      </code>
      
      ) and no card.
    </td>
  </tr>
</tbody>
</table>

## Related

- [Subscription management features](https://twominutebilling.com/product/subscriptions)


## Sitemap

See the full [sitemap](https://twominutebilling.com/sitemap.md) for all pages.
