# Subscriptions (/developers/subscriptions)

A subscription charges the payer the same price every 30 or 365 days, on Solana. Your server starts
it with a checkout session in `mode: subscription`, Fianto charges each renewal, and your server
follows along through webhooks. Subscriptions need the API: the dashboard alone cannot sell them.

**The life of a subscription**

1. A subscription starts `ACTIVE`; the first period is charged inside the subscribe transaction.
2. Fianto charges each renewal every 30 days or every 365 days, as the price sets.
3. The first counted failed renewal moves it to `PAST_DUE`; a later successful retry moves it back to `ACTIVE`.
4. The 4th counted failure ends it: `ENDED` with reason `PAYMENT_FAILED`.
5. From `ACTIVE` or `PAST_DUE`, a payer cancel ends it at the current period end with reason `PAYER_CANCELED`; a merchant cancel ends it now or at period end with reason `MERCHANT_CANCELED`: Fianto stops charging (a charge already in flight can still land), but the payer's on-chain subscription stays live.
6. Re-subscribing ends the old subscription with `PAYER_CANCELED`.
7. No back-billing: missed periods are never billed.

## Before you start [#before-you-start]

* Your merchant account is approved, and you have an application's app id and secret.
* You can already create checkout sessions. See [Checkout sessions](/developers/checkout-sessions).
* You have a webhook route. Subscriptions are followed only through events and reads; a
  subscription session creates no order.

## Steps [#steps]

**Step 1.**

### Create a recurring price in the dashboard [#create-a-recurring-price-in-the-dashboard]

Add a product and give it a recurring price. A recurring price charges every 30 days or every
365 days. Prices are never edited: to change one, add a new price and archive the old one. Copy the
price's id (`fian_price_…`) from the prices table.

**Step 2.**

### Create a subscription session [#create-a-subscription-session]

Send `mode: subscription` and the recurring `price_id`. Leave out `amount` and `description`: the
price sets the amount, and its product's name becomes the session's description. `order_id`,
`success_url` and `cancel_url` are required, as for a one-time payment.

#### curl

```bash
curl https://api.fianto.xyz/v1/checkout-sessions \
  --user "$FIANTO_APP_ID:$FIANTO_APP_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: signup-user_42-1" \
  -d '{
    "mode": "subscription",
    "order_id": "signup_user_42",
    "price_id": "fian_price_...",
    "success_url": "https://shop.example/welcome",
    "cancel_url": "https://shop.example/pricing"
  }'
```

#### SDK

```ts
import { Fianto } from '@fianto/sdk';

const fianto = new Fianto(); // reads FIANTO_APP_ID and FIANTO_APP_SECRET

export async function startSubscription(userId: string, priceId: string) {
  const session = await fianto.checkoutSessions.create({
    mode: 'subscription',
    order_id: `signup_${userId}`,
    price_id: priceId,
    success_url: 'https://shop.example/welcome',
    cancel_url: 'https://shop.example/pricing',
  });
  return session.url; // send the payer here
}
```

The `url`, reissue, cancel and redirect or popup rules are the same as for
[one-time sessions](/developers/checkout-sessions).

**Step 3.**

### Let the plan get ready [#let-the-plan-get-ready]

Each price gets a plan on Solana, created by the first subscription session for that price. Fianto
signs that transaction and pays its rent. Until the plan is on chain, the hosted checkout page shows
it as preparing (`plan_status: preparing`) and no pay button. Fianto retries a failed plan creation
after 30 seconds, 2 minutes, 10 minutes and 1 hour, then marks it `failed`.

A plan is tied to the price, your receiving wallet and the service fee. If the fee or your wallet
changes, new subscribers get a new plan; existing subscribers keep paying the old wallet and fee.
Fianto creates at most 50 new plans per merchant in any 24 hours.

**Step 4.**

### Follow the subscription through webhooks [#follow-the-subscription-through-webhooks]

Fianto sends `subscription.created` when the subscribe transaction finalizes. Give access there.
Then keep your records in step with the other events:

| Event                           | When                                                           |
| ------------------------------- | -------------------------------------------------------------- |
| `subscription.renewed`          | A renewal was charged                                          |
| `subscription.payment_failed`   | A renewal charge had a counted failure                         |
| `subscription.past_due`         | The first counted failure moved the subscription to `PAST_DUE` |
| `subscription.cancel_scheduled` | A cancel at the end of the period was scheduled                |
| `subscription.cancel_withdrawn` | A scheduled cancel was withdrawn                               |
| `subscription.ended`            | The subscription ended                                         |

