Skip to main content

Stripe Integration

The Premium system uses Stripe webhooks to manage subscription lifecycle events. This document covers webhook configuration, event routing, and subscription handling.

Architecture​

Webhook Configuration​

StripeEvent Engine​

The application uses the stripe_event gem to handle webhooks. Configuration is in config/initializers/stripe_event.rb:

StripeEvent.signing_secret = ENV['STRIPE_WEBHOOK_SECRET_V2']

StripeEvent.configure do |events|
events.all do |event|
StripeWebhookJob.perform_later(event.as_json)
end
end

Route Configuration​

The webhook endpoint is mounted in config/routes.rb:

mount StripeEvent::Engine, at: '/stripe-webhook'

Webhook URL: https://your-domain.com/stripe-webhook

Event Routing​

The StripeWebhookJob intelligently routes events based on type and product metadata:

Routing Logic​

class StripeWebhookJob < ApplicationJob
SUBSCRIPTION_EVENTS = %w[
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
].freeze

def perform(event_data)
event = Stripe::Event.construct_from(event_data)

if subscription_event?(event)
route_subscription_event(event)
else
Donations::ProcessStripeEventJob.perform_later(event_data)
end
end

private

def route_subscription_event(event)
subscription = event.data.object
product_id = subscription.plan&.product
metadata = fetch_product_metadata(product_id)

if metadata['platform'] == 'premium'
Premium::StripeEventJob.perform_later(event.as_json)
else
Donations::ProcessStripeEventJob.perform_later(event.as_json)
end
end
end

Subscription Events​

subscription.created​

Triggered when a new premium subscription is created.

subscription.deleted​

Triggered when a subscription is cancelled or expires.

Editions with unsubscription detached​

The premium_unsubscribe_disabled feature flag detaches cancellation handling for one or more editions. Premium::StripeEventJob reads the edition from the product metadata (Plan#locale) and, when the flag is enabled for that edition's group, logs the event and returns: premium status is not revoked in Firestore, no goodbye email is sent and no deactivated.premium event is emitted (so premium.unsubscriptions in Datadog does not count these cancellations).

Enable it per edition from /flipper, on the premium_unsubscribe_disabled feature, by adding the edition group (e.g. fr). Only subscription.deleted is affected — subscription.created keeps activating premium for every edition, so an edition with the flag on needs its cancellations handled elsewhere, otherwise premium status is never revoked for its users.

Stripe Service​

The Premium::StripeService handles all subscription-related business logic.

Event Handlers​

class Premium::StripeService
def handle_subscription_created(event)
subscription = event.data.object
customer = fetch_customer(subscription.customer)
plan = product_metadata(subscription.plan.product)

user_result = users_service.find_or_create!(
customer.email,
extract_first_name(customer.name),
extract_last_name(customer.name),
plan.locale
)

users_service.make_premium!(customer.email, plan: plan)

Premium::SendEmailJob.perform_later(
'welcome',
user_result.to_h,
plan.to_h
)
end

def handle_subscription_deleted(event)
subscription = event.data.object
customer = fetch_customer(subscription.customer)
plan = product_metadata(subscription.plan.product)

users_service.disable_premium!(customer.email)

Premium::SendEmailJob.perform_later(
'goodbye',
{ email: customer.email, display_name: customer.name },
plan.to_h
)
end
end

Plan Data Structure​

Plan = Data.define(:id, :name, :tier, :platform, :locale)

# Example:
# Plan(
# id: "prod_ABC123",
# name: "Premium Essential",
# tier: "essential",
# platform: "premium",
# locale: "it"
# )

Product Metadata​

Stripe products must include specific metadata for the premium system to work correctly.

Required Metadata Fields​

FieldTypeRequiredDescription
platformstringYesMust be "premium" for premium routing
tierstringYesPlan tier identifier (e.g., "essential")
namestringNoDisplay name for emails
localestringNoDefault locale (e.g., "it", "en")

Stripe Dashboard Configuration​

  1. Navigate to Products in Stripe Dashboard
  2. Select or create a product
  3. Scroll to Metadata section
  4. Add the required metadata fields:
platform: premium
tier: essential
name: Premium Essential
locale: it

API Example​

Stripe::Product.create(
name: 'Premium Essential',
metadata: {
platform: 'premium',
tier: 'essential',
name: 'Premium Essential',
locale: 'it'
}
)

Webhook Security​

Signature Verification​

All webhooks are verified using Stripe's signing secret:

# config/initializers/stripe_event.rb
StripeEvent.signing_secret = ENV['STRIPE_WEBHOOK_SECRET_V2']

The stripe_event gem automatically verifies signatures using the Stripe-Signature header.

Verification Flow​

Error Handling​

Retry Strategy​

Failed webhook processing is retried by Sidekiq:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours

After 5 attempts, the job is moved to the dead queue.

Common Errors​

ErrorCauseResolution
Stripe::SignatureVerificationErrorInvalid webhook signatureCheck STRIPE_WEBHOOK_SECRET_V2
Stripe::InvalidRequestErrorInvalid API requestCheck API key and request parameters
Stripe::RateLimitErrorToo many API requestsImplement backoff, reduce concurrency

Idempotency​

All handlers are designed to be idempotent:

def handle_subscription_created(event)
# Safe to retry - find_or_create! is idempotent
user_result = users_service.find_or_create!(...)

# Safe to retry - make_premium! updates to same state
users_service.make_premium!(email, plan: plan)

# Emails may be sent multiple times on retry
# Consider tracking sent emails by event ID
end

Testing Webhooks​

Local Development​

Use Stripe CLI to forward webhooks locally:

# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login to Stripe
stripe login

# Forward webhooks to local server
stripe listen --forward-to localhost:3000/stripe-webhook

# Note the webhook signing secret from output
# Set it as STRIPE_WEBHOOK_SECRET_V2

Procfile.dev Integration​

The development Procfile includes Stripe CLI:

web: bin/rails server
worker: bundle exec sidekiq
stripe: stripe listen --forward-to localhost:3000/stripe-webhook

Trigger Test Events​

# Trigger a subscription created event
stripe trigger customer.subscription.created

# Trigger with specific product
stripe trigger customer.subscription.created \
--override subscription:plan:product=prod_ABC123

Monitoring​

Logging​

The service logs all webhook events:

logger.info("Processing subscription event",
event_type: event.type,
event_id: event.id,
customer_id: subscription.customer,
product_id: subscription.plan&.product
)

Key Metrics​

Monitor these metrics in production:

MetricDescription
Webhook success rate% of webhooks processed successfully
Webhook latencyTime to process each webhook
Queue depthNumber of pending webhook jobs
Error rate by typeErrors grouped by exception class

Stripe Dashboard​

Monitor webhook delivery in Stripe Dashboard:

  1. Go to Developers > Webhooks
  2. Select your endpoint
  3. View Event deliveries for status and retries