# Free trials > Offer a free trial, grant access from webhooks, and collect the first payment when the trial ends. Source: https://abuna.app/docs/guides/trials Give customers free access before their first paid billing period, without charging them at checkout. ## Set a trial on a price Create a price with `trial_days`, an integer from 1 to 365. This example offers 14 days free, then FCFA 5,000 a month. **Create a price with a 14-day trial** cURL: ```bash curl https://api.abuna.app/v1/apps/8ddhXCDW/prices \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "product_id": "pR4dWx9K", "name": "Monthly", "currency": "XAF", "amount": 5000, "interval": "month", "trial_days": 14 }' ``` Node.js: ```javascript const response = await fetch("https://api.abuna.app/v1/apps/8ddhXCDW/prices", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json" }, body: JSON.stringify({ product_id: "pR4dWx9K", name: "Monthly", currency: "XAF", amount: 5000, interval: "month", trial_days: 14, }), }); const price = await response.json(); ``` Python: ```python import requests response = requests.post( "https://api.abuna.app/v1/apps/8ddhXCDW/prices", headers={"Authorization": "Bearer sk_test_..."}, json={ "product_id": "pR4dWx9K", "name": "Monthly", "currency": "XAF", "amount": 5000, "interval": "month", "trial_days": 14, }, ) price = response.json() ``` PHP: ```php true, CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_...", "Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode([ "product_id" => "pR4dWx9K", "name" => "Monthly", "currency" => "XAF", "amount" => 5000, "interval" => "month", "trial_days" => 14, ]), CURLOPT_RETURNTRANSFER => true, ]); $price = json_decode(curl_exec($ch), true); ``` Go: ```go body, _ := json.Marshal(map[string]any{ "product_id": "pR4dWx9K", "name": "Monthly", "currency": "XAF", "amount": 5000, "interval": "month", "trial_days": 14, }) req, _ := http.NewRequest("POST", "https://api.abuna.app/v1/apps/8ddhXCDW/prices", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer sk_test_...") req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var price map[string]any json.NewDecoder(res.Body).Decode(&price) ``` Omit `trial_days` or send `null` for no trial. The trial length cannot change after the price is created. Create another price for a different default. ## Send the customer to checkout Create a [checkout session](https://abuna.app/docs/api/sessions.md#create-checkout) from your server, then send the customer to its `url`. Replace the example IDs and secret key with your Test values. **Create a checkout using the price's trial** cURL: ```bash curl https://api.abuna.app/v1/apps/8ddhXCDW/sessions \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: checkout-user-42-trial-1" \ -d '{ "type": "checkout", "price_id": "Zt6YbN3q", "customer_id": "Cu5tM8rA", "success_url": "https://example.com/subscribed?session_id={SESSION_ID}", "cancel_url": "https://example.com/plans", "metadata": {"user_id": "42"} }' ``` Node.js: ```javascript const response = await fetch("https://api.abuna.app/v1/apps/8ddhXCDW/sessions", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", "Idempotency-Key": "checkout-user-42-trial-1", }, body: JSON.stringify({ type: "checkout", price_id: "Zt6YbN3q", customer_id: "Cu5tM8rA", success_url: "https://example.com/subscribed?session_id={SESSION_ID}", cancel_url: "https://example.com/plans", metadata: { user_id: "42" }, }), }); const session = await response.json(); ``` Python: ```python import requests response = requests.post( "https://api.abuna.app/v1/apps/8ddhXCDW/sessions", headers={"Authorization": "Bearer sk_test_...", "Idempotency-Key": "checkout-user-42-trial-1"}, json={ "type": "checkout", "price_id": "Zt6YbN3q", "customer_id": "Cu5tM8rA", "success_url": "https://example.com/subscribed?session_id={SESSION_ID}", "cancel_url": "https://example.com/plans", "metadata": {"user_id": "42"}, }, ) session = response.json() ``` PHP: ```php true, CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_...", "Content-Type: application/json", "Idempotency-Key: checkout-user-42-trial-1"], CURLOPT_POSTFIELDS => json_encode([ "type" => "checkout", "price_id" => "Zt6YbN3q", "customer_id" => "Cu5tM8rA", "success_url" => "https://example.com/subscribed?session_id={SESSION_ID}", "cancel_url" => "https://example.com/plans", "metadata" => ["user_id" => "42"], ]), CURLOPT_RETURNTRANSFER => true, ]); $session = json_decode(curl_exec($ch), true); ``` Go: ```go body, _ := json.Marshal(map[string]any{ "type": "checkout", "price_id": "Zt6YbN3q", "customer_id": "Cu5tM8rA", "success_url": "https://example.com/subscribed?session_id={SESSION_ID}", "cancel_url": "https://example.com/plans", "metadata": map[string]any{"user_id": "42"}, }) req, _ := http.NewRequest("POST", "https://api.abuna.app/v1/apps/8ddhXCDW/sessions", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer sk_test_...") req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", "checkout-user-42-trial-1") res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var session map[string]any json.NewDecoder(res.Body).Decode(&session) ``` The customer confirms their contact details, chooses reminders and agrees to start the trial. Abuna then emails them a 6-digit code, and the trial starts once they enter it on the checkout page. Until then, no subscription exists and no trial is used. Nothing is charged. The checkout completes and takes them to your `success_url`, with the usual reminder-channel setup. | Checkout or subscription request | Trial length | | --- | --- | | Omit `trial_days` | Use the price's default. | | `trial_days: 1` to `365` | Use this many days instead. | | `trial_days: 0` | No trial. The usual first payment applies. | > **Note:** A customer gets one trial per product in an app, across all its prices, even after canceling. If they already used it, Abuna starts the usual paid subscription without an error or another trial. ## Start a trial without checkout For an existing customer, [create a subscription](https://abuna.app/docs/api/subscriptions.md#create) directly. This example overrides the price's default with 7 days. **Create a subscription with a 7-day trial** cURL: ```bash curl https://api.abuna.app/v1/apps/8ddhXCDW/subscriptions \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: subscription-user-42-trial-1" \ -d '{ "customer_id": "Cu5tM8rA", "price_id": "Zt6YbN3q", "trial_days": 7, "metadata": {"user_id": "42"} }' ``` Node.js: ```javascript const response = await fetch("https://api.abuna.app/v1/apps/8ddhXCDW/subscriptions", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", "Idempotency-Key": "subscription-user-42-trial-1", }, body: JSON.stringify({ customer_id: "Cu5tM8rA", price_id: "Zt6YbN3q", trial_days: 7, metadata: { user_id: "42" }, }), }); const result = await response.json(); ``` Python: ```python import requests response = requests.post( "https://api.abuna.app/v1/apps/8ddhXCDW/subscriptions", headers={"Authorization": "Bearer sk_test_...", "Idempotency-Key": "subscription-user-42-trial-1"}, json={ "customer_id": "Cu5tM8rA", "price_id": "Zt6YbN3q", "trial_days": 7, "metadata": {"user_id": "42"}, }, ) result = response.json() ``` PHP: ```php true, CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_...", "Content-Type: application/json", "Idempotency-Key: subscription-user-42-trial-1"], CURLOPT_POSTFIELDS => json_encode([ "customer_id" => "Cu5tM8rA", "price_id" => "Zt6YbN3q", "trial_days" => 7, "metadata" => ["user_id" => "42"], ]), CURLOPT_RETURNTRANSFER => true, ]); $result = json_decode(curl_exec($ch), true); ``` Go: ```go body, _ := json.Marshal(map[string]any{ "customer_id": "Cu5tM8rA", "price_id": "Zt6YbN3q", "trial_days": 7, "metadata": map[string]any{"user_id": "42"}, }) req, _ := http.NewRequest("POST", "https://api.abuna.app/v1/apps/8ddhXCDW/subscriptions", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer sk_test_...") req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", "subscription-user-42-trial-1") res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var result map[string]any json.NewDecoder(res.Body).Decode(&result) ``` The subscription starts with `status: trialing`. Its `trial_ends_at` is the end of the trial, in Unix seconds. Without a trial, that field is `null`. **Subscription during a 7-day trial** ```json { "id": "x9QbL2sK", "app_id": "8ddhXCDW", "customer_id": "Cu5tM8rA", "price_id": "Zt6YbN3q", "status": "trialing", "starts_at": 1727600000, "activated_at": 1727600000, "trial_ends_at": 1728204800, "canceled_at": null, "cancel_at": null, "cancel_at_period_end": false, "current_period_start": 1727600000, "current_period_end": 1728204800, "ended_reason": null, "metadata": { "user_id": "42" }, "created_at": 1727600000, "manage_token": "8c1f4a7d2e9b3c6f0a5d8e1b4c7f2a9d3e6b" } ``` ## Grant access from webhooks [Verify webhook signatures](https://abuna.app/docs/guides/webhooks.md#verify-the-signature) before using an event. Use the subscription in the event, not the browser redirect, to decide access. - `subscription.created` is sent when a trial starts. A trial checkout also sends `session.completed`. Grant access when `data.subscription.status` is `trialing`; `data.subscription.trial_ends_at` is when the trial ends. - `subscription.paid` is sent once the customer pays the first invoice, during or after the trial. - `subscription.trial_ending` tells you the trial is ending, at your app's normal renewal reminder lead time. If you set up your webhook URL before free trials existed, turn this event on first: open **Webhooks** in the dashboard and edit **Events**. - Keep access during `active` and `past_due`. Stop access on `subscription.canceled`. ## The first payment The first paid period starts at `trial_ends_at`, even if the customer pays early. Abuna sends its invoice and pay link using your app's renewal lead time. The pay-by date is normally the trial end plus your app's `grace_period_days`, capped at the paid period's end. Use the invoice's `due_at` for the exact deadline, including when the invoice arrives later. Customers get 3 days to pay by default; you can set 1 to 14. Nothing is taken automatically. If the invoice remains unpaid, the subscription follows the [usual unpaid renewal rules](https://abuna.app/docs/api/subscriptions.md#past-due), including cancellation after the pay-by date. ## Cancel during a trial A customer who cancels during the trial keeps access until the trial ends. Any unpaid first invoice is canceled, so nothing remains to pay. A scheduled cancel sets `cancel_at` to `trial_ends_at`. From the [cancel API](https://abuna.app/docs/api/subscriptions.md#cancel), `at_period_end: true` schedules that same end. Omit it or send `false` to end access immediately. ## Test and Live limits Customers with a `trialing` subscription, which starts once they confirm their email, count toward your team's Live subscriber limit. Test mode has no subscriber limit.