Headlessly
Billing

Price

Amount, currency, and billing interval -- maps directly to Stripe Prices.

Schema

import { Noun } from 'digital-objects'

export const Price = Noun('Price', {
  amount: 'number!',
  currency: 'string',
  interval: 'Monthly | Quarterly | Yearly | OneTime',
  intervalCount: 'number',
  originalAmount: 'number',
  discountPercent: 'number',
  active: 'boolean',
  plan: '-> Plan.prices',
  stripeId: 'string##',
})

Fields

FieldTypeRequiredDescription
amountnumberYesPrice in the smallest currency unit (e.g. cents -- 4900 = $49.00)
currencystringNoISO 4217 currency code (e.g. usd, eur, gbp). Defaults to usd
intervalenumNoBilling frequency: Monthly, Quarterly, Yearly, or OneTime
intervalCountnumberNoNumber of intervals between billings (e.g. 2 with Monthly = every 2 months)
originalAmountnumberNoOriginal price before discount (used for strikethrough display)
discountPercentnumberNoDiscount percentage applied (e.g. 17 for annual discount)
activebooleanNoWhether this price is currently available for new subscriptions
stripeIdstringNoStripe Price ID (unique, indexed)

Relationships

FieldDirectionTargetDescription
plan->Plan.pricesThe plan this price belongs to

Verbs

VerbEventDescription
createCreatedCreate a new price (also creates in Stripe)
updateUpdatedUpdate price fields (syncs to Stripe)
deleteDeletedSoft-delete the price

Verb Lifecycle

Every verb follows the full conjugation pattern -- execute, before hook, after hook:

import { Price } from '@headlessly/billing'

// Execute
await Price.create({
  amount: 4900,
  currency: 'usd',
  interval: 'Monthly',
  plan: 'plan_Nw8rTxJv',
  active: true,
})

// Before hook -- validate pricing
Price.creating(price => {
  console.log(`Creating price: ${price.amount} ${price.currency}/${price.interval}`)
})

// After hook -- confirm Stripe sync
Price.created(price => {
  console.log(`Price live with Stripe ID ${price.stripeId}`)
})

Billing Intervals

IntervalDescriptionCommon Use
MonthlyRecurring monthly chargeStandard SaaS pricing
QuarterlyRecurring every 3 monthsMid-term commitment discount
YearlyRecurring annual chargeAnnual commitment discount
OneTimeSingle purchase, no recurrenceSetup fees, lifetime deals, credits

Use intervalCount to customize beyond the standard intervals. For example, interval: 'Monthly' with intervalCount: 6 bills every 6 months.

Pricing Patterns

Monthly and Annual Pricing

The most common pattern -- offer both intervals with a discount for annual commitment:

import { Price } from '@headlessly/billing'

// Monthly price
await Price.create({
  amount: 4900,
  currency: 'usd',
  interval: 'Monthly',
  plan: 'plan_Nw8rTxJv',
  active: true,
})

// Annual price with 17% discount
await Price.create({
  amount: 49000,
  currency: 'usd',
  interval: 'Yearly',
  originalAmount: 58800,
  discountPercent: 17,
  plan: 'plan_Nw8rTxJv',
  active: true,
})

Multi-Currency

Support international pricing with separate Price entities per currency:

import { Price } from '@headlessly/billing'

await Price.create({
  amount: 4900,
  currency: 'usd',
  interval: 'Monthly',
  plan: 'plan_Nw8rTxJv',
  active: true,
})

await Price.create({
  amount: 4500,
  currency: 'eur',
  interval: 'Monthly',
  plan: 'plan_Nw8rTxJv',
  active: true,
})

await Price.create({
  amount: 3900,
  currency: 'gbp',
  interval: 'Monthly',
  plan: 'plan_Nw8rTxJv',
  active: true,
})

Cross-Domain Patterns

Prices feed into analytics for revenue metrics. Amount changes drive upgrade/downgrade calculations:

import { Subscription } from '@headlessly/billing'

// When a subscription upgrades, calculate revenue impact
Subscription.upgraded((sub, $) => {
  const newPlan = await $.Plan.get(sub.plan, { include: ['prices'] })
  const monthlyPrice = newPlan.prices.find(
    p => p.interval === 'Monthly' && p.active
  )
  $.Event.create({
    type: 'billing.upgrade_revenue',
    data: { subscription: sub.$id, newMRR: monthlyPrice?.amount },
  })
})

Query Examples

SDK

import { Price } from '@headlessly/billing'

// Find all active prices for a plan
const prices = await Price.find({ plan: 'plan_Nw8rTxJv', active: true })

// Get a specific price
const price = await Price.get('price_pQ8xNfKm')

// Find all monthly prices
const monthly = await Price.find({ interval: 'Monthly', active: true })

// Find discounted prices
const discounted = await Price.find({ discountPercent: { $gt: 0 } })

MCP

headless.ly/mcp#search
{ "type": "Price", "filter": { "plan": "plan_Nw8rTxJv", "active": true } }
headless.ly/mcp#fetch
{ "type": "Price", "id": "price_pQ8xNfKm" }
headless.ly/mcp#do
const prices = await $.Price.find({ plan: 'plan_Nw8rTxJv', active: true })
const yearly = prices.find(p => p.interval === 'Yearly')

REST

GET https://headless.ly/~acme/prices?plan=plan_Nw8rTxJv&active=true
GET https://headless.ly/~acme/prices/price_pQ8xNfKm
POST https://headless.ly/~acme/prices
Content-Type: application/json

{ "amount": 4900, "currency": "usd", "interval": "Monthly", "plan": "plan_Nw8rTxJv" }

Event-Driven Patterns

React to price changes:

import { Price } from '@headlessly/billing'

// Track price creation for revenue analytics
Price.created((price, $) => {
  $.Event.create({
    type: 'billing.price_created',
    data: {
      plan: price.plan,
      amount: price.amount,
      currency: price.currency,
      interval: price.interval,
    },
  })
})

// When a price is deactivated, check for affected subscriptions
Price.updated((price, $) => {
  if (!price.active) {
    $.Event.create({
      type: 'billing.price_deactivated',
      data: { priceId: price.$id, stripeId: price.stripeId },
    })
  }
})

Stripe Sync

Prices map directly to Stripe Price objects. The stripeId field stores the Stripe Price ID (price_...). Stripe Prices are immutable -- to change pricing, create a new Price and deactivate the old one. This preserves billing history for existing subscribers.

On this page