Stripe Integration
Stripe is the primary payment processor for BookWish, handling both consumer orders and business subscriptions.
Overview
BookWish uses Stripe for:
- Order Checkout: PaymentIntent API for cart checkout and gift orders (mobile + web)
- Bookstore Subscriptions: Checkout Sessions for B2B SaaS subscriptions (web-only)
- Premium Subscriptions: Alternative to RevenueCat for web users
- Tax Calculation: Stripe Tax API for accurate sales tax
- Refunds: Automated and manual refund processing
Implementation
Location: /backend/src/integrations/stripe.ts
Configuration
Required environment variables:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Bookstore subscription products
STRIPE_PRODUCT_BOOKSTORE=price_...
STRIPE_PRODUCT_CUSTOM_DOMAIN=price_...
# Premium subscription products (web-only alternative to RevenueCat)
STRIPE_PRICE_PREMIUM_MONTHLY=price_...
STRIPE_PRICE_PREMIUM_YEARLY=price_...
STRIPE_PRICE_PREMIUM_LIFETIME=price_...
The integration initializes the Stripe SDK with API version 2025-11-17.clover and TypeScript support enabled.
API Version
Current API version: 2025-11-17.clover
Features
1. Payment Intents (Order Checkout)
Used for cart checkout and gift orders. Accessible by both mobile and web apps.
Create Payment Intent
import { createPaymentIntent } from '../integrations/stripe';
const paymentIntent = await createPaymentIntent(
2999, // Amount in cents ($29.99)
'usd',
{
orderId: 'order_123',
userId: 'user_456',
type: 'cart_checkout'
}
);
// Returns:
// {
// id: 'pi_...',
// clientSecret: 'pi_..._secret_...',
// amount: 2999,
// currency: 'usd',
// status: 'requires_payment_method',
// metadata: { ... }
// }
Retrieve Payment Intent
const paymentIntent = await retrievePaymentIntent('pi_...');
Confirm Payment (Server-Side)
const confirmed = await confirmPayment('pi_...');
Refund Payment
// Full refund
await refund('pi_...');
// Partial refund
await refund('pi_...', 1000); // Refund $10.00
// With reason
await refund('pi_...', undefined, 'requested_by_customer');
2. Tax Calculation
BookWish uses Stripe Tax for accurate sales tax calculation based on customer address.
Calculate Tax
import { calculateTax } from '../integrations/stripe';
const taxResult = await calculateTax({
currency: 'usd',
lineItems: [
{
amount: 2999, // $29.99 in cents
reference: 'book_123',
tax_code: 'txcd_99999999' // Physical books
}
],
customerAddress: {
line1: '123 Main St',
city: 'San Francisco',
state: 'CA',
postal_code: '94102',
country: 'US'
},
shippingCost: 500 // $5.00 shipping
});
// Returns:
// {
// taxAmountCents: 267, // $2.67 tax
// taxBreakdown: [
// {
// amount: 267,
// jurisdiction: 'California',
// taxability_reason: 'product_exempt',
// tax_rate_details: {
// percentage_decimal: '8.5',
// display_name: 'Sales Tax'
// }
// }
// ]
// }
Tax Codes
txcd_99999999: Physical bookstxcd_92010001: Shipping
3. Bookstore Subscriptions (Web-Only)
B2B SaaS subscriptions for independent bookstores.
Create Checkout Session
import { createBookstoreCheckoutSession } from '../integrations/stripe';
const session = await createBookstoreCheckoutSession({
ownerUserId: 'user_123',
ownerEmail: 'owner@bookstore.com',
storeName: 'Main Street Books',
storeSlug: 'main-street-books',
successUrl: 'https://bookwish.io/store/setup/success',
cancelUrl: 'https://bookwish.io/store/setup'
});
// Redirect user to session.checkoutUrl
Custom Domain Add-On
const session = await createCustomDomainCheckoutSession({
storeId: 'store_123',
customerId: 'cus_...', // Existing Stripe customer from bookstore subscription
successUrl: 'https://bookwish.io/store/settings/domain/success',
cancelUrl: 'https://bookwish.io/store/settings/domain'
});
Customer Portal
Allow customers to manage their subscription (update payment method, cancel, etc.):
import { createCustomerPortalSession } from '../integrations/stripe';
const portal = await createCustomerPortalSession(
'cus_...', // Customer ID
'https://bookwish.io/store/settings' // Return URL
);
// Redirect user to portal.portalUrl
4. Premium Subscriptions (Web-Only)
Web alternative to RevenueCat for consumer Premium subscriptions.
Create Premium Checkout Session
import { createPremiumCheckoutSession } from '../integrations/stripe';
const session = await createPremiumCheckoutSession({
userId: 'user_123',
email: 'user@example.com',
plan: 'monthly', // 'monthly' | 'yearly' | 'lifetime'
successUrl: 'https://bookwish.io/upgrade/success',
cancelUrl: 'https://bookwish.io/upgrade'
});
Check Plan Availability
import { getPremiumPlansAvailability } from '../integrations/stripe';
const plans = getPremiumPlansAvailability();
// Returns:
// [
// { plan: 'monthly', priceId: 'price_...', available: true },
// { plan: 'yearly', priceId: 'price_...', available: true },
// { plan: 'lifetime', priceId: 'price_...', available: false }
// ]
5. Webhooks
Stripe sends webhook events for payment updates. The integration provides signature verification.
Verify Webhook
import { constructWebhookEvent } from '../integrations/stripe';
// In your webhook handler:
const signature = request.headers['stripe-signature'];
const payload = request.rawBody; // Raw buffer, not parsed JSON
try {
const event = constructWebhookEvent(payload, signature);
// Handle event
switch (event.type) {
case 'payment_intent.succeeded':
// Handle successful payment
break;
case 'checkout.session.completed':
// Handle subscription activation
break;
// ... more event types
}
} catch (error) {
// Invalid signature
return res.status(400).send('Invalid signature');
}
Important Webhook Events
payment_intent.succeeded: Payment completed successfullypayment_intent.payment_failed: Payment failedcheckout.session.completed: Subscription checkout completedcustomer.subscription.updated: Subscription status changedcustomer.subscription.deleted: Subscription cancelledinvoice.payment_succeeded: Recurring payment successfulinvoice.payment_failed: Recurring payment failed
Error Handling
The integration throws descriptive errors for common failure scenarios:
try {
const paymentIntent = await createPaymentIntent(/* ... */);
} catch (error) {
if (error.message === 'Stripe is not configured. Please set STRIPE_SECRET_KEY.') {
// Stripe credentials missing
}
// Handle other Stripe errors
}
Common Error Codes
card_declined: Card was declinedinsufficient_funds: Card has insufficient fundsexpired_card: Card has expiredincorrect_cvc: CVC code is incorrectprocessing_error: Processing error occurredrate_limit: Too many requests
Testing
Test Cards
Stripe provides test cards for development:
| Card Number | Scenario |
|---|---|
| 4242 4242 4242 4242 | Success |
| 4000 0000 0000 9995 | Declined |
| 4000 0000 0000 0002 | Card declined |
| 4000 0025 0000 3155 | 3D Secure auth |
Use any future expiration date and any 3-digit CVC.
Test Mode vs Production
- Test keys start with
sk_test_ - Production keys start with
sk_live_ - Webhook secrets differ between test and live mode
Best Practices
- Always use HTTPS for webhook endpoints
- Verify webhook signatures before processing events
- Handle idempotency - Stripe may send duplicate webhooks
- Store minimal card data - Let Stripe handle PCI compliance
- Use metadata to link Stripe objects to your database records
- Enable automatic payment methods for best conversion rates
- Test webhook integration using Stripe CLI:
stripe listen --forward-to localhost:3000/webhooks/stripe
Limitations
- Mobile Subscriptions: Mobile apps must use RevenueCat, not Stripe directly (Apple/Google policy)
- Bookstore Subscriptions: Web-only, mobile apps should not access these endpoints
- Tax Calculation: Requires valid address, may return $0 if calculation fails
- Payment Intents: Expire after 24 hours if not confirmed
Rate Limits
Stripe API rate limits:
- Test mode: 100 requests per second
- Production mode: 100 requests per second (can be increased)