DevelopersAccept payments
Subscriptions
Sell a recurring USDC price with a subscription session, follow the subscription's status, and cancel it from your server.
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.
Scroll sideways to see the whole diagram →
Show as text
- A subscription starts `ACTIVE`; the first period is charged inside the subscribe transaction.
- Fianto charges each renewal every 30 days or every 365 days, as the price sets.
- The first counted failed renewal moves it to `PAST_DUE`; a later successful retry moves it back to `ACTIVE`.
- The 4th counted failure ends it: `ENDED` with reason `PAYMENT_FAILED`.
- 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.
- Re-subscribing ends the old subscription with `PAYER_CANCELED`.
- No back-billing: missed periods are never billed.
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.
- You have a webhook route. Subscriptions are followed only through events and reads; a subscription session creates no order.
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.
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 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"
}'The url, reissue, cancel and redirect or popup rules are the same as for
one-time sessions.
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.
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.
Cancel from your server
Cancel with POST /v1/subscriptions/{id}/cancel and at:
nowends the subscription at once and sendssubscription.ended.period_endstops it at the end of the current period and sendssubscription.cancel_scheduled, unless a cancel is already scheduled: then it sends no second event. If the payer scheduled that cancel, your request still setsmerchant_cancel_requestedtotrue(thecancel_reasonstaysPAYER_CANCELED), so your cancel stands even if the payer resumes the subscription on Solana.
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" }'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
After a payer subscribes and the transaction finalizes, your webhook route receives
subscription.created, and the subscription is listed by GET /v1/subscriptions:
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
| 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
Was this page helpful? Tell us