Trials & discounts
Free trials and discounts work for subscriptions, app subscriptions and per subscription of a checkout session. Both are set when the subscription is created.
Free trials
Give a subscription a free trial with trial_days (1–365, counted from the user's confirmation or the checkout) or a fixed trial_ends_at (ISO 8601, in the future) – not both. The subscription starts right away with status trial and nothing billed; the first period is billed when the trial ends.
$subscription = $user->subscriptions()->create([
'name' => 'Pixel Stream Pass',
'unit' => 'month',
'price' => 6.714286,
'trial_days' => 14,
'trial_once' => true,
]);trial_once: truerefuses the trial if the user already had one with your app in the samegroup(422when creating,409 trial_already_usedwhen checking out). In a checkout session only one subscription per group may ask for it.- A few days before the trial ends, Anikeen ID sends
subscription.trial_will_end, e.g. to remind the user;data.payment_method_missingtells you whether the user still has to add a payment method. - Cancelling during the trial bills nothing.
- A
trial_ends_atthat passed before the checkout means no trial.
Trials without a payment method
By default the user needs a payment method to confirm a trial. With trial_payment_method: "optional" (only together with a trial; required is the default) the user can confirm it without one, as long as nothing else has to be paid: in a checkout session every subscription has to be such a trial.
The confirmation page tells the user to add a payment method before the trial ends. If there is still none then, the subscription is cancelled at the trial's end instead of billed:
statuscancelled,cancellation_reasontrial_ended_without_payment_method,subscription.cancelledwithdata.reasonset to the same value, and nosubscription.renewed.
Adding a payment method later doesn't bring it back – create a new subscription. The trial still counts for trial_once. Anikeen ID also mails the user a few days before such a trial ends without a payment method.
use Anikeen\Id\Enums\CancellationReason;
use Anikeen\Id\Enums\TrialPaymentMethod;
use Anikeen\Id\Webhooks\EventType;
$subscription = $user->appSubscriptions()->create([
'group' => $teamId,
'name' => 'Example Pro',
'unit' => 'month',
'price' => 19.00,
'trial_days' => 14,
'trial_payment_method' => TrialPaymentMethod::OPTIONAL,
]);
// in your webhook handler
$data = $event['data'];
match ($event['type']) {
EventType::SUBSCRIPTION_TRIAL_WILL_END => $data['payment_method_missing']
? Reminder::addPaymentMethod($data['object']['id'])
: null,
EventType::SUBSCRIPTION_CANCELLED => $data['reason'] === CancellationReason::TRIAL_ENDED_WITHOUT_PAYMENT_METHOD
? Plan::downgrade($data['object']['id'])
: Plan::revoke($data['object']['id']),
default => null,
};Discounts
A discount (e.g. a coupon) lowers the price of billed periods: every discounted period gets a negative line with the label next to its usage on the same bill, until the discount ends.
$subscription = $user->subscriptions()->create([
'name' => 'Pixel Stream Pass',
'unit' => 'month',
'price' => 6.714286,
'discount' => [
'label' => 'SUMMER20', // shown to the user, e.g. the coupon code
'percent_off' => 20, // or 'amount_off' => 1.50 (net, per period)
'duration' => 'repeating', // once, repeating or forever
'duration_in_periods' => 3, // only for repeating
],
]);
$subscription->discount; // {label, percent_off, amount_off, duration, duration_in_periods, periods_remaining}| Field | |
|---|---|
label | required, at most 100 characters, shown to the user |
percent_off | 0.01–100, of the net price |
amount_off | net, per period, never more than the price (instead of percent_off) |
duration | once (the first billed period), repeating or forever |
duration_in_periods | 1–1200, only for repeating |
- A period is one unit of the subscription (a month for monthly prices).
- A free trial isn't a billed period: the discount starts with the first paid one.
periods_remainingcounts down with every billed period (nullforforever,0once it ended); thensubscription.discount_endedis sent.- A discount can't be combined with a
monthly_cap. - Trusted apps change it on a running subscription with
change. - A resize (
predecessor_id) doesn't carry the discount over: send the successor the rest ("duration": "repeating","duration_in_periods"= the oldperiods_remaining).
In order.* webhooks and app orders every item has discount (null, or {label, percent_off, amount_off} on a discount line), so you can attribute the net amount actually paid per subscription.