Billing events are financial records. When a subscription is created, upgraded, or canceled and nothing is logged, the only audit history is Stripe's own event log — which you can query through the Stripe Dashboard but cannot correlate with your application's user IDs, feature usage, or support tickets. SOC 2 CC7.2 (System Monitoring) and ISO 25010 operability both require that significant system events produce observable records. Without application-level billing logs, diagnosing a customer complaint about an incorrect charge or missed downgrade requires manual cross-referencing between Stripe and your database.
Low because missing billing logs don't create immediate security or data integrity risk, but significantly impair incident investigation and financial reconciliation.
Add structured logging to every significant billing event in your webhook handler — both success and failure events. Application logs are required; Stripe's event log alone does not satisfy this check.
case 'customer.subscription.updated': {
const subscription = event.data.object
console.log(JSON.stringify({
event: 'billing.subscription_updated',
stripe_event_id: event.id,
customer_id: subscription.customer,
status: subscription.status,
plan: subscription.items.data[0]?.price.lookup_key,
timestamp: new Date().toISOString()
}))
// handle the event
}
For a durable audit trail, insert a row into a billing_events table on each significant event. This enables SQL queries like "show me all plan changes for user X in the last 90 days" without touching the Stripe Dashboard.
ID: saas-billing.financial-data.billing-audit-trail
Severity: low
What to look for: Look for logging or event recording in the billing flow. Check whether significant billing events (subscription created, subscription canceled, plan changed, payment failed, refund issued) are logged to an application log, stored in a database events/audit table, or sent to an observability platform. Look for structured logging in webhook handlers and billing API routes.
Pass criteria: Count every webhook handler case — at least 2 event types must be logged. BOTH success events (e.g., subscription created, payment received) AND failure events (e.g., payment failed, invoice overdue) are logged with enough context to reconstruct what happened (event type, user ID, plan, amount, timestamp). Logging only failures — such as console.error on payment failure but no log on subscription creation — does not pass. This can be application logs (console.log with structure), a dedicated audit log table in the database, or an observability service (Sentry, Datadog, etc.). Stripe's own event log counts as a partial audit trail but application-level logging is required for a pass.
Fail criteria: Webhook handlers log only failure events (e.g., only console.error on failures) but silently process successes. Or webhook handlers process all events with no logging at all. Billing state changes (plan upgrades, subscription creation, successful payments) are invisible in application logs.
Skip (N/A) when: No billing integration detected.
Detail on fail: "Webhook handler processes customer.subscription.updated and customer.subscription.deleted events with no logging — state changes are invisible" or "No billing event logging found — only Stripe Dashboard provides any audit history"
Remediation: Add structured logging to your webhook handler:
case 'customer.subscription.updated': {
const subscription = event.data.object
console.log(JSON.stringify({
event: 'subscription.updated',
stripe_event_id: event.id,
customer_id: subscription.customer,
status: subscription.status,
plan: subscription.items.data[0]?.price.lookup_key,
timestamp: new Date().toISOString()
}))
// ... handle the event
}
For more durable audit trails, insert a row into an audit_log or billing_events table on each significant event.