# Subscriptions and payments

Source: https://docs.orbytelabs.com/subscriptions

Sell Stripe subscriptions with feature limits and accept paid credit top-ups on the same connected account.

Each Credit application connects to a Stripe account under Orbyte's platform. The plan catalog, Stripe customers, subscription charges, and paid top-ups belong to that connected account. Development uses Stripe test credentials; Production uses live credentials. Configure and test them separately.

A plan defines a recurring price and included usage for individual features. For example, Pro can cost $20 per month and include 20 web searches. Each feature decides what happens after its allowance: block further usage or charge the wallet's credits.

## Connect Stripe [#connect-stripe]

In the Credit dashboard, select an application, open **Plans**, and enter your business registration country code, and choose **Connect Stripe**. Complete Stripe's hosted onboarding. When you return, select **Refresh** to check whether payments and payouts are enabled. If the dashboard says Stripe is not configured, the platform operator needs to configure that environment first.

Plans can be saved before onboarding is complete. The dashboard shows whether each plan has synchronized with Stripe and lets you retry a failed sync. Checkout requires a connected account that can accept charges and a synchronized active plan.

## Define plans in code [#define-plans-in-code]

Add plans to the same config as your features:

```ts
import { credit, feature, Orbit } from "@orbytelabs/credit";

export const credits = credit("credits");

export default Orbit({
  features: {
    search: feature("Web search", credits(2)),
  },
  plans: {
    pro: {
      name: "Pro",
      amountCents: 2000,
      currency: "usd",
      interval: "month",
      creditGrants: [
        { creditKey: "credits", amountNanocredits: "100000000000" },
      ],
      topupsEnabled: true,
      featureLimits: [
        {
          featureKey: "search",
          limit: 20,
          period: "month",
          overage: "credits",
        },
      ],
    },
  },
});
```

`amountCents` is an integer in the currency's smallest Stripe unit, such as 2000 for $20 USD. It is separate from nanocredits. Use a lowercase three-letter currency code. `interval` is `month` or `year`. Plan keys are stable within an application.

```sh
pnpm exec orbyte deploy --dry-run
pnpm exec orbyte deploy
```

The CLI deploys credits and features before plans. The API queues synchronization of plan products and prices to Stripe. Use a key with `plans:read` and `plans:write`, in addition to the catalog permissions. The dashboard's **Setup and ingestion** preset includes these permissions for newly created keys.

You can also create or edit the same plans in the dashboard. Changes affect the shared catalog; a later config deployment can overwrite values changed in the dashboard. Pick a source of truth for each plan.

## Recurring wallet credits [#recurring-wallet-credits]

`creditGrants` adds balances that customers can spend across features. It is separate from included feature units. Each entry selects a deployed `creditKey` and a positive `amountNanocredits` string. The example above adds 100 credits after a confirmed creation, renewal, or full-period migration invoice.

Grants add to the current wallet balance. Unspent credits carry over; this integration does not expire or reset them. A monthly subscription grants on each monthly invoice, and a yearly subscription grants on each annual invoice. A migration that starts and pays for a full new period, such as free to paid or monthly to yearly, receives the new plan's grant. A proration-only plan-change invoice does not issue another grant. Replayed payment events cannot add the same grant twice.

Free plans also receive their configured credits through their confirmed subscription invoice. Grants are capped once per wallet and credit type per UTC month for free monthly plans, or per UTC year for free yearly plans. Canceling and resubscribing cannot collect another allowance in the same capped period.

Set `topupsEnabled: false` to prevent new paid top-up checkouts for subscribers on that plan. Paid top-ups also require an active subscription period when the wallet has a subscription. An already-approved checkout still fulfills after confirmed payment, even if the plan changes before payment completes. Direct manual deposits remain available for grants and billing corrections.

Both fields are optional: `creditGrants` defaults to `[]` and `topupsEnabled` defaults to `true`. Existing subscribers retain their saved grant and top-up terms until a plan migration.

## Different feature prices per plan [#different-feature-prices-per-plan]

Use `featureMultipliers` when a plan changes the price of an existing feature:

```ts
featureMultipliers: [{ featureKey: "search", basisPoints: 15000 }];
```

`10000` basis points keeps the catalog price, `15000` charges 150% of it, and `5000` charges half. A zero multiplier makes that feature free while the subscription is active. Each feature can appear once, with a multiplier from 0 to 1,000,000 basis points. Unlisted features keep their catalog price.

