Skip to main content
Subscriptions represent recurring billing relationships between your customers and your SaaS. Revstack handles the entire subscription lifecycle, from creation to cancellation, with automatic invoicing and proration.

Subscription Lifecycle

A subscription progresses through these states:

Subscription Statuses

active
status
Subscription is active and billing normally. Customer has full access to entitlements.
trialing
status
Customer is in free trial period. No payment required yet. Full access granted.
past_due
status
Payment failed. Customer loses access to all features until payment succeeds.
The Entitlement Engine blocks all feature checks when status is past_due.
paused
status
Customer temporarily paused billing (if provider supports it). Access may be restricted.
canceled
status
Subscription was canceled. No further billing. Access revoked.

Creating Subscriptions

Use the Subscriptions Client to create a new subscription:
import { Revstack } from "@revstackhq/node";

const revstack = new Revstack({ secretKey: process.env.REVSTACK_SECRET_KEY });

// Create a subscription with a free trial
const subscription = await revstack.subscriptions.create({
  customerId: "usr_abc123",
  planId: "plan_pro",
  priceId: "price_monthly", // Optional: specify which price to use
});

console.log(subscription);
// {
//   id: 'sub_xyz789',
//   customerId: 'usr_abc123',
//   planId: 'plan_pro',
//   status: 'trialing',
//   currentPeriodStart: '2026-03-05T00:00:00Z',
//   currentPeriodEnd: '2026-03-19T00:00:00Z', // 14-day trial
//   trialEnd: '2026-03-19T00:00:00Z',
//   canceledAt: null,
// }

Checkout Flow

For new subscriptions, use the hosted checkout:
import { createCheckoutSession } from "@revstackhq/node";

// Create a checkout session
const session = await revstack.checkout.create({
  customerId: "usr_abc123",
  planId: "plan_pro",
  priceId: "price_yearly",
  successUrl: "https://myapp.com/success",
  cancelUrl: "https://myapp.com/pricing",
  addons: ["extra_seats"], // Optional: include add-ons
  coupon: "LAUNCH50", // Optional: apply discount
});

// Redirect user to checkout
res.redirect(session.url);
Revstack creates the subscription automatically when checkout completes. You’ll receive a subscription.created webhook event.

Retrieving Subscriptions

Get a single subscription

const subscription = await revstack.subscriptions.get("sub_xyz789");

List all subscriptions

// List all subscriptions
const { data, hasMore, nextCursor } = await revstack.subscriptions.list();

// Filter by customer
const customerSubs = await revstack.subscriptions.list({
  customerId: "usr_abc123",
});

// Filter by status
const activeSubs = await revstack.subscriptions.list({
  status: "active",
  limit: 100,
});

// Pagination
const nextPage = await revstack.subscriptions.list({
  cursor: nextCursor,
  limit: 50,
});

Changing Plans

Upgrade or downgrade a subscription to a different plan:
// Upgrade from Pro to Enterprise
const updatedSub = await revstack.subscriptions.changePlan("sub_xyz789", {
  planId: "plan_enterprise",
  priceId: "price_monthly", // Optional: specify interval
});

Proration Behavior

Revstack (and most payment providers) handle proration automatically:
1

Calculate unused time

Determine how much time remains in the current billing period.
2

Credit old plan

Issue a credit for the unused portion of the old plan.
3

Charge new plan

Charge for the new plan, prorated for the remaining period.
4

Adjust next invoice

