ScaleServices-as-Software
Pricing & Billing Integration
Integration with payment systems and billing automation
Learn how to integrate pricing and billing into your Services-as-Software for automated revenue collection.
flowchart LR
Usage[Track Usage] --> Meter[Meter Events]
Meter --> Period[Billing Period End]
Period --> Calc[Calculate Charges]
Calc --> Invoice[Generate Invoice]
Invoice --> Payment[Process Payment]
Payment --> Success{Success?}
Success -->|Yes| Record[Record Payment]
Success -->|No| Retry[Retry Payment]
Retry --> Attempts{Attempts Left?}
Attempts -->|Yes| Payment
Attempts -->|No| Suspend[Suspend Service]
Record --> Email[Email Receipt]
Email --> Done[Complete]
Payment Providers
Stripe Integration
Integrate with Stripe for payment processing:
export default service({
name: 'My Service',
pricing: {
model: 'subscription',
provider: 'stripe',
plans: [
{
name: 'Pro',
price: 99,
interval: 'month',
stripeProductId: 'prod_xxx',
stripePriceId: 'price_xxx',
},
],
},
on: {
'$.Customer.subscribed': async (event) => {
const customer = event.data
// Create Stripe subscription
const subscription = await stripe.subscriptions.create({
customer: customer.stripeCustomerId,
items: [{ price: customer.plan.stripePriceId }],
trial_period_days: 14,
})
// Store subscription
await customer.update({
subscriptionId: subscription.id,
status: '$.Active',
})
},
'$.stripe.invoice.paid': async (event) => {
const invoice = event.data
// Record payment
await $.Payment.create({
customerId: invoice.customer,
amount: invoice.amount_paid,
invoiceId: invoice.id,
status: '$.Paid',
})
},
'$.stripe.payment_failed': async (event) => {
const invoice = event.data
// Handle failed payment
await $.Email.send({
to: invoice.customer_email,
template: 'payment-failed',
data: { invoice },
})
// Retry payment
await $.schedule('3 days', async () => {
await stripe.invoices.pay(invoice.id)
})
},
},
})PayPal Integration
Accept PayPal payments:
pricing: {
model: 'subscription',
provider: 'paypal',
plans: [
{
name: 'Pro',
price: 99,
interval: 'month',
paypalPlanId: 'P-xxx',
},
],
}
on: {
'$.Customer.subscribed': async (event) => {
// Create PayPal subscription
const subscription = await paypal.subscriptions.create({
plan_id: event.data.plan.paypalPlanId,
subscriber: {
email_address: event.data.email,
},
})
await event.data.update({
paypalSubscriptionId: subscription.id,
})
},
}Usage-Based Billing
Metering Usage
Track and bill for usage:
export default service({
name: 'API Service',
pricing: {
model: 'usage',
metric: 'api_calls',
provider: 'stripe',
tiers: [
{ up_to: 1000, price_per_unit: 0.1 },
{ up_to: 10000, price_per_unit: 0.08 },
{ above: 10000, price_per_unit: 0.05 },
],
},
on: {
'$.API.request': async (event) => {
// Process request
const result = await processRequest(event.data)
// Record usage
await stripe.billing.meterEvents.create({
event_name: 'api_calls',
payload: {
value: 1,
stripe_customer_id: event.data.customerId,
},
})
return result
},
},
})Usage Reports
Generate usage reports for customers:
api: {
'GET /usage': async (req) => {
const customerId = req.headers.get('x-customer-id')
const period = req.url.searchParams.get('period') || 'current_month'
// Query usage
const usage = await $.usage.query({
customerId,
period,
groupBy: 'day',
})
// Calculate cost
const cost = calculateCost(usage, pricingTiers)
return {
usage: usage.total,
breakdown: usage.byDay,
cost,
period,
}
},
}Usage Alerts
Alert customers about usage:
on: {
'$.Usage.recorded': async (event) => {
const customer = await $.Customer.findById(event.data.customerId)
const usage = await $.usage.query({
customerId: customer.id,
period: 'current_month',
})
const limit = customer.plan.limits?.monthly || Infinity
const percent = (usage.total / limit) * 100
// Alert at 80%, 90%, 100%
if (percent >= 80 && !customer.alerts?.usage80) {
await $.Email.send({
to: customer.email,
template: 'usage-alert-80',
data: { usage: usage.total, limit, percent },
})
await customer.update({
'alerts.usage80': true,
})
}
},
}Invoicing
Invoice Generation
Automatically generate invoices:
on: {
'$.Billing.period-end': async (event) => {
const customers = await $.Customer.findMany({
where: { status: '$.Active' },
})
for (const customer of customers) {
// Calculate charges
const usage = await $.usage.query({
customerId: customer.id,
period: 'last_month',
})
const charges = calculateCharges(customer.plan, usage)
// Create invoice
const invoice = await $.Invoice.create({
customerId: customer.id,
period: 'last_month',
items: [
{
description: 'Subscription',
amount: customer.plan.price,
},
{
description: 'Usage overage',
amount: charges.overage,
quantity: usage.overage,
},
],
subtotal: charges.subtotal,
tax: charges.tax,
total: charges.total,
dueDate: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000),
})
// Send invoice
await $.Email.send({
to: customer.email,
template: 'invoice',
attachments: [
{
filename: `invoice-${invoice.number}.pdf`,
content: await generateInvoicePDF(invoice),
},
],
})
}
},
}Invoice Payment
Process invoice payments:
on: {
'$.Invoice.created': async (event) => {
const invoice = event.data
// Attempt automatic payment
try {
const payment = await stripe.invoices.pay(invoice.stripeInvoiceId)
await invoice.update({
status: '$.Paid',
paidAt: new Date(),
paymentMethod: payment.payment_method,
})
} catch (error) {
// Payment failed
await invoice.update({
status: '$.PaymentFailed',
lastError: error.message,
})
// Send payment link
await $.Email.send({
to: invoice.customer.email,
template: 'invoice-payment-required',
data: {
invoice,
paymentLink: invoice.hostedInvoiceUrl,
},
})
}
},
'$.Invoice.overdue': async (event) => {
const invoice = event.data
// Send overdue notice
await $.Email.send({
to: invoice.customer.email,
template: 'invoice-overdue',
data: { invoice },
})
// Suspend service after 7 days overdue
const daysSincedue = Math.floor(
(Date.now() - invoice.dueDate.getTime()) / (24 * 60 * 60 * 1000)
)
if (daysSincedue >= 7) {
await $.Customer.update(invoice.customerId, {
status: '$.Suspended',
suspensionReason: 'overdue-payment',
})
}
},
}Subscription Management
stateDiagram-v2
[*] --> Trial
Trial --> Active: Payment Success
Trial --> Expired: Trial End
Active --> PastDue: Payment Failed
PastDue --> Active: Payment Recovered
PastDue --> Suspended: Retry Failed
Active --> Cancelled: User Cancels
Suspended --> Active: Payment Recovered
Suspended --> Cancelled: Grace Period End
Cancelled --> [*]
Expired --> [*]
note right of Trial
14-30 day trial
end note
note right of PastDue
Retry payment 3x
end note
note right of Suspended
7 day grace period
end note
Subscription Creation
Create customer subscriptions:
api: {
'POST /subscribe': async (req) => {
const { planId, paymentMethodId } = await req.json()
const customerId = req.headers.get('x-customer-id')
const customer = await $.Customer.findById(customerId)
const plan = await $.Plan.findById(planId)
// Create subscription in Stripe
const subscription = await stripe.subscriptions.create({
customer: customer.stripeCustomerId,
items: [{ price: plan.stripePriceId }],
payment_behavior: 'default_incomplete',
payment_settings: {
payment_method_types: ['card'],
},
expand: ['latest_invoice.payment_intent'],
})
// Store subscription
await customer.update({
plan: planId,
subscriptionId: subscription.id,
status: '$.Active',
})
return {
subscriptionId: subscription.id,
clientSecret: subscription.latest_invoice.payment_intent.client_secret,
}
},
}Subscription Updates
Handle plan changes:
api: {
'POST /change-plan': async (req) => {
const { newPlanId } = await req.json()
const customerId = req.headers.get('x-customer-id')
const customer = await $.Customer.findById(customerId)
const newPlan = await $.Plan.findById(newPlanId)
// Update subscription
const subscription = await stripe.subscriptions.update(
customer.subscriptionId,
{
items: [
{
id: customer.subscriptionItemId,
price: newPlan.stripePriceId,
},
],
proration_behavior: 'create_prorations',
}
)
// Update customer
await customer.update({
plan: newPlanId,
updatedAt: new Date(),
})
return { subscription }
},
}Subscription Cancellation
Handle cancellations:
api: {
'POST /cancel': async (req) => {
const { immediate } = await req.json()
const customerId = req.headers.get('x-customer-id')
const customer = await $.Customer.findById(customerId)
if (immediate) {
// Cancel immediately
await stripe.subscriptions.cancel(customer.subscriptionId)
await customer.update({
status: '$.Cancelled',
cancelledAt: new Date(),
})
} else {
// Cancel at period end
await stripe.subscriptions.update(customer.subscriptionId, {
cancel_at_period_end: true,
})
await customer.update({
status: '$.CancellingAtPeriodEnd',
cancelsAt: customer.subscriptionPeriodEnd,
})
}
return { cancelled: true }
},
}Proration
Handle Prorations
Calculate prorated charges:
async function calculateProration(customer, newPlan) {
const now = Date.now()
const periodEnd = customer.subscriptionPeriodEnd.getTime()
const periodStart = customer.subscriptionPeriodStart.getTime()
// Time remaining in period
const remainingDays = (periodEnd - now) / (24 * 60 * 60 * 1000)
const totalDays = (periodEnd - periodStart) / (24 * 60 * 60 * 1000)
// Calculate proration
const oldPlanDaily = customer.plan.price / totalDays
const newPlanDaily = newPlan.price / totalDays
const credit = oldPlanDaily * remainingDays
const charge = newPlanDaily * remainingDays
return {
credit,
charge,
net: charge - credit,
}
}Discounts and Coupons
Create Coupons
Offer discounts:
// Create coupon
const coupon = await stripe.coupons.create({
percent_off: 25,
duration: 'repeating',
duration_in_months: 3,
name: 'WELCOME25',
})
// Apply coupon
api: {
'POST /apply-coupon': async (req) => {
const { couponCode } = await req.json()
const customerId = req.headers.get('x-customer-id')
const customer = await $.Customer.findById(customerId)
// Apply to subscription
await stripe.subscriptions.update(customer.subscriptionId, {
coupon: couponCode,
})
return { applied: true }
},
}Volume Discounts
Automatic volume-based discounts:
function calculateDiscount(usage) {
if (usage > 100000) {
return 0.20 // 20% off
} else if (usage > 10000) {
return 0.10 // 10% off
}
return 0
}
on: {
'$.Usage.calculated': async (event) => {
const { customerId, usage } = event.data
const discount = calculateDiscount(usage.total)
if (discount > 0) {
await $.Email.send({
to: event.data.customer.email,
template: 'volume-discount',
data: {
usage: usage.total,
discount: discount * 100,
},
})
}
},
}Tax Handling
Automatic Tax Calculation
Calculate sales tax:
on: {
'$.Invoice.created': async (event) => {
const invoice = event.data
const customer = await $.Customer.findById(invoice.customerId)
// Calculate tax using Stripe Tax
const tax = await stripe.tax.calculations.create({
currency: 'usd',
line_items: invoice.items.map((item) => ({
amount: item.amount,
reference: item.description,
})),
customer_details: {
address: customer.address,
address_source: 'billing',
},
})
// Update invoice with tax
await invoice.update({
tax: tax.tax_amount_exclusive,
total: invoice.subtotal + tax.tax_amount_exclusive,
})
},
}Revenue Recognition
Track Revenue
Record revenue properly:
on: {
'$.Payment.received': async (event) => {
const payment = event.data
// Record revenue
await $.Revenue.create({
amount: payment.amount,
customerId: payment.customerId,
recognizedAt: new Date(),
period: getCurrentPeriod(),
type: payment.type, // 'subscription' | 'usage' | 'one-time'
})
// Update MRR if subscription
if (payment.type === 'subscription') {
await $.Metric.set('mrr', {
customerId: payment.customerId,
value: payment.amount,
})
}
},
}Best Practices
Transparent Pricing
Always show customers what they'll pay:
api: {
'GET /pricing/estimate': async (req) => {
const planId = req.url.searchParams.get('plan')
const usage = parseInt(req.url.searchParams.get('usage') || '0')
const plan = await $.Plan.findById(planId)
const charges = calculateCharges(plan, usage)
return {
subscription: plan.price,
usage: charges.usage,
tax: charges.tax,
total: charges.total,
breakdown: charges.breakdown,
}
},
}Failed Payment Handling
Handle failures gracefully:
on: {
'$.Payment.failed': async (event) => {
const payment = event.data
const customer = await $.Customer.findById(payment.customerId)
// Retry schedule: 3 days, 7 days, 14 days
const retries = [3, 7, 14]
for (const days of retries) {
await $.schedule(`${days} days`, async () => {
try {
await stripe.invoices.pay(payment.invoiceId)
} catch (error) {
// Still failing
if (days === 14) {
// Final attempt failed - suspend
await customer.update({ status: '$.Suspended' })
}
}
})
}
},
}Audit Trail
Maintain billing audit trail:
on: {
'$.Payment.processed': async (event) => {
await $.AuditLog.create({
type: 'payment.processed',
customerId: event.data.customerId,
amount: event.data.amount,
metadata: event.data,
timestamp: new Date(),
})
},
}Next Steps
- Monetization - Monetization strategies
- Examples - Billing examples
- Best Practices - Billing best practices