DevelopersAccept payments

Subscriptions

Sell a recurring USDC price with a subscription session, follow the subscription's status, and cancel it from your server.

4 min read

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
The statuses of a subscription and how it endspayer or merchant cancelscountedfailureretry succeeds4th countedfailurerenews every 30 or 365 daysACTIVEcharged each periodPAST_DUEretryingENDEDmissed periods arenever billedend reasonPAYER_CANCELEDMERCHANT_CANCELEDPAYMENT_FAILED

Scroll sideways to see the whole diagram →

Show as text
  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

  • 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:

EventWhen
subscription.renewedA renewal was charged
subscription.payment_failedA renewal charge had a counted failure
subscription.past_dueThe first counted failure moved the subscription to PAST_DUE
subscription.cancel_scheduledA cancel at the end of the period was scheduled
subscription.cancel_withdrawnA scheduled cancel was withdrawn
subscription.endedThe 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:

  • 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 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 seeWhyFix
400 price_id_requiredA subscription session was sent without price_id.Send the recurring price's fian_price_… id.
400 amount_not_allowedThe session sent amount or description.Remove both; the price and its product set them.
422 price_not_recurringThe price is one-time.Use a recurring price, or send mode: payment.
429 plan_limit_reachedThe 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_endedYou 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

On this page