> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ionicfi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Recurring billing with Subscriptions

> Attach recurring-price items to a customer and bill automatically each period.

A subscription binds one or more recurring-price items to a customer and bills them automatically on each renewal date. Your backend creates the subscription; Ionic creates each subsequent invoice.

On Node, `npm install @ionicfi/sdk` gives you typed calls for every request
below; the [server SDK](/sdks/server) guide has details. Each example also
shows raw curl.

## Lifecycle

<Steps>
  <Step title="Create the subscription">
    Your server calls `POST /v1/subscriptions` with a customer ID, one or more recurring-price items, and a collection method. An initial invoice is issued immediately unless the subscription opens with a trial.
  </Step>

  <Step title="Trial period (optional)">
    When `trial_end` is set, the subscription starts in `trialing` and no invoice is issued. At `trial_end`, Ionic charges `default_payment_method` and advances the status to `active`.
  </Step>

  <Step title="Automatic renewals">
    At each `current_period_end`, Ionic issues a renewal invoice and advances the period. A failed renewal moves the subscription to `past_due`; exhausting the retry period moves it to `unpaid`.
  </Step>

  <Step title="Gate access with entitlement">
    Call `GET /v1/subscriptions/{id}/entitlement` on each access check. Listen for `subscription.updated` and `subscription.canceled` webhooks to react to state changes.
  </Step>
</Steps>

## Create a subscription

<CodeGroup>
  ```ts TypeScript theme={null}
  import { Ionic } from "@ionicfi/sdk";

  const ionic = new Ionic({ token: process.env.IONIC_SECRET_KEY });

  const subscription = await ionic.subscriptions.create({
    "Idempotency-Key": "sub_create_cus_123",
    customer: "cus_000000000000000000000000",
    items: [
      {
        price: "price_000000000000000000000001",
        quantity: 1,
      },
    ],
    collection_method: "charge_automatically",
    default_payment_method: "pm_000000000000000000000000",
    metadata: {
      plan: "pro",
    },
  });
  ```

  ```bash curl theme={null}
  curl https://api.ionicfi.com/v1/subscriptions \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: sub_create_cus_123" \
    -H "Content-Type: application/json" \
    -d '{
      "customer": "cus_000000000000000000000000",
      "items": [
        {
          "price": "price_000000000000000000000001",
          "quantity": 1
        }
      ],
      "collection_method": "charge_automatically",
      "default_payment_method": "pm_000000000000000000000000",
      "metadata": {
        "plan": "pro"
      }
    }'
  ```
</CodeGroup>

## Request fields

| Field                    | Type    | Notes                                                                                                  |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------ |
| `customer`               | string  | Required. Customer ID (`cus_…`).                                                                       |
| `items`                  | array   | Required. One or more price items. All items must share one currency and one billing cadence.          |
| `collection_method`      | string  | Required. `charge_automatically` or `send_invoice`.                                                    |
| `default_payment_method` | string  | Payment method ID (`pm_…`). Required for `charge_automatically` subscriptions that start with a trial. |
| `trial_start`            | integer | Unix epoch seconds. Start of the trial. Defaults to creation time when `trial_end` is set.             |
| `trial_end`              | integer | Unix epoch seconds. End of the trial. No invoice is issued while `status` is `trialing`.               |
| `metadata`               | object  | Key-value pairs stored on the subscription.                                                            |

## Item fields

| Field      | Type    | Notes                                                                                                  |
| ---------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `price`    | string  | Required. Recurring price ID (`price_…`).                                                              |
| `quantity` | integer | Required. Must be at least 1.                                                                          |
| `id`       | string  | Subscription item ID (`si_…`). Provide when updating an existing item in place via `POST /{id}/items`. |
| `metadata` | object  | Key-value pairs stored on the item.                                                                    |

<Tip>
  For a `charge_automatically` subscription with a trial, provide `default_payment_method` at creation. If it is absent when the trial ends, the renewal invoice cannot be collected automatically.
</Tip>

## The billing cycle

Each subscription exposes these period fields on the response object:

