---
title: "SubscriptionConductor"
description: "Multi-provider membership billing — Stripe, WayForPay, credit, native token, NFT gate, PayPal Subscriptions v1, and Telegram Stars (XTR). One ledger, manage page, webhooks, and cron."
locale: "en"
---
# SubscriptionConductor

> **Info**
> Filter this page with **Founder** / **Developer** in the docs sidebar. Founders learn provider costs, the manage page, PayPal, and Stars; developers get ledger fields, provider modules, and webhook branches.

SubscriptionConductor is Ring Platform's multi-provider **membership** billing facade. One SSOT ledger (`subscription_ledger`) tracks subscriber state for every rail. Provider modules implement `SubscriptionProviderModule`; PaymentConductor still owns one-shot Orders (store / wallet / one-shot membership).

Implementation: `lib/payments/subscription/`. Manage UI: `/membership/manage`.

### For founders

## What founders get

Recurring membership revenue with clear gateway fees. Internal rails (credit points, native token, NFT gate) charge **0%** platform PSP fee. Members manage cancel / renew / history on **Manage membership** (`/membership/manage`) — not only the upgrade funnel at `/membership`.

  **Own** file manager (`/file-cabinet`) and gallery manage (`/profile/gallery`) are **member+** (`hasMemberPrivileges` + `MemberUpgradeGate`). **Shared-with-me** (`/profile/shared`) is **subscriber+** for trustee shares; subscriber-only users see an upgrade CTA for an own manager — see [Ring File Cabinet](/docs/features/file-cabinet.md).

### Feature status

| Provider | Details | Gateway | Status |
|----------|---------|---------|--------|
| Credit Balance | Auto-deduct credit units (points) | Credit points | ✅ Live |
| WayForPay | Card HPP + `recToken` regularApi | WayForPay | ✅ Live |
| Stripe | Stripe Subscriptions API | Stripe | ✅ Live |
| Native Token | On-chain Solana / RingMembership | Native RING | ✅ Live |
| NFT Gate | Metaplex Core + GateEscrow stake | NFT Gate | ✅ Live (MVP-A) |
| PayPal | **Subscriptions v1** (recurring) + Orders one-shot | PayPal | ✅ Live (flags + credentials) |
| Telegram Stars | XTR invoice link via Bot API `createInvoiceLink` | Telegram | ✅ Create pending — webhook activation TBD |

### Telegram Stars (operator view)

1. Set a Mini App / Stars bot token: prefer `TELEGRAM_MINI_APP_BOT_TOKEN` (same helper as Mini App auth — see [Authentication](/docs/features/authentication.md)).
2. Checkout must pass `metadata.telegramUserId` (the payer’s Telegram numeric id). Optional: `metadata.invoiceTitle` / `invoiceDescription`.
3. Conductor creates a **`pending`** ledger row and returns a Telegram invoice URL (`redirect.mode: navigate`). Member pays in Telegram (currency **XTR**).
4. Ledger stays **pending** until Telegram `successful_payment` on `POST /api/telegram/stars-bot/webhook` activates it (same pattern as PayPal `BILLING.SUBSCRIPTION.ACTIVATED`). Do not treat invoice creation alone as paid membership.

### PayPal for membership (operator view)

1. Set `payment.gateways.paypal.enabled: true` in `ring-config.json`.
2. Fill sandbox/live credentials: `PAYPAL_CLIENT_ID`, `PAYPAL_CLIENT_SECRET`, `PAYPAL_WEBHOOK_ID`, `PAYPAL_MODE`.
3. Turn on the public UI flag: `NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true` (also gates store PayPal + membership PaymentModal PayPal tab).
4. In PayPal Developer, subscribe the webhook to Orders **and** billing events (`BILLING.SUBSCRIPTION.*`, `PAYMENT.SALE.COMPLETED`).
5. After first Product/Plan create, cache `PAYPAL_MEMBERSHIP_PRODUCT_ID` / `PAYPAL_MEMBERSHIP_PLAN_ID` so checkout does not recreate catalog objects every time.

Members land on `/membership/manage` after upgrade (members hitting `/membership` redirect there). Cancel uses the PayPal subscription id (`I-…`) stored on the ledger.

### Fee rates & net revenue

| Provider | Fee % | Fixed Fee | Currency |
|----------|-------|-----------|----------|
| WayForPay | 2.5% | — | UAH (typical) |
| Stripe | 2.9% | $0.30 | USD |
| Credit Balance | 0% | — | Credit units |
| Native Token | 0% | — | RING |
| NFT Gate | 0% | — | RING |
| PayPal | 2.9% | $0.30 | USD |
| Telegram Stars | Telegram commission (operator-side) | — | XTR |

**Net:** `net = gross - (gross × feePercent / 100) - feeFixed` via `calculateNetRevenue()` in `subscription-config.ts`. Stars fee percent in admin stats defaults to `0` until you configure gateway fees for `telegram_stars`.

### Configuration sketch

