Skip to main content
fn subscribe(
    env: Env,
    subscriber: Address,
    plan_id: u64,
    expiration_ledger: u32,
    allowance_periods: u32,
) -> u64
Subscribes a user to an existing plan. In a single transaction the contract sets the SEP-41 token allowance and creates the subscription record. The subscriber signs once and both operations are authorized. If the plan has no trial (trial_periods = 0), the contract charges the first period atomically during subscribe — Stripe-style semantics. If the plan has a trial, no funds move until the trial ends. Returns the chain-assigned sub_id.

Parameters

NameTypeDescription
subscriberAddressThe subscriber’s Stellar address. Must sign the transaction.
plan_idu64The ID of the plan to subscribe to.
expiration_ledgeru32Absolute ledger at which the SAC allowance expires. Typically current_ledger + MAX_APPROVAL_LEDGERS.
allowance_periodsu32Number of billing periods the allowance should cover. Clamped to plan.max_periods, or 120 for unlimited plans.
Both expiration_ledger and allowance_periods are passed in rather than computed inside the contract. This is intentional: the nested token.approve() call’s args must match exactly between simulation and submission, otherwise Soroban’s auth tree rejects the invocation.

Authorization

subscriber.require_auth();
The subscriber’s single signature covers both the subscribe() contract call and the nested token.approve() call. Soroban’s auth tree bundles both operations into one authorization.
The subscriber signs once. Their wallet shows exactly what is being authorized: the Vowena contract call and the token allowance (amount = price_ceiling * allowance_periods, spender = Vowena contract).

Allowance calculation

allowance = price_ceiling * effective_periods
Where effective_periods is:
  • min(allowance_periods, plan.max_periods) if plan.max_periods > 0
  • min(allowance_periods, 120) if the plan is unlimited
The allowance is set against the plan’s price_ceiling, not the current amount. This way the merchant can adjust pricing within the ceiling without requiring re-authorization.

Return value

u64 — the newly created subscription ID.

Events emitted

EventTopicsData
sub_created"sub_created", subscriber(sub_id, plan_id)
charge_ok"charge_ok", subscriber(sub_id, amount) — only emitted when there is no trial and the first-period charge succeeds

Error cases

CodeNameDescription
6PlanNotFoundNo plan exists with the given plan_id.
7PlanInactiveThe plan is not accepting new subscribers.

Examples

import { VowenaClient, NETWORKS } from "@vowena/sdk";

const client = new VowenaClient({
  contractId: NETWORKS.testnet.contractId,
  rpcUrl: NETWORKS.testnet.rpcUrl,
  networkPassphrase: NETWORKS.testnet.networkPassphrase,
});

// Minimal: the SDK picks safe defaults for expirationLedger
// and allowancePeriods.
const tx = await client.buildSubscribe(
  "GSUBSCRIBER...ADDR", // Subscriber's address
  1,                    // Plan ID
);

// Or with explicit values:
const latest = await client["server"].getLatestLedger();
const tx2 = await client.buildSubscribe(
  "GSUBSCRIBER...ADDR",
  1,
  {
    expirationLedger: latest.sequence + 2_900_000,
    allowancePeriods: 24,
  },
);

const signedXdr = await signTransaction(tx);
const result = await client.submitTransaction(signedXdr);
console.log("Subscription ID:", result.subscriptionId);
If the plan has trial periods, the subscription starts immediately but billing begins after the trial ends. The next_billing_time is set to now + plan.period and the trial periods are advanced without charging.