Get startedQuickstarts

Subscriptions quickstart

Sell a recurring price. Create it in the dashboard, start a subscription session, and handle the subscription webhooks.

3 min read

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

  • You finished the quickstart: your checkout route, button and webhook route work.
  • Your merchant account is approved.

Steps

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.

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.

// 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),
});

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.

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.

// 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);
  },
});

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.

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.

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

With your server running locally, send it a signed sample subscription.created. This makes no API call.

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

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

See also

Was this page helpful? Tell us

On this page