Multipliers apply to fixed or server-calculated charges after included units are accounted for. They do not consume more feature allowance units. Like other plan terms, each subscription retains its saved multipliers until migrated. Without an active subscription, its price multiplier does not grant paid-plan access.

## How allowances work [#how-allowances-work]

| Setting              | Behavior                                                                                                        |
| -------------------- | --------------------------------------------------------------------------------------------------------------- |
| `limit: 20`          | Includes 20 feature units. A tracked event with quantity 5 uses five units.                                     |
| `period: "month"`    | Resets at the start of each UTC calendar month, while the subscription is active.                               |
| `period: "billing"`  | Resets with the current subscription billing period. A yearly plan gives one allowance per year.                |
| `overage: "block"`   | Rejects an event that exceeds the remaining allowance. No quota or credits are consumed by that rejected event. |
| `overage: "credits"` | Charges the normal feature price for units above the allowance. The wallet must have enough credits.            |

Plan allowances do not deposit credits into the wallet. Included units cost no credits. An event spanning the remaining allowance charges only the excess units at the feature's fixed quantity price, rounded once. For example, with two searches remaining, five searches at two credits each cost six credits. Callback-priced events have quantity 1 and charge the resolved callback amount only when the included allowance is exhausted.

Retries with the same event ID and period-deduplicated uses do not consume an allowance again. Allowances count the usage admitted by the feature's existing counting rule. Features omitted from both the plan's feature allowances and price multipliers retain their usual credit price. A wallet without a subscription can still use those prepaid feature prices.

Only an `active` or `trialing` subscription within its current period grants included usage. An inactive, past-due, or expired subscription blocks features in its saved allowance or multiplier lists, even if the wallet has credits. Other features retain prepaid access. A subscription awaiting invoice confirmation can have `pending_payment` status. A subscription changed outside the supported Credit plan structure can have `unmanaged` status. Both block access to its saved plan features until reconciled. Paid top-ups do not extend a subscription or bypass a feature's block rule.

Continue to use `check` before work and `track` afterward. Checks report `allowed: false` when access is denied. An optional `reason` can identify `feature_limit_exceeded`, `subscription_inactive`, or `insufficient_balance`. Always read `allowed`; a missing reason does not mean access was granted. An allowance result includes its `limit`, `used`, `remaining`, and period boundaries. A plan allowance with `period: "billing"` follows the subscription period from Stripe. Features with `deduplicationPeriod: "billing"` also use the current subscription period when one exists. Wallets without a subscription use their configured wallet billing anchor.

Checks do not reserve allowance or credits. Tracking verifies both again and records usage and charges atomically.

## Subscription checkout [#subscription-checkout]

Resolve the customer's identity from your authenticated session on the server:

```ts
import { createCheckout } from "@orbytelabs/credit";
import config from "./orbyte.config";

const checkout = await createCheckout("customer_123", config.plans.pro, {
  successUrl: "https://your-app.com/billing",
  cancelUrl: "https://your-app.com/pricing",
  idempotencyKey: "subscription_order_123",
});

// Redirect this customer to checkout.url.
```

The customer pays on Stripe's hosted checkout. Subscription state arrives through verified Stripe events. A return to `successUrl` is not proof of payment; read the customer's subscription before granting access. The same wallet has at most one current subscription.

Use HTTPS return URLs, or HTTP on localhost during development. All checkout, portal, migration, and cancellation calls need a server key with `subscriptions:write`. Never let browser input choose another customer's identity, arbitrary plan prices, or paid top-up amounts.

## Paid credit top-ups [#paid-credit-top-ups]

```ts
import { createTopupCheckout } from "@orbytelabs/credit";
import { credits } from "./orbyte.config";

const checkout = await createTopupCheckout("customer_123", credits(100), {
  amountCents: 1000,
  currency: "usd",
  successUrl: "https://your-app.com/billing",
  cancelUrl: "https://your-app.com/billing",
  idempotencyKey: "topup_order_123",
});

// Redirect this customer to checkout.url.
```

This collects $10 USD on the same connected account and adds 100 credits after payment confirmation. Stripe event retries cannot add the credits again. A delayed payment adds nothing until it succeeds. The return URL alone never funds the wallet. Do not also call `topup` for this payment; Credit already fulfills the paid checkout and a separate deposit would fund it twice.

