How do I update the Stripe Billing & Subscription for PACT/Excise Bundle Customers
Overview
This document describes the implementation of automated Stripe catalog and subscription management for Token of Trust's PACT/Excise compliance bundle. The implementation provides a single, authoritative source for pricing configuration and subscription management, replacing manual Stripe Dashboard operations with code-driven, auditable, and repeatable processes.
Problem Statement
Token of Trust offers a compliance bundle with complex, multi-dimensional pricing:
- Base Bundle Pricing: Two-dimensional matrix (number of reports × order volume)
- Up to 15, 25, or 50 reports per month
- 7 order volume tiers (0-999, 1000-2499, etc.)
- 21 distinct base price points
- Usage-Based Charges: Three metered components
- Order Volume: Volume tiers with flat fees based on monthly snapshot
- Active SKUs: Volume tiers with flat fees (26 tiers, 0-16000+)
- Ad-hoc Reports: Simple per-report charge ($200/report)
- Fixed Add-ons: Monthly subscription add-ons
- Product Sync ($250/mo) - included free with bundle
- Order Sync ($250/mo) - included free with bundle
- Complex Requirements:
- Manual Stripe Dashboard management was error-prone and time-consuming
- No version control for pricing changes
- Difficult to ensure consistency across customers
- Hard to migrate customers between pricing tiers
- No audit trail for subscription changes
Solution Architecture
Core Components
stripe_billing.node.js- Main automation script- Seed/update Stripe catalog (products, prices, coupons)
- Create/update customer subscriptions
- Idempotent operations (safe to run multiple times)
- Dry-run mode for all operations
stripe_billing.md- Comprehensive documentation- Usage guide with scenarios
- Troubleshooting guide
- Governance and naming conventions
- Migration strategies
- This document
- Architecture decisions
- Design rationale
- Future roadmap
Key Design Decisions
1. Configuration as Code
Decision: All pricing, products, and business rules live in a single CONFIG object at the top of the script.
Rationale:
- Single source of truth for pricing
- Version controlled in Git with PR review
- Easy to diff pricing changes
- Can be extracted to JSON for non-engineer access (future roadmap)
Example:
const CONFIG = {
baseMatrix: {
reportsBands: ['r15', 'r16_25', 'r26_50'],
ordersBands: ['o0_999', 'o1000_2499', ...],
amounts: {
r15: { o0_999: 3250.00, o1000_2499: 3250.00, ... }
}
},
usage: {
orders: { /* volume tiers */ },
activeSkus: { /* 26 volume tiers */ }
}
}
2. Lookup Keys for Idempotency
Decision: Use Stripe's lookup_key feature for all prices and metadata.lookup_key for products.
Rationale:
- Enables idempotent operations (running twice doesn't create duplicates)
- Allows finding items by meaningful names instead of Stripe IDs
- Supports price versioning (e.g.,
price-bdl-orders-volume-v1,v2) - Prevents accidental overwrites
Naming Convention:
- Products:
product-bdl-<component>(e.g.,product-bdl-bundle_base) - Prices:
price-bdl-<component>-<version>(e.g.,price-bdl-base-r15-o0_999) - All bundle components use
bdl-prefix for consistency
3. Automatic Included Add-ons via Coupon
Decision: Product Sync and Order Sync are added to all subscriptions but made free via a 100% coupon.
Rationale:
- Shows the value of included add-ons on invoices ($250/mo each = $500/mo value)
- Single consistent subscription structure for all customers
- Coupon ("Included with Bundle") clearly communicates the benefit
- Easier to support and troubleshoot
- Can remove coupon for specific customers who want to pay separately (future flexibility)
Implementation:
// Coupon automatically created during seed-catalog
{
id: 'bdl-addons-included',
name: 'Included with Bundle',
percent_off: 100,
applies_to: { products: [product_sync, order_sync] }
}
4. Volume Tiers with flat_amount
Decision: Use Stripe's volume tier pricing with flat_amount (not unit_amount) for Order Volume and Active SKUs.
Rationale:
- Charges a flat fee based on which tier the customer lands in at end of month
- Customer pays for the tier reached, not per-unit within the tier
- Example: Customer with 3,200 SKUs pays $1,345.80 flat (not 3,200 × some rate)
- Matches contract pricing structure
- Simplifies invoicing and customer understanding
Stripe API Structure:
{
tiers_mode: 'volume',
aggregate_usage: 'last_during_period', // snapshot at month end
tiers: [
{ up_to: 249, flat_amount: 0 }, // 0-249 SKUs: free
{ up_to: 499, flat_amount: 0 }, // 250-499 SKUs: free
{ up_to: 749, flat_amount: 124.50 }, // 500-749 SKUs: $124.50 flat
...
]
}
5. Migration-Safe Subscription Updates
Decision: Check for existing subscription items by both product lookup key AND price ID.
Rationale:
- Handles migration from old product naming (without
bdl-prefix) to new - Prevents duplicate items when product keys change but prices stay the same
- Allows gradual migration without breaking existing subscriptions
Implementation:
// Try product lookup key first
let item = currentItems.find(ci => ci.product?.metadata?.lookup_key === wantKey)?.si;
// Fall back to price ID (migration scenario)
if (!item) {
item = currentItems.find(ci => ci.price.id === wantPriceId)?.si;
}
6. Dry-Run Mode for All Operations
Decision: Every operation supports --dry-run flag that shows what would happen without making changes.
Rationale:
- Safety: Preview changes before applying
- Debugging: Understand what script will do
- Confidence: Run in production without fear
- Documentation: Dry-run output serves as change documentation
Usage Patterns
Initial Setup
# 1. Install dependencies
npm install stripe config
# 2. Configure Stripe API key
export STRIPE_SECRET=sk_test_...
# OR set in config/default.json: verifications.stripe.secretKey
# 3. Seed catalog (creates all products, prices, coupon)
NODE_ENV=test node scripts/stripe/stripe_billing.node.js seed-catalog --dry-run
NODE_ENV=test node scripts/stripe/stripe_billing.node.js seed-catalog
Managing Customer Subscriptions
# Preview what would be created/updated
node scripts/stripe/stripe_billing.node.js ensure-subscription \
--customer mike@1541retail.com \
--base-tier price-bdl-base-r15-o0_999 \
--dry-run
# Create or update subscription
node scripts/stripe/stripe_billing.node.js ensure-subscription \
--customer mike@1541retail.com \
--base-tier price-bdl-base-r15-o0_999 \
--create-if-missing
Updating Pricing
# 1. Update CONFIG in stripe_billing.node.js
# 2. Bump version (e.g., price-bdl-orders-volume-v1 → v2)
# 3. Seed catalog
NODE_ENV=test node scripts/stripe/stripe_billing.node.js seed-catalog
# 4. Migrate customers to new prices (repeat for each)
node scripts/stripe/stripe_billing.node.js ensure-subscription \
--customer cus_XXX \
--base-tier price-bdl-base-v2-r15-o0_999
What Gets Created in Stripe
Products (6 total)
- PACT/Excise Bundle Base Subscription (
product-bdl-bundle_base) - Order Volume (
product-bdl-orders_volume) - Active SKUs (
product-bdl-active_skus) - Ad-hoc Reports (
product-bdl-ad_hoc_reports) - Product Sync AddOn (
product-bdl-addon-product_sync) - includes "[INCLUDED WITH BUNDLE]" in description - Order Sync AddOn (
product-bdl-addon-order_sync) - includes "[INCLUDED WITH BUNDLE]" in description
Prices (50+ total)
- 21 Base Bundle prices (3 report tiers × 7 order tiers)
price-bdl-base-r15-o0_999throughprice-bdl-base-r26_50-o7000_8499
- 3 Usage prices
price-bdl-orders-volume-v1(17 volume tiers)price-bdl-skus-volume-v1(26 volume tiers)price-bdl-adhoc-per_report-v1(simple $200/report)
- 2 Add-on prices
price-bdl-addon-product_sync-monthly-v1($250/mo)price-bdl-addon-order_sync-monthly-v1($250/mo)
Coupon (1 total)
- bdl-addons-included ("Included with Bundle")
- 100% off forever
- Applies to: Product Sync and Order Sync products
- Promotion Code:
BDL-ADDONS-INCLUDED
Typical Subscription Structure
Every customer subscription has 6 items:
- Base Bundle (1 of 21 prices, depending on tier)
- Order Volume (metered, volume tiers)
- Active SKUs (metered, volume tiers)
- Ad-hoc Reports (metered, per-report)
- Product Sync AddOn (fixed $250/mo, 100% off via coupon)
- Order Sync AddOn (fixed $250/mo, 100% off via coupon)
Testing Strategy
Dry-Run First, Always
# Test catalog changes
node scripts/stripe/stripe_billing.node.js seed-catalog --dry-run
# Test subscription changes
node scripts/stripe/stripe_billing.node.js ensure-subscription \
--customer test@example.com \
--base-tier price-bdl-base-r15-o0_999 \
--dry-run
Test Environment
# Use NODE_ENV=test to load test Stripe keys from config
NODE_ENV=test node scripts/stripe/stripe_billing.node.js seed-catalog
Validation Checklist
After seeding catalog:
- Check Stripe Dashboard: 6 products exist
- Check Stripe Dashboard: 50+ prices exist with correct amounts
- Check Stripe Dashboard: Coupon
bdl-addons-includedexists - Verify promotion code
BDL-ADDONS-INCLUDEDis active
After creating subscription:
- Check Stripe Dashboard: Subscription has 6 items
- Verify base tier price matches expected amount
- Verify Product Sync and Order Sync show $0 (coupon applied)
- Verify coupon "Included with Bundle" shows at bottom
- Check invoice preview shows correct line items
Operational Considerations
When to Run seed-catalog
Run seed-catalog when:
- Initial setup (first time)
- Adding new products/add-ons
- Updating pricing (with new version numbers)
- Updating product descriptions
Safe to run multiple times - idempotent operations won't create duplicates.
When to Run ensure-subscription
Run ensure-subscription when:
- Creating new customer subscription
- Changing customer's base tier
- Migrating customer to new price version
- Ensuring subscription matches desired state
Safe to run multiple times - only applies needed changes.
Handling Pricing Changes
Option 1: New Version (Recommended)
- Update CONFIG with new version (e.g.,
price-bdl-orders-volume-v2) - Run
seed-catalogto create new prices - Existing customers stay on v1 until explicitly migrated
- New customers get v2
Option 2: Force Replace (Use Sparingly)
- Update CONFIG amounts for same lookup_key
- Run
seed-catalog --force-replace-lookup-keys - Script re-keys old price and creates new one with same lookup_key
- Requires explicit customer migration
Monitoring and Alerts
Recommended monitoring:
- Track
seed-catalogruns in deployment logs - Alert on
ensure-subscriptionfailures - Monitor for pricing mismatches (script warns on dry-run)
- Track coupon application (script logs when applied)
Future Enhancements
Planned
- Externalize CONFIG to JSON
- Allow non-engineers to propose pricing changes
- Schema validation for safety
- CLI:
--config /path/to/billing.json
- Subscription ID Support
- Add
--subscription sub_XXXparameter - Support customers with multiple subscriptions
- More precise targeting for migrations
- Add
- Bulk Migration Tool
- Migrate cohorts of customers to new prices
- Scheduling windows to avoid rate limits
- Progress reporting and rollback capability
- Strict Mode for Add-ons
--strict-addonsflag to remove unlisted add-ons- Enforce canonical subscription structure
- Cleanup legacy items
- Usage Reporting Integration
- Automated posting of Order Volume usage
- Automated posting of Active SKUs snapshot
- Automated posting of Ad-hoc Report count
Considered but Deferred
- Two-way Sync - Export Stripe state back to JSON (auditing drift)
- Web UI - RevOps-friendly interface for subscription management
- Webhook Handlers - React to Stripe events (invoice.payment_failed, etc.)
Governance
Approvals Required
- Pricing Changes: Product + Finance approval
- Catalog Seeds: DevOps/Engineering execution
- Subscription Changes: Success/RevOps via script or Dashboard
Code Review Process
All changes to stripe_billing.node.js require:
- PR with clear description of pricing changes
- Reference this implementation doc in PR
- Finance approval for pricing updates
- Product approval for structural changes
- DevOps review for technical correctness
Documentation Updates
When updating pricing:
- Update
CONFIGinstripe_billing.node.js - Update examples in
stripe_billing.md - Update version history in CHANGELOG (if maintained)
- Create migration plan for existing customers
- Notify Finance/RevOps of changes
Troubleshooting
Common Issues
"Price not found" Error
- Run
seed-catalogfirst to create prices - Verify lookup_key matches CONFIG
"Can't add Price, already exists" Error
- Script now handles this via price ID fallback
- If persists, check for duplicate items in Stripe Dashboard
- Manually remove duplicate items or use dry-run to debug
"Coupon not found" Warning
- Run
seed-catalogto create coupon - Verify coupon
bdl-addons-includedexists in Dashboard
Subscription Has Old Items
- Script detects by price ID now, should skip duplicates
- If issues persist, manually clean old items via Dashboard
- Then run
ensure-subscriptionto add missing items
Debug Process
- Run with
--dry-runto see planned changes - Check script output for warnings/errors
- Verify Stripe Dashboard state matches expectations
- Check Stripe API logs for detailed error messages
- Review script's item detection logic (by product key + price ID)
Security Considerations
API Keys
- Never commit
STRIPE_SECRETto Git - Use environment variables or secure config management
- Separate test/production keys (use
NODE_ENV) - Rotate keys periodically
Access Control
- Limit who can run
ensure-subscription(financial impact) - Require
--dry-runreview before production runs - Log all script executions with user/timestamp
- Consider wrapper script with additional auth
Audit Trail
Every operation logs:
- Timestamp of execution
- Customer ID affected
- Changes made (create/update/skip)
- Dry-run vs. actual execution
Consider capturing this to structured logs or database.
Performance
Rate Limits
Stripe API rate limits:
- 100 requests/second in test mode
- 25 requests/second in live mode
Script design:
- Sequential operations (no parallel API calls)
- Respects Stripe rate limits naturally
- For bulk migrations, add delays between customers
Execution Time
Typical times:
seed-catalog: 30-60 seconds (50+ API calls)ensure-subscription: 5-10 seconds per customer
References
- Main Script:
scripts/stripe/stripe_billing.node.js - User Guide:
scripts/stripe/stripe_billing.md - Stripe API Docs: https://stripe.com/docs/api
- Volume Tier Pricing: https://stripe.com/docs/billing/subscriptions/tiers
- Lookup Keys: https://stripe.com/docs/api/prices/object#price_object-lookup_key
Change History
- 2025-01-XX: Initial implementation
- Created automation script for catalog and subscriptions
- Implemented
product-bdl-*andprice-bdl-*naming convention - Added automatic included add-ons via coupon
- Migration-safe subscription updates (check by price ID)
- Comprehensive documentation (this file + stripe_billing.md)
Document Maintained By: Engineering Team Last Updated: 2025-01-13 Version: 1.0