Skip to main content

What is a Subscription?

A subscription is the on-chain record linking a subscriber to a plan. It tracks billing state, lifecycle status, and timing. Each subscription is its own persistent ledger entry, enabling parallel processing across the network.

State Machine

Every subscription moves through a defined set of states. Transitions are enforced by the contract - there is no way to skip a state or force an invalid transition.
FromToTrigger
-Activesubscribe() called
ActivePausedCharge fails and grace period expires
ActiveExpiredmax_periods reached during charge
ActiveCancelledSubscriber calls cancel()
PausedActiveSubscriber calls reactivate() (funds now available)
PausedCancelledOne more billing period passes while paused, or subscriber calls cancel()
Paused is a recoverable state. If a subscriber tops up their wallet and calls reactivate(), the subscription returns to Active and billing resumes. But if another full billing period passes while paused, the next charge() call will transition it to Cancelled permanently.
There is no transition from Cancelled or Expired back to any other state. These are terminal. The subscriber must create a new subscription if they want to resume.

Creating a Subscription

When a subscriber calls subscribe(), several things happen in a single atomic transaction:
1

Subscriber authorization

The subscriber signs the transaction. Soroban’s auth tree means the subscriber signs once - this single signature covers both the subscribe() contract call and the token.approve() call nested inside it. No separate approval transaction needed.
2

Plan validation

The contract loads the plan and verifies:
  • The plan exists
  • The plan is active == true
  • The subscriber is not the merchant (cannot subscribe to your own plan)
3

Allowance calculation

The contract calculates how much token allowance to request:
allowance = price_ceiling * effective_periods
Where effective_periods is:
  • max_periods if the plan has a limit, or
  • 120 if the plan is unlimited (max_periods == 0)
The allowance expiry is set to the maximum Soroban allows: approximately 6,312,000 ledgers (~347 days at 5-second block times).
4

Token approval

The contract calls token.approve(subscriber, contract_address, allowance, expiry_ledger). This grants the Vowena contract permission to pull up to allowance tokens from the subscriber over the approval period.
5

Subscription stored

The contract increments NextSubId, creates the Subscription struct with status: Active, and writes it to persistent storage. The subscription ID is appended to both SubscriberSubs(subscriber) and PlanSubs(plan_id) indexes.
6

Event emitted

A SubscriptionCreated event fires with the full subscription struct.
The allowance is based on price_ceiling, not the current amount. This is intentional - it ensures that if the merchant raises the price (within the ceiling), existing subscribers do not need to re-authorize. The subscriber sees the ceiling amount in their wallet’s approval prompt.

Allowance Calculation Examples

Plan: 10 USDC/month, ceiling 15 USDC, max 12 periods
allowance = 15 USDC * 12 = 180 USDC
expiry    = current_ledger + 6,312,000 (~347 days)
The subscriber’s wallet will show: “Approve 180 USDC for Vowena contract”.

Cancellation

Cancellation is always available to the subscriber and is immediate:
const tx = await client.cancel({
  subscriber: subscriberKeypair.publicKey(),
  sub_id: 42n,
});
Only the subscriber can cancel their own subscription. The contract calls subscriber.require_auth(). Merchants cannot cancel on behalf of subscribers.
The remaining token allowance stays until it expires naturally on the ledger. The contract will never use it after cancellation because the subscription status is Cancelled and charge() checks status before attempting any transfer.
Cancellation does not trigger an automatic refund. The current period is already paid. Merchants can issue voluntary refunds separately.

Reactivation

If a subscription is Paused (charge failed, grace expired), the subscriber can reactivate it:
1

Top up wallet

The subscriber ensures they have sufficient token balance and that the contract’s allowance has not expired.
2

Call reactivate

The subscriber calls reactivate(sub_id). The contract verifies the subscription is in Paused status and transitions it back to Active.
3

Billing resumes

The next_billing_time is reset so that the next charge() call can process immediately.
If the original token allowance has expired while the subscription was paused, the subscriber will need to create a new subscription instead. Reactivation only works if the allowance is still valid.

What’s Next

Billing

The most important page - understand exactly what happens during a charge.

Migrations

How merchants move subscribers to new plans with explicit consent.