# Stripe Integration Manual

Complete guide for integrating Stripe payments and webhooks into web applications.

## Table of Contents

1. [Prerequisites](#prerequisites)
2. [Stripe Account Setup](#stripe-account-setup)
3. [Environment Configuration](#environment-configuration)
4. [Backend Integration](#backend-integration)
5. [Frontend Integration](#frontend-integration)
6. [Webhook Configuration](#webhook-configuration)
7. [Testing](#testing)
8. [Production Deployment](#production-deployment)
9. [Troubleshooting](#troubleshooting)

---

## Prerequisites

- Node.js backend (Express.js recommended)
- PostgreSQL database (or any database)
- Frontend framework (React, Vue, etc.)
- Stripe account

---

## Stripe Account Setup

### 1. Create Stripe Account

1. Go to https://dashboard.stripe.com/register
2. Sign up for a new account
3. Complete email verification
4. Complete business profile (for production use)

### 2. Get API Keys

1. Go to **Developers** > **API keys**
2. Copy the following keys:
   - **Publishable key** (`pk_test_...` for test, `pk_live_...` for production)
   - **Secret key** (`sk_test_...` for test, `sk_live_...` for production)
   - **Webhook signing secret** (created later in webhook section)

### 3. Create Products and Prices (Optional)

You can create products/prices via API or Dashboard:

**Via Dashboard:**
1. Go to **Products** > **Add product**
2. Create product with pricing details
3. Copy the **Price ID** (`price_...`)

**Via API:**
```javascript
const price = await stripe.prices.create({
  currency: 'sar',
  unit_amount: 10000, // 100.00 in cents/pennies
  recurring: { interval: 'month' },
  product_data: { name: 'Pro Plan' }
});
```

---

## Environment Configuration

### Server Environment Variables

Add these to your `.env` file or server configuration:

```bash
# Stripe Keys
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here

# Frontend URL (for redirects)
FRONTEND_URL=https://yourdomain.com
```

### Frontend Environment Variables

```bash
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
```

---

## Backend Integration

### Install Dependencies

```bash
npm install stripe
npm install express
npm install dotenv
```

### Initialize Stripe

```javascript
// src/stripe.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia', // Use latest API version
  typescript: true,
});

export default stripe;
```

### Create Checkout Session API

```typescript
// src/routes/payments.ts
import { Router } from 'express';
import Stripe from 'stripe';
import { requireAuth, type AuthRequest } from '../lib/auth';

const router = Router();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// Create checkout session
router.post('/create-checkout-session', requireAuth, async (req: AuthRequest, res, next) => {
  try {
    const { priceId, userId, metadata = {} } = req.body;

    const session = await stripe.checkout.sessions.create({
      mode: 'subscription', // or 'payment' for one-time payments
      payment_method_types: ['card'],
      customer_email: req.user?.email,
      metadata: {
        userId: String(userId),
        ...metadata
      },
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      success_url: `${process.env.FRONTEND_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.FRONTEND_URL}/cancel`,
    });

    res.json({ url: session.url });
  } catch (err) {
    next(err);
  }
});

// Create checkout session with dynamic pricing
router.post('/create-checkout-session', requireAuth, async (req: AuthRequest, res, next) => {
  try {
    const { planName, amount, currency = 'usd', interval = 'month' } = req.body;

    const session = await stripe.checkout.sessions.create({
      mode: 'subscription',
      payment_method_types: ['card'],
      customer_email: req.user?.email,
      metadata: {
        userId: String(req.user?.id),
        planName: planName,
      },
      line_items: [
        {
          price_data: {
            currency: currency,
            unit_amount: Math.round(amount * 100), // Convert to cents
            recurring: { interval: interval as 'month' | 'year' },
            product_data: {
              name: planName,
            },
          },
          quantity: 1,
        },
      ],
      success_url: `${process.env.FRONTEND_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.FRONTEND_URL}/plans`,
    });

    res.json({ url: session.url });
  } catch (err) {
    next(err);
  }
});

export default router;
```

### Cancel Subscription API

```typescript
router.post('/cancel-subscription', requireAuth, async (req: AuthRequest, res, next) => {
  try {
    const { subscriptionId } = req.body;

    // Cancel at period end (keeps access until paid period ends)
    const subscription = await stripe.subscriptions.update(subscriptionId, {
      cancel_at_period_end: true,
    });

    res.json(subscription);
  } catch (err) {
    next(err);
  }
});

// Immediate cancellation
router.post('/cancel-subscription-immediate', requireAuth, async (req: AuthRequest, res, next) => {
  try {
    const { subscriptionId } = req.body;

    const subscription = await stripe.subscriptions.cancel(subscriptionId);

    res.json(subscription);
  } catch (err) {
    next(err);
  }
});
```

---

## Frontend Integration

### Create Payment Hook

```typescript
// src/hooks/useStripePayment.ts
import { useMutation } from '@tanstack/react-query';

interface CreateCheckoutParams {
  planName: string;
  amount: number;
  currency?: string;
  interval?: 'month' | 'year';
}

export function useCreateCheckout() {
  return useMutation({
    mutationFn: async (params: CreateCheckoutParams) => {
      const response = await fetch('/api/payments/create-checkout-session', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify(params),
      });

      if (!response.ok) throw new Error('Failed to create checkout session');

      return response.json();
    },
    onSuccess: (data) => {
      // Redirect to Stripe Checkout
      if (data.url) {
        window.location.href = data.url;
      }
    },
  });
}
```

### Usage in React Component

```typescript
// src/components/PlanCard.tsx
import { useCreateCheckout } from '@/hooks/useStripePayment';

export function PlanCard({ plan }: { plan: Plan }) {
  const checkoutMutation = useCreateCheckout();

  const handleSubscribe = () => {
    checkoutMutation.mutate({
      planName: plan.name,
      amount: plan.price,
      currency: 'sar',
      interval: 'month',
    });
  };

  return (
    <div className="border rounded-xl p-6">
      <h3 className="text-xl font-bold">{plan.name}</h3>
      <p className="text-2xl font-bold">{plan.price} SAR/month</p>
      <button
        onClick={handleSubscribe}
        disabled={checkoutMutation.isPending}
        className="bg-blue-600 text-white px-6 py-2 rounded-lg"
      >
        {checkoutMutation.isPending ? 'Processing...' : 'Subscribe'}
      </button>
    </div>
  );
}
```

### Success/Cancellation Pages

```typescript
// src/pages/PaymentSuccess.tsx
import { useEffect, useState } from 'react';
import { useSearchParams } from 'wouter';

export function PaymentSuccessPage() {
  const [params] = useSearchParams();
  const sessionId = params.get('session_id');
  const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');

  useEffect(() => {
    if (sessionId) {
      // Verify session with your backend
      fetch(`/api/payments/verify-session?session_id=${sessionId}`, {
        credentials: 'include',
      })
        .then(res => res.ok ? setStatus('success') : setStatus('error'))
        .catch(() => setStatus('error'));
    }
  }, [sessionId]);

  if (status === 'loading') {
    return <div>Processing your payment...</div>;
  }

  if (status === 'error') {
    return <div>There was an error processing your payment.</div>;
  }

  return (
    <div className="text-center py-20">
      <h1 className="text-3xl font-bold text-green-600 mb-4">Payment Successful!</h1>
      <p>Thank you for your subscription.</p>
      <a href="/dashboard" className="text-blue-600 underline">Go to Dashboard</a>
    </div>
  );
}
```

---

## Webhook Configuration

### Create Webhook Endpoint

```typescript
// src/routes/webhooks.ts
import { Router, type Request } from 'express';
import Stripe from 'stripe';
import { db } from '@workspace/db';
import { eq } from 'drizzle-orm';

const router = Router();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

// Webhook signature verification
async function verifySignature(payload: string, signature: string) {
  try {
    const event = stripe.webhooks.constructEvent(
      payload,
      signature,
      webhookSecret
    );
    return event;
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    return null;
  }
}

// Webhook handler
router.post('/webhook', async (req: Request, res, next) => {
  const payload = req.body;
  const signature = req.headers['stripe-signature'] as string;

  const event = await verifySignature(payload, signature);
  if (!event) {
    return res.status(400).json({ error: 'Invalid signature' });
  }

  console.log('Webhook received:', event.type);

  try {
    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object as Stripe.Checkout.Session;
        await handleCheckoutCompleted(session);
        break;
      }

      case 'customer.subscription.created': {
        const subscription = event.data.object as Stripe.Subscription;
        await handleSubscriptionCreated(subscription);
        break;
      }

      case 'customer.subscription.updated': {
        const subscription = event.data.object as Stripe.Subscription;
        await handleSubscriptionUpdated(subscription);
        break;
      }

      case 'customer.subscription.deleted': {
        const subscription = event.data.object as Stripe.Subscription;
        await handleSubscriptionDeleted(subscription);
        break;
      }

      case 'invoice.payment_succeeded': {
        const invoice = event.data.object as Stripe.Invoice;
        await handleInvoicePaymentSucceeded(invoice);
        break;
      }

      case 'invoice.payment_failed': {
        const invoice = event.data.object as Stripe.Invoice;
        await handleInvoicePaymentFailed(invoice);
        break;
      }

      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    res.json({ received: true });
  } catch (err) {
    console.error('Webhook handler error:', err);
    // Still return 200 to avoid retry loops
    res.json({ received: true, error: 'Processing error' });
  }
});

// Event handlers
async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
  const { userId, planName } = session.metadata || {};

  // Create subscription record in database
  await db.insert(subscriptionsTable).values({
    userId: Number(userId),
    stripeCustomerId: session.customer as string,
    stripeSubscriptionId: session.subscription as string,
    planName: planName || 'Unknown',
    status: 'active',
    currentPeriodStart: new Date(),
    currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
  });

  console.log('Subscription created for user:', userId);
}

async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
  // Update subscription status in database
  await db.update(subscriptionsTable)
    .set({
      status: subscription.status as 'active' | 'past_due' | 'canceled' | 'incomplete',
      currentPeriodStart: new Date(subscription.current_period_start * 1000),
      currentPeriodEnd: new Date(subscription.current_period_end * 1000),
    })
    .where(eq(subscriptionsTable.stripeSubscriptionId, subscription.id));
}

async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
  const status =
    subscription.status === 'active' ? 'active' :
    subscription.status === 'past_due' ? 'past_due' :
    subscription.status === 'canceled' ? 'canceled' :
    subscription.status === 'trialing' ? 'trialing' : 'past_due';

  await db.update(subscriptionsTable)
    .set({
      status,
      cancelAtPeriodEnd: subscription.cancel_at_period_end,
      currentPeriodEnd: subscription.current_period_end
        ? new Date(subscription.current_period_end * 1000)
        : undefined,
    })
    .where(eq(subscriptionsTable.stripeSubscriptionId, subscription.id));

  console.log('Subscription updated:', subscription.id);
}

async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
  await db.update(subscriptionsTable)
    .set({ status: 'canceled' })
    .where(eq(subscriptionsTable.stripeSubscriptionId, subscription.id));

  console.log('Subscription canceled:', subscription.id);
}

async function handleInvoicePaymentSucceeded(invoice: Stripe.Invoice) {
  // Record successful payment
  const subscriptionId = invoice.subscription as string;

  // Create invoice record
  await db.insert(invoicesTable).values({
    stripeInvoiceId: invoice.id,
    subscriptionId: subscriptionId,
    amount: (invoice.total as number) / 100,
    currency: invoice.currency,
    status: 'paid',
    paidAt: new Date(),
  });

  // Ensure subscription is active
  await db.update(subscriptionsTable)
    .set({ status: 'active' })
    .where(eq(subscriptionsTable.stripeSubscriptionId, subscriptionId));

  console.log('Invoice payment succeeded:', invoice.id);
}

async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
  // Mark subscription as past due
  if (invoice.subscription) {
    await db.update(subscriptionsTable)
      .set({ status: 'past_due' })
      .where(eq(subscriptionsTable.stripeSubscriptionId, invoice.subscription as string));
  }

  // Send payment failed notification email
  await sendPaymentFailedEmail(invoice.customer_email);

  console.log('Invoice payment failed:', invoice.id);
}

export default router;
```

### Setup Webhook in Stripe Dashboard

1. Go to **Developers** > **Webhooks**
2. Click **Add endpoint**
3. Enter your webhook URL: `https://yourdomain.com/api/webhooks`
4. Select events to listen for:
   - `checkout.session.completed`
   - `customer.subscription.created`
   - `customer.subscription.updated`
   - `customer.subscription.deleted`
   - `invoice.payment_succeeded`
   - `invoice.payment_failed`
5. Click **Add endpoint**
6. Copy the **Webhook signing secret** (`whsec_...`)
7. Add it to your environment variables: `STRIPE_WEBHOOK_SECRET=whsec_...`

---

## Testing

### Test Mode

Stripe provides test mode for development:

1. Use test API keys (`pk_test_...`, `sk_test_...`)
2. Use test card numbers from https://docs.stripe.com/testing

### Common Test Card Numbers

| Card Number | Description |
|-------------|-------------|
| `4242 4242 4242 4242` | Visa (success) |
| `4000 0000 0000 0002` | Card declined |
| `4000 0000 0000 9995` | Insufficient funds |
| `4000 0025 0000 3155` | Require 3D Secure |

### Testing with CLI

```bash
# Trigger a test webhook
stripe trigger checkout.session.completed

# Trigger payment failure
stripe trigger invoice.payment_failed

# Listen for webhooks locally (use Stripe CLI)
stripe listen --forward-to localhost:9080/api/webhooks
```

### Test Checkout Flow

1. Create a test checkout session
2. Use test card `4242 4242 4242 4242`
3. Use any future expiry date (e.g., `12/34`)
4. Use any CVC (e.g., `123`)
5. Complete payment
6. Verify webhook events are received
7. Check database for subscription creation

---

## Production Deployment

### Pre-Deployment Checklist

- [ ] Switch from test to live API keys
- [ ] Update webhook endpoint to production URL
- [ ] Verify all prices are created in live mode
- [ ] Test with real card (small amount first)
- [ ] Ensure webhook endpoint is accessible publicly
- [ ] Set up proper error logging
- [ ] Configure database backups
- [ ] Set up email notifications for failed payments

### Environment Configuration for Production

```bash
# Production .env
STRIPE_SECRET_KEY=sk_live_your_live_secret_key
STRIPE_PUBLISHABLE_KEY=pk_live_your_live_publishable_key
STRIPE_WEBHOOK_SECRET=whsec_your_live_webhook_secret
FRONTEND_URL=https://yourdomain.com
```

### Security Best Practices

1. **Never expose secret keys** in frontend code
2. **Use HTTPS** for all production endpoints
3. **Verify webhook signatures** for all webhook requests
4. **Implement idempotency** for payment operations
5. **Log sensitive data** minimally and securely
6. **Set up monitoring** for payment failures

---

## Troubleshooting

### Common Issues

#### Webhook Not Receiving Events

**Symptoms:** Webhook endpoint not getting called

**Solutions:**
1. Verify webhook URL is accessible publicly (use ngrok for local testing)
2. Check Stripe Dashboard for webhook delivery logs
3. Verify webhook secret matches exactly
4. Check server logs for errors

#### Payment Not Redirecting

**Symptoms:** User not redirected after payment

**Solutions:**
1. Verify `success_url` and `cancel_url` are absolute URLs
2. Check CORS configuration
3. Ensure frontend URL is correct in environment variables

#### Subscription Not Created

**Symptoms:** Webhook received but subscription not in database

**Solutions:**
1. Check webhook handler logs
2. Verify database connection
3. Ensure user ID is passed in metadata
4. Check for database constraint violations

#### Webhook Signature Verification Failing

**Symptoms:** All webhooks return 400 error

**Solutions:**
1. Ensure webhook secret is correct (no extra whitespace)
2. Verify raw request body is being passed
3. Check for encoding issues
4. Regenerate webhook secret if needed

### Debug Mode

Enable Stripe debug logging:

```typescript
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia',
  typescript: true,
  // Enable debug logging
  telemetry: false,
});

// Log all requests
stripe.on('request', (request) => {
  console.log('Stripe Request:', request);
});

stripe.on('response', (response) => {
  console.log('Stripe Response:', response);
});
```

---

## Quick Start Template

### Backend Setup (5 minutes)

```typescript
// 1. Install Stripe
npm install stripe

// 2. Create checkout endpoint
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

app.post('/api/create-checkout', async (req, res) => {
  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [{ price: req.body.priceId, quantity: 1 }],
    success_url: 'https://yourdomain.com/success',
    cancel_url: 'https://yourdomain.com/cancel',
  });
  res.json({ url: session.url });
});

// 3. Create webhook endpoint
app.post('/api/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature']!;
  const event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);

  if (event.type === 'checkout.session.completed') {
    // Handle successful payment
    console.log('Payment successful!');
  }

  res.json({ received: true });
});
```

### Frontend Setup (5 minutes)

```typescript
// 1. Create checkout function
async function checkout(priceId: string) {
  const res = await fetch('/api/create-checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ priceId }),
  });
  const { url } = await res.json();
  window.location.href = url;
}

// 2. Add checkout button
<button onClick={() => checkout('price_abc123')}>
  Subscribe Now
</button>
```

---

## Additional Resources

- [Stripe Official Documentation](https://docs.stripe.com)
- [Stripe API Reference](https://docs.stripe.com/api)
- [Stripe Testing Guide](https://docs.stripe.com/testing)
- [Stripe Webhooks Guide](https://docs.stripe.com/webhooks)
- [Stripe CLI](https://docs.stripe.com/stripe-cli)

---

## Support

For issues specific to this integration manual, check the Troubleshooting section or refer to Stripe's official documentation.
