Easy Labs
SDKsNode.jsExamples

Subscription System

Subscription System pattern for Node.js.

A complete subscription system on @easylabs/node: pricing catalog, signup with trial, in-flight upgrade with proration preview, metered usage reporting, dunning, and cancellation at period end.

Goal

Everything you need to ship a B2B SaaS with monthly + yearly tiers, optional metered add-ons, and graceful failed-payment handling — all driven by the SDK plus webhooks.

Implementation

1. Catalog

const product = await easy.createProduct({ name: "Pro plan", active: true });

const monthly = await easy.createPrice({
  product_id: product.data.id,
  active: true,
  recurring: true,
  currency: "USD",
  unit_amount: 4900,
  interval: "month",
  interval_count: 1,
  tax_behavior: "exclusive",
  trial_period_days: 14,
});

const yearly = await easy.createPrice({
  product_id: product.data.id,
  active: true,
  recurring: true,
  currency: "USD",
  unit_amount: 49000, // $490 — 2 months free
  interval: "year",
  interval_count: 1,
  tax_behavior: "exclusive",
});

const meteredApiCalls = await easy.createPrice({
  product_id: product.data.id,
  active: true,
  recurring: true,
  currency: "USD",
  unit_amount: 1, // $0.01 per unit
  interval: "month",
  interval_count: 1,
  tax_behavior: "exclusive",
  pricing_model: "metered",
});

2. Signup with a 14-day trial

const sub = await easy.createSubscription({
  identity_id: customer.id,
  items: [
    { price_id: monthly.data.id, quantity: 1 },
    { price_id: meteredApiCalls.data.id }, // qty driven by usage reports
  ],
  instrument_id: paymentInstrumentId,
  trial_period_days: 14,
  metadata: { plan: "pro_monthly" },
});

3. Upgrade with proration preview

const preview = await easy.getSubscriptionProrationPreview(sub.data.id, {
  items: [{ price_id: yearly.data.id, quantity: 1 }],
  remove_items: [
    sub.data.items.find((i) => i.price_id === monthly.data.id)!.id,
  ],
});

// Show preview.data to the user, then commit:
await easy.updateSubscription(sub.data.id, {
  items: [{ price_id: yearly.data.id, quantity: 1 }],
  remove_items: [
    sub.data.items.find((i) => i.price_id === monthly.data.id)!.id,
  ],
  proration_behavior: "create_prorations",
});

4. Metered usage

import { randomUUID } from "node:crypto";

async function reportApiCall(subscriptionId: string, itemId: string) {
  await easy.reportSubscriptionUsage(subscriptionId, {
    subscription_item_id: itemId,
    quantity: 1,
    action: "increment",
    timestamp: new Date().toISOString(),
    idempotency_key: randomUUID(),
  });
}

// Period summary for an in-app dashboard:
const summary = await easy.getSubscriptionUsageSummary(sub.data.id, {
  subscription_item_id: meteredItemId,
  from: periodStart,
  to: periodEnd,
});

5. Dunning

await easy.createOrReplaceDunningConfig({
  retry_mode: "smart",
  smart_retry_attempts: 8,
  smart_retry_window: "2_weeks",
  subscription_terminal_action: "cancel",
  invoice_terminal_action: "uncollectible",
  payment_failed_email_enabled: true,
  expiring_card_email_enabled: true,
  card_expiry_warn_days: 30,
});

Wire your webhook handler to react to invoice.payment_failed, subscription.paused, and subscription.deleted — see Webhooks.

6. Cancellation at period end

await easy.cancelSubscription(sub.data.id, { at_period_end: true });

7. Discount on retention

const winback = await easy.createCoupon({
  duration: "repeating",
  duration_in_months: 3,
  percent_off: 50,
  name: "Winback offer",
});
await easy.applySubscriptionDiscount(sub.data.id, { coupon_id: winback.data.id });

Tradeoffs

  • Trial accounting. Pass trial_period_days (or trial_end) at create time and set instrument_id so the card is on file for the first paid charge. Listen for subscription.trial_will_end to nudge users 3 days out.
  • Proration policy. "create_prorations" issues credits/charges immediately; "none" defers everything to the next invoice. Preview first if your users are price-sensitive.
  • Metered idempotency. Reuse the upstream event ID (e.g. webhook delivery ID) as idempotency_key so retries don't double-bill.
  • Cancellation timing. at_period_end: true keeps service active until renewal; omit it for instant termination + final invoice.
  • Multiple plans on one subscription. A single SubscriptionData may carry many items (e.g. base seat + metered calls + storage). Use addSubscriptionItem / removeSubscriptionItem for granular changes.

On this page