Stripe Integration Operations
Integration Shape
- Miru uses the official
stripeRuby gem (Gemfile) with API key setup inconfig/initializers/stripe.rb. - Stripe is used in two flows:
- Invoice collection via Stripe Checkout (
InvoicePayment::Checkout) and webhook fulfillment (Webhooks::StripeController+HandleStripeCheckoutEventService). - Subscription billing via Stripe Checkout/Billing Portal (
Api::V1::SubscriptionsController) and webhook-driven sync (Subscriptions::StripeSyncService). - Stripe Connect onboarding is handled by
StripeConnectedAccountand exposed from Payment Settings APIs.
Required Local Secrets
Add these to your local env file (or shell profile) before starting the app:
STRIPE_SECRET_KEY(required)STRIPE_PUBLISHABLE_KEY(required for frontend/public usage)STRIPE_WEBHOOK_ENDPOINT_SECRET(required to validate webhook signatures)APP_BASE_URL(required for Stripe Connect refresh/return URLs; local value:http://127.0.0.1:3000)STRIPE_SUBSCRIPTION_PRICE_ID_MONTHLY(recommended for subscriptions)STRIPE_SUBSCRIPTION_PRICE_ID_YEARLY(recommended for subscriptions)STRIPE_SUBSCRIPTION_PRICE_ID(fallback if interval-specific IDs are not set)STRIPE_PLAN_PAGE_URL(optional; if present, billing checkout redirects to this hosted Stripe URL instead of creating a session server-side)
Notes:
- Use Stripe test-mode keys locally.
- Never commit keys or webhook secrets.
Create a New Stripe Test Account
- Sign up at Stripe Dashboard in test mode.
- Copy test API keys from Developers > API keys.
- Create test prices for subscriptions and copy price IDs:
- Monthly recurring price ->
STRIPE_SUBSCRIPTION_PRICE_ID_MONTHLY - Yearly recurring price ->
STRIPE_SUBSCRIPTION_PRICE_ID_YEARLY - If you use a single fallback price, set
STRIPE_SUBSCRIPTION_PRICE_ID.
Stripe Signup Wizard Choice (Important)
When Stripe asks how you want to accept recurring payments, choose:
Pre-built checkout form
Why this is the correct choice for Miru:
- Miru already creates Stripe Checkout Sessions server-side for subscriptions:
Api::V1::SubscriptionsController#checkout- Miru also uses Stripe Checkout for invoice payment collection:
InvoicePayment::Checkout- Current architecture is hosted Stripe Checkout + webhook fulfillment, not embedded Elements.
How each wizard option maps to Miru:
Pre-built checkout form:- Matches current Miru implementation and should be used by default.
Shareable payment links:- Optional. Only relevant if you intentionally use a hosted Stripe Payment Link URL via
STRIPE_PLAN_PAGE_URL. Embedded components:- Not used in current Miru implementation; requires separate frontend integration work.
Miru pricing alignment:
- Free: no Stripe checkout needed.
- Pro (monthly/yearly): configure recurring Stripe Prices and set:
STRIPE_SUBSCRIPTION_PRICE_ID_MONTHLYSTRIPE_SUBSCRIPTION_PRICE_ID_YEARLY- Enterprise/Hosted Enterprise: keep contact-sales/manual flow unless product explicitly moves them to self-serve Stripe checkout.
Stripe Dashboard Field Checklist (What To Enter)
For Miru Pro in Stripe Dashboard:
- Product:
- Name:
Miru Pro - Pricing model:
Standard
- Monthly price:
- Type:
Recurring - Billing period:
Monthly - Usage model:
Licensed(per seat) - Amount: your monthly per-seat amount
- Yearly price:
- Type:
Recurring - Billing period:
Yearly - Usage model:
Licensed(per seat) - Amount: your yearly per-seat amount
Do not use:
- One-time price (not compatible with Miru subscription checkout flow)
- Metered usage (Miru currently sends explicit seat quantity)
Where To Copy Env Var Values In Stripe
STRIPE_SECRET_KEY:Stripe Dashboard -> Developers -> API keys -> Secret key (
<stripe_secret_key>)STRIPE_PUBLISHABLE_KEY:Stripe Dashboard -> Developers -> API keys -> Publishable key (
<stripe_publishable_key>)STRIPE_SUBSCRIPTION_PRICE_ID_MONTHLY:Stripe Dashboard -> Product catalog -> Miru Pro -> select monthly recurring price -> copy
<monthly_price_id>valueSTRIPE_SUBSCRIPTION_PRICE_ID_YEARLY:Stripe Dashboard -> Product catalog -> Miru Pro -> select yearly recurring price -> copy
<yearly_price_id>valueSTRIPE_SUBSCRIPTION_PRICE_ID(optional fallback):Set to either monthly or yearly price ID if you want a default fallback
STRIPE_WEBHOOK_ENDPOINT_SECRET:If using Stripe CLI locally: value printed by
stripe listen(<webhook_signing_secret>)If using Dashboard webhook endpoint: Developers -> Webhooks -> select endpoint -> Reveal signing secret (
<webhook_signing_secret>)STRIPE_PLAN_PAGE_URL(optional):Use only if you want billing checkout to redirect to a hosted Payment Link page instead of server-created Checkout Sessions.
Copy the full hosted URL from Payment Links/Checkout link setup if you intentionally adopt that path.
Local Webhook Setup (Stripe CLI)
- Install Stripe CLI and authenticate:
stripe login
- Forward Stripe events to local Miru webhook:
stripe listen --forward-to http://127.0.0.1:3000/webhooks/stripe/checkout/fulfillment
- Copy the signing secret printed by Stripe CLI and set it as
STRIPE_WEBHOOK_ENDPOINT_SECRET. - Restart web/worker processes after changing env vars.
Start Miru Locally
Run app stack using repo-preferred runtime wrapper:
rtk mise exec -- bundle exec foreman start -f Procfile.dev
Primary local URL:
http://127.0.0.1:3000
End-to-End Verification Checklist
A) Stripe Connect (Payment Settings)
- Log in as an owner/admin user.
- Open Payment Settings and click Connect Stripe.
- Complete Stripe onboarding in test mode.
- Confirm provider status returns connected from:
GET /api/v1/payments/settings
Expected outcome:
stripe.connectedistrue.
B) Invoice Payment Checkout + Webhook
- Create a client + invoice with Stripe enabled.
- Open client invoice payment URL (
/invoices/:id/payments/newflow) and choose Stripe. - Complete checkout using Stripe test card
4242 4242 4242 4242. - In Stripe CLI terminal, verify
checkout.session.completedis delivered. - In Miru UI/API, verify invoice moved to paid and payment entry is created.
Expected webhook/event handling:
- Endpoint:
POST /webhooks/stripe/checkout/fulfillment - Service:
HandleStripeCheckoutEventService->InvoicePayment::StripeCheckoutFulfillment
C) Subscription Checkout + Sync
- Open billing settings in app.
- Trigger upgrade checkout (monthly/yearly).
- Complete test checkout in Stripe.
- Verify webhook events are forwarded and processed:
checkout.session.completedcustomer.subscription.created/updatedinvoice.paid(on invoice cycles)
- Verify company billing fields are updated (
stripe_customer_id,stripe_subscription_id) and plan status in billing UI changes.
Expected service path:
Api::V1::SubscriptionsController#checkoutHandleStripeCheckoutEventService->Subscriptions::StripeSyncService
API/Flow Smoke Tests
Use these as focused checks during integration work:
GET /api/v1/subscriptionPOST /api/v1/subscription/checkout?interval=monthlyPOST /api/v1/subscription/portalPOST /api/v1/payments/settings/stripe/connectDELETE /api/v1/payments/settings/stripe/disconnect
Common Failure Modes
Invalid signature!on webhook:STRIPE_WEBHOOK_ENDPOINT_SECRETdoes not match the current Stripe signing secret.- Stripe checkout session creation fails:
- Missing/invalid
STRIPE_SECRET_KEYor missing subscription price env var. - Stripe Connect refresh/return links are wrong:
APP_BASE_URLis missing or not set to the active local host.- Billing checkout returns pricing error:
- Missing
STRIPE_SUBSCRIPTION_PRICE_ID_MONTHLY/YEARLYand no fallbackSTRIPE_SUBSCRIPTION_PRICE_ID.
Focused Test Commands
Run focused specs first when changing Stripe flows:
rtk mise exec -- bundle exec rspec spec/requests/webhooks/stripe_controller_spec.rb
rtk mise exec -- bundle exec rspec spec/services/subscriptions/stripe_sync_service_spec.rb
rtk mise exec -- bundle exec rspec spec/requests/api/v1/subscriptions_controller_spec.rb
If JavaScript or TypeScript files are changed as part of Stripe work, run:
rtk mise exec -- timeout 30 bin/vite build