| Field                  | Type    | Notes                                                                          |
| ---------------------- | ------- | ------------------------------------------------------------------------------ |
| `current_period_start` | integer | Unix epoch seconds. Start of the current billing period.                       |
| `current_period_end`   | integer | Unix epoch seconds. End of the current billing period. Renewal occurs here.    |
| `billing_cycle_anchor` | integer | Unix epoch seconds. Reference point used to compute all period boundaries.     |
| `trial_start`          | integer | Unix epoch seconds. Null when no trial.                                        |
| `trial_end`            | integer | Unix epoch seconds. Null when no trial. Automatic billing begins at this time. |

### Status values

| Status       | Meaning                                            |
| ------------ | -------------------------------------------------- |
| `incomplete` | Created; initial invoice not yet paid.             |
| `trialing`   | In free trial; no invoice issued yet.              |
| `active`     | Most recent invoice paid; in good standing.        |
| `past_due`   | Most recent renewal failed; in dunning.            |
| `unpaid`     | Dunning exhausted; service stopped. Terminal.      |
| `canceled`   | Terminated immediately or at period end. Terminal. |

## Cancel a subscription

Pass `at_period_end: true` to let the current period run out before canceling. Omit the field or pass `false` for an immediate cancellation.

<CodeGroup>
  ```ts TypeScript theme={null}
  const canceled = await ionic.subscriptions.cancel({
    id: "sub_000000000000000000000000",
    "Idempotency-Key": "cancel_sub_000000000000000000000000",
    at_period_end: true,
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/subscriptions/sub_000000000000000000000000/cancel \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: cancel_sub_000000000000000000000000" \
    -H "Content-Type: application/json" \
    -d '{
      "at_period_end": true
    }'
  ```
</CodeGroup>

An immediate cancellation transitions the subscription to `canceled`. A period-end cancellation sets `cancel_at_period_end: true` on the subscription; the status remains unchanged until the period ends, when the subscription is canceled automatically.

<Warning>
  A period-end cancellation fires `subscription.updated`, not `subscription.canceled`. Do not revoke access when `cancel_at_period_end` flips to `true`. Revoke access only when you receive `subscription.canceled`.
</Warning>

## Resume a scheduled cancellation

