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
| Name | Type | Description |
|---|
subscriber | Address | The subscriber’s Stellar address. Must sign the transaction. |
plan_id | u64 | The ID of the plan to subscribe to. |
expiration_ledger | u32 | Absolute ledger at which the SAC allowance expires. Typically current_ledger + MAX_APPROVAL_LEDGERS. |
allowance_periods | u32 | Number 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
| Event | Topics | Data |
|---|
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
| Code | Name | Description |
|---|
| 6 | PlanNotFound | No plan exists with the given plan_id. |
| 7 | PlanInactive | The 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);
# expiration_ledger = current + 2_900_000 (~168-day buffer)
# allowance_periods = 24 (covers 2 years of monthly billing)
soroban contract invoke \
--id CONTRACT_ID \
--network testnet \
--source SUBSCRIBER_SECRET \
-- \
subscribe \
--subscriber GSUBSCRIBER...ADDR \
--plan_id 1 \
--expiration_ledger 3100000 \
--allowance_periods 24
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.