Get startedQuickstarts
Subscriptions quickstart
Sell a recurring price. Create it in the dashboard, start a subscription session, and handle the subscription webhooks.
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 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
Was this page helpful? Tell us