Reverses a period-end cancellation that has not yet taken effect. Only valid when `cancel_at_period_end` is `true`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const resumed = await ionic.subscriptions.resume({
    id: "sub_000000000000000000000000",
    "Idempotency-Key": "resume_sub_000000000000000000000000",
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/subscriptions/sub_000000000000000000000000/resume \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: resume_sub_000000000000000000000000"
  ```
</CodeGroup>

## Update items

Replaces the subscription's billed items. Items with a known `id` (`si_…`) are updated in place; items without an `id` are added as new items. All items must share the subscription's existing currency and billing cadence.

<CodeGroup>
  ```ts TypeScript theme={null}
  const withUpdatedItems = await ionic.subscriptions.updateItems({
    id: "sub_000000000000000000000000",
    "Idempotency-Key": "items_sub_000000000000000000000000_v2",
    items: [
      {
        id: "si_000000000000000000000000",
        price: "price_000000000000000000000001",
        quantity: 2,
      },
      {
        price: "price_000000000000000000000002",
        quantity: 1,
      },
    ],
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/subscriptions/sub_000000000000000000000000/items \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: items_sub_000000000000000000000000_v2" \
    -H "Content-Type: application/json" \
    -d '{
      "items": [
        {
          "id": "si_000000000000000000000000",
          "price": "price_000000000000000000000001",
          "quantity": 2
        },
        {
          "price": "price_000000000000000000000002",
          "quantity": 1
        }
      ]
    }'
  ```
</CodeGroup>

## Update collection method

<CodeGroup>
  ```ts TypeScript theme={null}
  const withNewCollectionMethod = await ionic.subscriptions.updateCollectionMethod({
    id: "sub_000000000000000000000000",
    "Idempotency-Key": "collmethod_sub_000000000000000000000000",
    collection_method: "send_invoice",
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/subscriptions/sub_000000000000000000000000/collection_method \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: collmethod_sub_000000000000000000000000" \
    -H "Content-Type: application/json" \
    -d '{
      "collection_method": "send_invoice"
    }'
  ```
</CodeGroup>

## Update default payment method

Pass `null` for `default_payment_method` to clear it.

<CodeGroup>
  ```ts TypeScript theme={null}
  const withNewDefaultPaymentMethod = await ionic.subscriptions.updateDefaultPaymentMethod({
    id: "sub_000000000000000000000000",
    "Idempotency-Key": "pm_sub_000000000000000000000000",
    default_payment_method: "pm_000000000000000000000001",
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/subscriptions/sub_000000000000000000000000/default_payment_method \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: pm_sub_000000000000000000000000" \
    -H "Content-Type: application/json" \
    -d '{
      "default_payment_method": "pm_000000000000000000000001"
    }'
  ```
</CodeGroup>

## Update metadata

Replaces the metadata map entirely. Keys absent from the request are removed.

<CodeGroup>
  ```ts TypeScript theme={null}
  const withNewMetadata = await ionic.subscriptions.updateMetadata({
    id: "sub_000000000000000000000000",
    "Idempotency-Key": "meta_sub_000000000000000000000000",
    metadata: {
      plan: "pro",
      seat_count: "5",
    },
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/subscriptions/sub_000000000000000000000000/metadata \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: meta_sub_000000000000000000000000" \
    -H "Content-Type: application/json" \
    -d '{
      "metadata": {
        "plan": "pro",
        "seat_count": "5"
      }
    }'
  ```
</CodeGroup>

## Check entitlement

Call the entitlement endpoint to check whether the subscription currently includes access. Use `entitled` as the gate.

<CodeGroup>
  ```ts TypeScript theme={null}
  const entitlement = await ionic.subscriptions.checkEntitlement({
    id: "sub_000000000000000000000000",
  });
  ```

  ```bash curl theme={null}
  curl https://api.ionicfi.com/v1/subscriptions/sub_000000000000000000000000/entitlement \
    -H "Authorization: Bearer sk_v1_test_..."
  ```
</CodeGroup>

```json theme={null}
{
  "entitled": true,
  "reason": "Subscription is active.",
  "access_until": "2026-08-01T00:00:00Z"
}
```

<Warning>
  Gate access on `entitled`, not on `access_until`. The `access_until` timestamp is informational — it can be null or in the past while `entitled` is `true`, for example during a dunning retry or when a renewal sweep is pending.
</Warning>

## Retrieve and list

<CodeGroup>
  ```ts TypeScript theme={null}
  const subscription = await ionic.subscriptions.retrieve({
    id: "sub_000000000000000000000000",
  });

  const subscriptions = await ionic.subscriptions.list({
    customer: "cus_000000000000000000000000",
    status: "active",
    limit: 20,
  });
  ```

  ```bash curl theme={null}
  curl https://api.ionicfi.com/v1/subscriptions/sub_000000000000000000000000 \
    -H "Authorization: Bearer sk_v1_test_..."

  curl "https://api.ionicfi.com/v1/subscriptions?customer=cus_000000000000000000000000&status=active&limit=20" \
    -H "Authorization: Bearer sk_v1_test_..."
  ```
</CodeGroup>

The list endpoint accepts `customer`, `status`, `limit` (default 20, max 100), and `offset`.

## Webhooks

| Event                   | When it fires                                                                                     |
| ----------------------- | ------------------------------------------------------------------------------------------------- |
| `subscription.created`  | Once, when the subscription is first created.                                                     |
| `subscription.updated`  | Any state change: status transition, item update, period advance, or `cancel_at_period_end` flip. |
| `subscription.canceled` | When the subscription reaches a terminal `canceled` state — immediately or at period end.         |

Read `data.object` on every event for the full subscription snapshot. Do not infer access from the event type alone.

<Note>
  A period-end cancellation fires `subscription.updated` with `cancel_at_period_end: true`. The `subscription.canceled` event arrives later, when the period ends. Keep the subscriber's access open between these two events.
</Note>

See [Webhook events](/webhooks/events) for the full event catalog and delivery guarantees.

## Operational guidance

* Always send an `Idempotency-Key` on write requests. Network retries can otherwise create duplicate subscriptions, double item updates, or duplicate cancellations.
* A `409 Conflict` response means the subscription was modified concurrently. Retry the same request with the same `Idempotency-Key`.
* For `charge_automatically` subscriptions, keep `default_payment_method` current. An expired card causes the renewal to fail and moves the subscription to `past_due`.
* Store your plan IDs, seat counts, or feature flags in `metadata`. Update metadata from your application layer; do not derive access decisions from it.
* Use the entitlement endpoint for runtime access checks. Use webhooks to react to state changes asynchronously in your provisioning and access-control systems.
