# Subscriptions quickstart (/get-started/subscriptions-quickstart)

A subscription uses the same three pieces as a one-time payment: a checkout route, a button and a
webhook route. Only the session and the webhook events change.

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

* You finished the [quickstart](/get-started/quickstart): your checkout route, button and webhook
  route work.
* Your merchant account is approved.

## Steps [#steps]

**Step 1.**

### Create a recurring price [#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 and put it in your server's
environment, for example as `FIANTO_PRICE_PRO`.

**Step 2.**

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

Change your checkout route to 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
description.

#### Next.js

```ts
// app/api/checkout/route.ts
import { Checkout } from '@fianto/nextjs';

export const POST = Checkout({
  createSession: async () => {
    const priceId = process.env.FIANTO_PRICE_PRO;
    if (!priceId) return new Response('Plan not configured', { status: 500 });

    return {
      mode: 'subscription',
      order_id: `sub_${crypto.randomUUID()}`, // use your own id for this sign-up
      price_id: priceId,
      success_url: 'https://shop.example/thank-you',
      cancel_url: 'https://shop.example/pricing',
    };
  },
  onError: (error) => console.error(error),
});
```

#### Express

```ts
// server.ts
import express from 'express';
import { checkout } from '@fianto/express';

const app = express();

app.post(
  '/api/checkout',
  checkout({
    createSession: async () => {
      const priceId = process.env.FIANTO_PRICE_PRO;
      if (!priceId) return new Response('Plan not configured', { status: 500 });

      return {
        mode: 'subscription',
        order_id: `sub_${crypto.randomUUID()}`, // use your own id for this sign-up
        price_id: priceId,
        success_url: 'https://shop.example/thank-you',
        cancel_url: 'https://shop.example/pricing',
      };
    },
    onError: (error) => console.error(error),
  }),
);

app.use(express.json());
app.listen(3000);
```

#### Hono

```ts
// src/index.ts
import { Hono } from 'hono';
import { checkout } from '@fianto/hono';

const app = new Hono();

app.post(
  '/api/checkout',
  checkout({
    createSession: async () => {
      const priceId = process.env.FIANTO_PRICE_PRO;
      if (!priceId) return new Response('Plan not configured', { status: 500 });

      return {
        mode: 'subscription',
        order_id: `sub_${crypto.randomUUID()}`, // use your own id for this sign-up
        price_id: priceId,
        success_url: 'https://shop.example/thank-you',
        cancel_url: 'https://shop.example/pricing',
      };
    },
    onError: (error) => console.error(error),
  }),
);

export default app;
```

Set your button's label to `subscribe`. Nothing else on the page changes.

The first subscription session for a price creates its plan on Solana; Fianto pays for that. Until
the plan is on chain, the checkout page shows it as preparing (`plan_status: preparing`) instead of
a pay button. Fianto retries a failed plan creation after 30 seconds, 2 minutes, 10 minutes and
1 hour, then marks it `failed`.

**Step 3.**

### Handle the subscription webhooks [#handle-the-subscription-webhooks]

Fianto sends `subscription.created` when the subscribe transaction is finalized. Give access
there, and keep it in step with the other subscription events.

#### Next.js

```ts
// app/api/webhooks/fianto/route.ts
import { Webhooks } from '@fianto/nextjs';

export const POST = Webhooks({
  secret: process.env.FIANTO_WEBHOOK_SECRET,
  // Dedupe on event.id in the same database transaction as each change.
  onSubscriptionCreated: async (event) => {
    console.log('grant access', event.data.id, event.data.order_id);
  },
  onSubscriptionRenewed: async (event) => {
    console.log('extend access', event.data.id, event.data.current_period_end);
  },
  onSubscriptionPastDue: async (event) => {
    console.log('renewal failing', event.data.id);
  },
  onSubscriptionEnded: async (event) => {
    console.log('remove access', event.data.id, event.data.end_reason);
  },
});
```

#### Express

