.do
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