A subscription is `ACTIVE`, `PAST_DUE` or `ENDED`. It moves between `ACTIVE` and `PAST_DUE` as
renewals fail and succeed, and it ends when the payer cancels, you cancel, or the 4th counted renewal
failure ends it with `end_reason` `PAYMENT_FAILED`. `ENDED` is final.

**Step 5.**

### Cancel from your server [#cancel-from-your-server]

Cancel with `POST /v1/subscriptions/{id}/cancel` and `at`:

* **`now`** ends the subscription at once and sends `subscription.ended`.
* **`period_end`** stops it at the end of the current period and sends
  `subscription.cancel_scheduled`, unless a cancel is already scheduled: then it sends no second
  event. If the payer scheduled that cancel, your request still sets `merchant_cancel_requested` to
  `true` (the `cancel_reason` stays `PAYER_CANCELED`), so your cancel stands even if the payer
  resumes the subscription on Solana.

#### curl

```bash
curl https://api.fianto.xyz/v1/subscriptions/SUBSCRIPTION_ID/cancel \
  --user "$FIANTO_APP_ID:$FIANTO_APP_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-SUBSCRIPTION_ID-1" \
  -d '{ "at": "period_end" }'
```

#### SDK

```ts
import { Fianto } from '@fianto/sdk';

const fianto = new Fianto();

export async function cancelAtPeriodEnd(subscriptionId: string) {
  return fianto.subscriptions.cancel(subscriptionId, { at: 'period_end' });
}
```

> **A cancel is final and changes nothing on Solana:**
>
> A cancel only stops Fianto from charging. It cannot be undone, and it does not end the payer's
> subscription on Solana, which stays live. With `now`, a renewal charge already in flight can still
> land. There is no refund API: refund a payer by sending USDC from your wallet yourself.

What the payer sees after your cancel: while a `period_end` cancel is pending, the payer portal offers
no cancel of its own. Because the on-chain subscription stays live, the same wallet cannot subscribe
to the same plan again until the payer uses "Cancel on Solana" in the portal, waits for the on-chain
end and reclaims the rent.

> **What the payer pays:**
>
> When subscribing, the payer pays the first period's price, plus the service fee when it is above
> zero, the network fee, and the rent for the accounts the subscribe transaction creates. The rent is
> read from Solana at subscribe time. Each renewal takes the price again, plus the service fee when it
> is above zero; Fianto pays the network fee for renewals.

## Check it worked [#check-it-worked]

> After a payer subscribes and the transaction finalizes, your webhook route receives
> `subscription.created`, and the subscription is listed by `GET /v1/subscriptions`:
>
> ```bash
> curl "https://api.fianto.xyz/v1/subscriptions?limit=5" \
>   --user "$FIANTO_APP_ID:$FIANTO_APP_SECRET"
> ```
>
> Its `status` is `ACTIVE`. After a cancel at `period_end`, `cancel_at_period_end` is `true`; after a
> cancel at `now`, `status` is `ENDED`.

## Troubleshooting [#troubleshooting]

| You see | Why | Fix |
|---|---|---|
| 400 `price_id_required` | A subscription session was sent without `price_id`. | Send the recurring price's `fian_price_…` id. |
| 400 `amount_not_allowed` | The session sent `amount` or `description`. | Remove both; the price and its product set them. |
| 422 `price_not_recurring` | The price is one-time. | Use a recurring price, or send `mode: payment`. |
| 429 `plan_limit_reached` | The session needed a new plan and your account already created 50 new plans in the last 24 hours. | Try again later. |
| The checkout page shows the plan as preparing (`subscription_preparing`) | The first session for this price is creating its plan on Solana. | Wait. The payer can pay once the plan is on chain. |
| The checkout page cannot start the subscription (`subscription_plan_failed`) | Creating the plan failed after every retry. | Create a new subscription checkout session for the price: it starts a new plan attempt. If that fails too, write to support@fianto.xyz. |
| 409 `subscription_already_ended` | You cancelled a subscription that has already ended. | Nothing to do. Read it to see its `end_reason`. |
| 400 `validation_failed` with `field: "at"` | `at` is missing, or is not `now` or `period_end`. | Send one of the two. |

## See also [#see-also]

- [Subscriptions quickstart](/get-started/subscriptions-quickstart): The same flow with the SDK route helpers.

- [Webhook events](/developers/webhooks/events): What each subscription event carries.

- [Checkout sessions](/developers/checkout-sessions): The session rules subscriptions share.