```ts
// server.ts
import express from 'express';
import { webhooks } from '@fianto/express';

const app = express();

app.post(
  '/webhooks/fianto',
  webhooks({
    secret: process.env.FIANTO_WEBHOOK_SECRET,
    // Dedupe on event.id in the same database transaction as each change.
    onSubscriptionCreated: async (event) => {
      console.log('grant access', event.data.id, event.data.order_id);
    },
    onSubscriptionRenewed: async (event) => {
      console.log('extend access', event.data.id, event.data.current_period_end);
    },
    onSubscriptionPastDue: async (event) => {
      console.log('renewal failing', event.data.id);
    },
    onSubscriptionEnded: async (event) => {
      console.log('remove access', event.data.id, event.data.end_reason);
    },
  }),
);

app.use(express.json());
app.listen(3000);
```

#### Hono

```ts
// src/index.ts
import { Hono } from 'hono';
import { webhooks } from '@fianto/hono';

const app = new Hono();

app.post(
  '/webhooks/fianto',
  webhooks({
    secret: process.env.FIANTO_WEBHOOK_SECRET,
    // Dedupe on event.id in the same database transaction as each change.
    onSubscriptionCreated: async (event) => {
      console.log('grant access', event.data.id, event.data.order_id);
    },
    onSubscriptionRenewed: async (event) => {
      console.log('extend access', event.data.id, event.data.current_period_end);
    },
    onSubscriptionPastDue: async (event) => {
      console.log('renewal failing', event.data.id);
    },
    onSubscriptionEnded: async (event) => {
      console.log('remove access', event.data.id, event.data.end_reason);
    },
  }),
);

export default app;
```

The seven subscription events are `subscription.created`, `subscription.renewed`,
`subscription.payment_failed`, `subscription.past_due`, `subscription.cancel_scheduled`,
`subscription.cancel_withdrawn` and `subscription.ended`. A subscription session creates no order,
so fulfil subscriptions from these events.

**Step 4.**

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

Cancel with the API, now or at the end of the current period. A cancel at `now` sends
`subscription.ended`; a cancel at `period_end` sends `subscription.cancel_scheduled`.

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

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

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 stops Fianto from charging, and it cannot be undone. It does not end the payer's
> subscription on Solana. With `now`, a charge already in flight can still land; there is no refund
> API, so refund it by hand from your wallet.

> **What the payer pays:**
>
> When subscribing, the payer pays the price for the first period 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 that moment. Each renewal takes the price again, plus the service fee
> when it is above zero, and Fianto pays the network fee for it.

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

> With your server running locally, send it a signed sample `subscription.created`. This makes no
> API call.
>
> ```bash
> npx @fianto/cli trigger subscription.created \
>   --forward-to http://localhost:3000/api/webhooks/fianto \
>   --secret "$FIANTO_WEBHOOK_SECRET"
> ```
>
> Export `FIANTO_WEBHOOK_SECRET` in this shell first, or paste the `whsec_…` value. For Express and
> Hono, forward to `/webhooks/fianto` on the port your server listens on.
>
> The CLI prints `→ 200 subscription.created (local sample)` and your `onSubscriptionCreated` logs
> the subscription. After a real sign-up, the subscription is listed by `GET /v1/subscriptions`.

## Troubleshooting [#troubleshooting]

| You see | Why | Fix |
|---|---|---|
| Creating the session fails with `price_id_required` | A subscription session was sent without `price_id`. | Send the recurring price's `fian_price_…` id. |
| Creating the session fails with `amount_not_allowed` | The session sent `amount` or `description`. | Remove both; the price and its product set them. |
| Creating the session fails with `price_not_recurring` | The price is one-time. | Use a recurring price, or send `mode: 'payment'`. |
| The checkout page shows the plan as 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. |
| Creating the session fails with `plan_limit_reached` | Your account created 50 new plans in the last 24 hours, the limit. | Try again later. |

## See also [#see-also]

- [Quickstart](/get-started/quickstart): The checkout route, button and webhook route this guide builds on.

- [How money moves](/get-started/how-money-moves): What the subscribe transaction contains and who pays for what.

- [Developers](/developers): The v1 API, webhooks, SDKs and CLI in depth.