The net difference is applied to the next invoice (or charged immediately).
Example: Customer on Pro (29/month)upgradestoEnterprise(29/month) upgrades to Enterprise (99/month) halfway through the month.
  • Pro credit: 14.50(5014.50 (50% of 29)
  • Enterprise charge: 49.50(5049.50 (50% of 99)
  • Net charge: 35.00(35.00 (49.50 - $14.50)
Proration logic depends on the payment provider. Stripe and Polar both support automatic proration.

Canceling Subscriptions

Cancel a subscription at the end of the current period:
const canceledSub = await revstack.subscriptions.cancel("sub_xyz789");

console.log(canceledSub);
// {
//   id: 'sub_xyz789',
//   status: 'active', // Still active until period ends
//   canceledAt: '2026-03-05T12:34:56Z',
//   currentPeriodEnd: '2026-04-01T00:00:00Z', // Access until this date
// }
Subscriptions are not canceled immediately. They remain active until the end of the current billing period. This ensures customers receive the service they’ve paid for.

Immediate Cancellation

Some providers support immediate cancellation (no refund):
// Provider-specific: check if immediate cancellation is supported
const canceledSub = await revstack.subscriptions.cancel("sub_xyz789", {
  immediate: true, // Not supported by all providers
});

Pausing and Resuming

Some providers (like Stripe) support pausing subscriptions:
// Pause billing (if provider supports it)
try {
  const pausedSub = await revstack.subscriptions.pause("sub_xyz789");
  console.log(pausedSub.status); // 'paused'
} catch (error) {
  // Provider doesn't support pausing
  console.error("Pause not supported by this provider");
}

// Resume billing
const resumedSub = await revstack.subscriptions.resume("sub_xyz789");
console.log(resumedSub.status); // 'active'
Provider Capabilities: Not all providers support pause/resume. Check your provider’s manifest in the Providers documentation.

Subscription Events (Webhooks)

Revstack sends webhook events for subscription changes:
Sent when a new subscription is created (usually after checkout).
{
  "type": "subscription.created",
  "data": {
    "id": "sub_xyz789",
    "customerId": "usr_abc123",
    "planId": "plan_pro",
    "status": "trialing",
    "currentPeriodStart": "2026-03-05T00:00:00Z",
    "currentPeriodEnd": "2026-03-19T00:00:00Z",
    "trialEnd": "2026-03-19T00:00:00Z"
  }
}
Sent when a subscription is modified (plan change, pause, resume, etc.).
{
  "type": "subscription.updated",
  "data": {
    "id": "sub_xyz789",
    "planId": "plan_enterprise",
    "status": "active"
  }
}
Sent when a subscription is canceled (at period end or immediately).
{
  "type": "subscription.canceled",
  "data": {
    "id": "sub_xyz789",
    "status": "canceled",
    "canceledAt": "2026-04-01T00:00:00Z"
  }
}
Sent when a payment fails and the subscription enters past_due status.
{
  "type": "subscription.past_due",
  "data": {
    "id": "sub_xyz789",
    "status": "past_due",
    "unpaidInvoiceId": "inv_failed123"
  }
}
When this event fires, the Entitlement Engine automatically blocks all feature access for this customer.

Handling Webhooks

Listen for subscription events in your webhook handler:
import { Revstack } from "@revstackhq/node";

const revstack = new Revstack({ secretKey: process.env.REVSTACK_SECRET_KEY });

export async function POST(req: Request) {
  const signature = req.headers.get("revstack-signature");
  const payload = await req.text();

  // Verify webhook signature
  const event = await revstack.webhooks.verify(payload, signature);

  switch (event.type) {
    case "subscription.created":
      // Grant access to new subscriber
      await grantAccess(event.data.customerId);
      break;

    case "subscription.past_due":
      // Notify customer their payment failed
      await sendPaymentFailureEmail(event.data.customerId);
      break;

    case "subscription.canceled":
      // Revoke access
      await revokeAccess(event.data.customerId);
      break;
  }

  return new Response(JSON.stringify({ received: true }), { status: 200 });
}

Add-ons and Subscriptions

Add-ons are purchased on top of a subscription:
// Attach an add-on to an existing subscription
const updatedSub = await revstack.subscriptions.addAddon("sub_xyz789", {
  addonId: "extra_seats",
  quantity: 2, // 2x "10 Extra Seats" = 20 seats total
});

// Remove an add-on
await revstack.subscriptions.removeAddon("sub_xyz789", "extra_seats");
Add-ons are prorated just like plan changes. If you add an add-on halfway through the month, you’re only charged for half the month.

Subscription Metadata

Store custom data on subscriptions:
const subscription = await revstack.subscriptions.update("sub_xyz789", {
  metadata: {
    internal_account_id: "acc_123",
    sales_rep: "[email protected]",
    contract_id: "contract_789",
  },
});
Metadata is returned with the subscription object and passed to webhooks.

Common Patterns

Enforcing Subscription Status

Gate application access based on subscription status:
import { Revstack } from "@revstackhq/node";

const revstack = new Revstack({ secretKey: process.env.REVSTACK_SECRET_KEY });

export async function requireActiveSubscription(userId: string) {
  const subs = await revstack.subscriptions.list({ customerId: userId });
  const activeSub = subs.data.find((s) => s.status === "active" || s.status === "trialing");

  if (!activeSub) {
    throw new Error("No active subscription. Please upgrade.");
  }

  return activeSub;
}

Trial-to-Paid Conversion

Track conversion from trial to paid:
// In your webhook handler
if (event.type === "subscription.updated") {
  const { status, trialEnd } = event.data;

  // Customer converted from trial to paid
  if (status === "active" && trialEnd && new Date(trialEnd) < new Date()) {
    await analytics.track({
      userId: event.data.customerId,
      event: "Trial Converted",
      properties: {
        planId: event.data.planId,
      },
    });
  }
}

Self-Service Billing Portal

Let customers manage their own subscriptions:
import { Revstack } from "@revstackhq/node";

const revstack = new Revstack({ secretKey: process.env.REVSTACK_SECRET_KEY });

// Create a billing portal session
const portal = await revstack.portal.create({
  customerId: "usr_abc123",
  returnUrl: "https://myapp.com/settings/billing",
});

// Redirect user to portal
res.redirect(portal.url);
The portal lets customers:
  • Update payment methods
  • Change plans
  • View invoices
  • Cancel subscriptions
Portal availability depends on your payment provider. Stripe and Polar both offer hosted billing portals.

Next Steps

Providers

Learn how payment providers integrate with Revstack

Entitlements

Control feature access based on subscription status

Build docs developers (and LLMs) love