The existing `topup` function and the dashboard's **Add credits** action still add credits directly. Use them for grants or separately verified payments. They do not collect money.

## Change plans and manage billing [#change-plans-and-manage-billing]

```ts
import {
  cancelSubscription,
  changeSubscription,
  createBillingPortal,
  getSubscription,
} from "@orbytelabs/credit";

const current = await getSubscription("customer_123");

const updated = await changeSubscription("customer_123", "pro", {
  idempotencyKey: "plan_change_123",
});

const portal = await createBillingPortal("customer_123", {
  returnUrl: "https://your-app.com/billing",
});

await cancelSubscription("customer_123", {
  cancelAtPeriodEnd: true,
  idempotencyKey: "cancel_123",
});
```

`getSubscription` needs `subscriptions:read` and returns `null` when none exists. Otherwise it returns the plan key, subscription status, current period in Unix milliseconds, scheduled cancellation, feature limit snapshot, and any pending or scheduled plan change.

A plan change selects the latest synchronized plan version. Upgrades start a full new billing period immediately and charge its full price, without prorating the previous period. Credits for the new period are granted after confirmed payment. If payment needs customer action, the previous plan remains in place until payment succeeds. The returned subscription includes `pendingChange` while payment is required, with the target `planKey`, expiry in Unix milliseconds, and an optional `paymentUrl`. Send that private payment URL to the authenticated customer, or create a billing portal link when it is absent.

Downgrades retain the current plan, allowances, and credit balance until renewal. The returned `scheduledChange` contains the target `planKey` and `effectiveAt` in Unix milliseconds. Lower prices are compared at their annualized rate in the same currency. Both flows update the existing Stripe subscription. Changing a plan does not reset usage already counted in the same quota period.

To let the customer confirm a paid upgrade on Stripe, supply the target plan:

```ts
const confirmation = await createBillingPortal("customer_123", {
  plan: "pro", // A config.plans entry is also accepted.
  returnUrl: "https://your-app.com/billing",
  successUrl: "https://your-app.com/billing?updated=true",
});
// Redirect the authenticated customer to confirmation.url.
```

Stripe's confirmation page shows the selected upgrade on the existing subscription and handles payment details and authentication. Confirmation starts a full paid billing period immediately. It creates no second subscription and needs no prior card-setup step in your app. After completion, Stripe redirects to the optional `successUrl`, defaulting to `returnUrl`. Creating the link does not change the plan; read the webhook-confirmed subscription after return. Schedule downgrades with `changeSubscription`.

Free subscriptions may have no saved payment method. If Stripe cannot charge a direct `changeSubscription` for that reason, it throws `OrbyteApiError` with code `payment_method_required` and status `409`. Use the confirmation flow above to complete that upgrade.

Without `plan`, the portal lets customers manage payment details, invoices, and cancellation, with plan selection disabled. Portal links grant access to that customer's billing account; return them only to the authenticated customer.

Cancellation takes effect at the end of the current paid period. Set `cancelAtPeriodEnd: false` with a new operation key to remove a scheduled cancellation or downgrade and keep the current plan renewing. Canceling renewal also releases a queued downgrade.

## Edit or archive a plan [#edit-or-archive-a-plan]

Price, feature limit, credit grant, top-up policy, or multiplier changes create a new version. Existing subscriptions keep the price and allowance snapshot associated with their Stripe price. Financial terms of features used by plans are fixed too: changing their cost, credit, ratio, pricing mode or deduplication period requires a new feature key and an explicit plan change. Names and display units remain editable. To migrate an existing customer, call `changeSubscription`, even when the plan key is unchanged. Setting `active: false` stops new subscriptions; it does not cancel existing customers.

## Retries and payment history [#retries-and-payment-history]

Keep a stable `idempotencyKey` for each checkout, change, and cancellation operation. Retry uncertain responses with exactly the same input. A different purchase or plan change needs a new key. An expired or completed checkout cannot be reopened as a new purchase with its old key.

Payment confirmation uses signed Stripe webhooks and stores processed event IDs. Credit deposits remain separate from the Stripe payment record. Verified refunds and disputes append credit adjustments. A proportional refund removes the corresponding purchased or invoice-granted credits. Already spent credits become visible wallet debt, repaid before new deposits become spendable. Won disputes restore the unrefunded portion. A disputed or fully refunded subscription invoice suspends plan benefits for the affected period and returns `paymentHold: true`; partial refunds leave included benefits active.