Easy Labs
SDKsRubyExamples

Subscription System

Subscription System pattern for Ruby.

Recipe for standing up a recurring-billing system on top of client.products, client.product_prices, and client.subscriptions — catalog setup, customer signup, mid-cycle upgrades, and cancellation.

Goal

Run a SaaS-style subscription where:

  1. Catalog is defined once (one Product, multiple Prices).
  2. Customers sign up to a price; payment instrument is on file.
  3. Mid-cycle upgrades use proration preview before applying.
  4. Cancellations honor "end of period" by default.
  5. Lifecycle webhooks keep your local state in sync.

Implementation

1. Define the catalog (one-time)

client = EasyLabs::Client.new(api_key: ENV.fetch("EASY_API_KEY"))

product = client.products.create(name: "Pro plan")

monthly = client.product_prices.create(
  product_id:     product[:id],
  active:         true,
  recurring:      true,
  currency:       "USD",
  unit_amount:    4_900,
  interval:       "month",
  interval_count: 1,
  tax_behavior:   "exclusive"
)

annual = client.product_prices.create(
  product_id:     product[:id],
  active:         true,
  recurring:      true,
  currency:       "USD",
  unit_amount:    49_900,
  interval:       "year",
  interval_count: 1,
  tax_behavior:   "exclusive"
)

2. Sign up a customer

customer = client.customers.create(
  first_name: "Ada", last_name: "Lovelace", email: "ada@example.com"
)

card = client.payment_instruments.create(
  tokenId:    token_from_browser,
  identityId: customer[:id],
  type:       "PAYMENT_CARD",
  name:       "Personal card"
)

sub = client.subscriptions.create(
  identity_id:   customer[:id],
  items:         [{ price_id: monthly[:id] }],
  instrument_id: card[:id]
)

3. Preview, then upgrade

preview = client.subscriptions.proration_preview(
  sub[:id],
  items: [{ price_id: annual[:id], quantity: 1 }]
)

if preview[:total] <= max_charge_cents
  client.subscriptions.update(sub[:id], items: [{ price_id: annual[:id] }])
end

4. Cancel at period end

client.subscriptions.cancel(sub[:id], at_period_end: true)

5. Sync state from webhooks

post "/webhooks/easy" do
  event = EasyLabs::Webhooks.construct_event(
    payload:   request.body.read,
    signature: request.headers["X-Easy-Webhook-Signature"],
    secret:    ENV.fetch("EASY_WEBHOOK_SECRET")
  )

  data = event[:data]

  case event[:type]
  when "subscription.created", "subscription.updated"
    Subscription.upsert_by_easy_id(data)
  when "subscription.deleted"
    Subscription.find_by(easy_id: data[:id])&.update!(state: "canceled")
  when "subscription.paused"
    Subscription.find_by(easy_id: data[:id])&.update!(state: "paused")
  when "subscription.resumed"
    Subscription.find_by(easy_id: data[:id])&.update!(state: "active")
  when "invoice.paid"
    Invoice.upsert_by_easy_id(data)
  when "invoice.payment_failed"
    DunningMailer.with(invoice: data).failed.deliver_later
  end

  status 204
end

Tradeoffs

  • Always preview prorations. proration_preview is cheap and lets you show the customer (and verify against your business rules) what an upgrade actually costs before you charge them.
  • Cancel at period end vs. immediately. Defaulting to at_period_end: true is friendlier and matches most customers' expectations — passing false cancels immediately and forfeits the remainder of the period.
  • Source of truth. Treat the Easy API as the source of truth for subscription state and use webhooks to keep your local cache fresh. Avoid recording subscription state synchronously from the create call — the create response and a subsequent webhook can race.
  • Metered billing. For usage-based pricing, layer report_usage calls into your existing event pipeline rather than doing it inline at request time. See Subscriptions › Metered usage.

On this page