```json
{
  "payment": {
    "cardPaymentProcessor": "wayforpay",
    "gateways": {
      "paypal": { "enabled": true, "feePercent": 2.9, "feeFixedCents": 30, "currency": "USD" }
    }
  }
}
```

Prod public flags must also be baked as Docker `NEXT_PUBLIC_*` build-args and applied via `k8s` ConfigMap/Secret — see [ENV-PROD-WIRING](https://github.com/connectplatform/ring/blob/main/k8s/ENV-PROD-WIRING.md) in the clone (`ring-platform.org/k8s/ENV-PROD-WIRING.md`).

### Admin dashboard

Superadmins: `/admin/subscriptions` — list, filters, net revenue, `/api/admin/subscriptions/stats`.

### For developers

## Architecture

```
Membership upgrade / PaymentModal / manage cancel
        ↓
SubscriptionConductor (create | cancel | renew | recordPaidSubscription)
        ↓
Providers: stripe | wayforpay | credit_balance | native_token | nft_gate | paypal | telegram_stars
        ↓
subscription_ledger  (paypal_subscription_id = PayPal I-…; Stars stores invoice link pending)
        ↓
Cron: expiry · credit-balance · payment · solana-batch · nft-gate
```

PayPal **recurring** uses Subscriptions v1 inside `paypal-subscription.ts` (Product → Plan → `POST /v1/billing/subscriptions`). Ledger starts **`pending`**; `BILLING.SUBSCRIPTION.ACTIVATED` promotes to **`active`** without calling provider create again.

PayPal **one-shot** (`auto_subscribe=false`) uses PaymentConductor Orders v2; fulfillment is `PAYMENT.CAPTURE.COMPLETED` → `handleMembershipPayPalCapture` → `recordPaidSubscription` / activate pending.

**Telegram Stars** (`telegram_stars`): `createInvoiceLink` with `currency: XTR`, `payload` = `stars_` (also used as ledger `id`). Returns `ledgerStatus: 'pending'` + navigate URL. Activation: Mini App bot webhook `POST /api/telegram/stars-bot/webhook` handles `pre_checkout_query` → `answerPreCheckoutQuery`, then `successful_payment` → `SubscriptionConductor.activateTelegramStarsPayment` (status `active` + MEMBER upgrade + stores `telegram_payment_charge_id`). Env: `TELEGRAM_MINI_APP_BOT_TOKEN` + `TELEGRAM_STARS_WEBHOOK_SECRET`.

Store checkout PayPal remains Orders-only: `POST /api/store/payments/paypal`.

### Provider registry

| Provider ID | Module | Path |
|-------------|--------|------|
| `credit_balance` | `ringCreditSubscriptionProvider` | `providers/ring-credit-subscription.ts` |
| `native_token` | `nativeTokenSubscriptionProvider` | `providers/native-token-subscription.ts` |
| `stripe` | `stripeSubscriptionProvider` | `providers/stripe-subscription.ts` |
| `wayforpay` | `wayforpaySubscriptionProvider` | `providers/wayforpay-subscription.ts` |
| `nft_gate` | `nftGateSubscriptionProvider` | `providers/nft-gate-subscription.ts` |
| `paypal` | `paypalSubscriptionProvider` | `providers/paypal-subscription.ts` |
| `telegram_stars` | `telegramStarsSubscriptionProvider` | `providers/telegram-stars-subscription.ts` |

Client helpers: `createPayPalBillingSubscription`, `cancelPayPalBillingSubscription`, `getPayPalBillingSubscription` in `lib/payments/processors/paypal-client.ts`.

Stars env helper: `getTelegramMiniAppBotToken()` from `lib/auth/telegram-miniapp-initdata.ts`.

### subscription_ledger (verified fields)

Schema: `lib/payments/subscription/subscription-ledger-schema.ts`.

| Field | Notes |
|-------|--------|
| `status` | `pending` \| `active` \| `expired` \| `cancelled` \| `suspended` \| `grace_period` |
| `method` | `card` \| `credit_balance` \| `crypto` \| `nft` \| `paypal` \| **`stars`** |
| `provider` | includes **`telegram_stars`** |
| `paypal_subscription_id` | PayPal Subscriptions `I-…` (not Orders `orderReference`) |
| `telegram_stars_payload` / `telegram_stars_invoice_link` | Payload = ledger id; invoice URL from createInvoiceLink |
| `telegram_payment_charge_id` | Set on successful_payment (refundStarPayment) |
| `stripe_subscription_id` / `wayforpay_rec_token` / `solana_tx_signature` / `nft_mint_address` | Other rails |

`gatewayRefToField('paypal')` → `{ paypal_subscription_id }`. Cancel API + `app/_actions/membership.ts` pass that id into `SubscriptionConductor.cancelSubscription`.

### Conductor extras (PayPal / webhooks)

| Operation | Role |
|-----------|------|
| `createSubscription` | Honors `ledgerStatus: 'pending'` — skips MEMBER upgrade until active |
| `recordPaidSubscription` | Insert active row **without** calling provider (Orders capture) |
| `findByPaypalSubscriptionId` | Webhook lookup by `I-…` |
| `updateSubscriptionStatus` | Can set `paypal_subscription_id`, payments aggregates |

### PayPal webhook branches

`dispatchPayPalWebhook` (`webhook-dispatcher.ts`):

| Event | Handler |
|-------|---------|
| `PAYMENT.CAPTURE.COMPLETED` | Orders purpose handlers (store / membership capture / wallet / news) |
| `BILLING.SUBSCRIPTION.ACTIVATED` | `membership-paypal-subscription.ts` → activate ledger |
| `PAYMENT.SALE.COMPLETED` | Advance `next_payment_due` / `payments_count` |
| `BILLING.SUBSCRIPTION.CANCELLED` / `SUSPENDED` / `EXPIRED` / `PAYMENT.FAILED` | Terminal / suspended status |

Canonical route: `POST /api/payments/paypal/webhook`.

### Membership & manage routes

| Method | Path | Role |
|--------|------|------|
| `POST` | `/api/membership/payment/paypal` | Create recurring (default) or renew status-sync |
| `POST` | `/api/membership/subscription/cancel` | Cancel; extracts `paypal_subscription_id` |
| `GET` | `/api/membership/subscription/status` | `can_cancel` for `active` \| `pending` |
| Page | `/[locale]/(protected)/membership/manage` | `SubscriptionManagement` + role + NFT stakes + `payment_transactions` |
| Route helper | `ROUTES.MEMBERSHIP_MANAGE(locale)` | `/membership/manage` |

UI: `components/membership/payment-modal.tsx` PayPal tab when `NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true`; `components/membership/subscription-management.tsx` on the manage page.

### Environment variables

{`NEXT_PUBLIC_PAYMENT_STORE_ALLOW_TOKEN=true
NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true
PAYMENT_STORE_ALLOW_TOKEN=true
PAYPAL_MODE=sandbox
PAYPAL_CLIENT_ID=
PAYPAL_CLIENT_SECRET=
PAYPAL_WEBHOOK_ID=
# After first catalog create:
# PAYPAL_MEMBERSHIP_PRODUCT_ID=
# PAYPAL_MEMBERSHIP_PLAN_ID=
PAYMENT_MEMBERSHIP_PROCESSOR=wayforpay

# Telegram Stars invoices (+ Mini App initData auth share this helper):
# TELEGRAM_MINI_APP_BOT_TOKEN=
# TELEGRAM_BOT_TOKEN= / ADMIN_BOT_TOKEN=  # fallbacks`}

Secrets stay runtime (K8s Secret). Public flags also need Dockerfile / `prod.js` build-args for client bundles.

### Cron pipelines

Unchanged five membership crons in `lib/processes/registry.ts` (expiry, credit-balance monthly, subscription-payment, solana-batch, nft-gate-expiry). PayPal renewals are primarily webhook-driven (`PAYMENT.SALE.COMPLETED`); manual renew is a Subscriptions `GET` status sync. Stars renew currently returns a synthetic success + 30-day `nextPaymentDue` — production renew should follow Telegram payment events once the webhook is live.

### Anti-patterns

- Do **not** call `SubscriptionConductor.createSubscription` from PayPal webhooks (re-opens checkout). Use `recordPaidSubscription` / `updateSubscriptionStatus`.
- Do **not** store Orders `orderReference` as `paypal_subscription_id`.
- Do **not** treat `returnUrl` alone as paid — wait for webhook events.
- Do **not** treat Stars `createInvoiceLink` success as membership — wait for `successful_payment` on `/api/telegram/stars-bot/webhook` → activate pending ledger.
- Do **not** omit `metadata.telegramUserId` on Stars create (provider returns an error).

## Related documentation

  
- [features/payment-conductor](/docs/features/payment-conductor.md) — Prerequisite: one-shot Orders vs this recurring ledger facade.

  
- [features/wayforpay-integration](/docs/features/wayforpay-integration.md) — Same-workflow: card HPP + recToken pattern mirrored by PayPal I-….

  
- [architecture/payment-conductor](/docs/architecture/payment-conductor.md) — Deep-dive: webhook dispatcher and API route table.

  
- [features/nft-gates](/docs/features/nft-gates.md) — See-also: GateEscrow stakes shown on the manage page.

  
- [features/payments](/docs/features/payments.md) — See-also: purpose map across the clone.

  
- [customization/payment-integration](/docs/customization/payment-integration.md) — Next-step: operator PSP checklist.

  
- [features/authentication](/docs/features/authentication.md) — Depends-on: Mini App bot token helper used by telegram_stars invoices.

  
- [features/file-cabinet](/docs/features/file-cabinet.md) — Same-workflow: own File Cabinet (/file-cabinet) and gallery are member+; shared-with-me (/profile/shared) is subscriber+ for trustees.
