Skip to main content

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 books
  • txcd_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 successfully
  • payment_intent.payment_failed: Payment failed
  • checkout.session.completed: Subscription checkout completed
  • customer.subscription.updated: Subscription status changed
  • customer.subscription.deleted: Subscription cancelled
  • invoice.payment_succeeded: Recurring payment successful
  • invoice.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 declined
  • insufficient_funds: Card has insufficient funds
  • expired_card: Card has expired
  • incorrect_cvc: CVC code is incorrect
  • processing_error: Processing error occurred
  • rate_limit: Too many requests

Testing

Test Cards

Stripe provides test cards for development:

Card NumberScenario
4242 4242 4242 4242Success
4000 0000 0000 9995Declined
4000 0000 0000 0002Card declined
4000 0025 0000 31553D 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

  1. Always use HTTPS for webhook endpoints
  2. Verify webhook signatures before processing events
  3. Handle idempotency - Stripe may send duplicate webhooks
  4. Store minimal card data - Let Stripe handle PCI compliance
  5. Use metadata to link Stripe objects to your database records
  6. Enable automatic payment methods for best conversion rates
  7. 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)

Additional Resources