Optare v1.0 is now available. Get started →
Guides
PayPal Integration

PayPal Integration Setup Guide

🚀 Quick Start

Your PayPal integration is now fully working! You can use it in two ways:

Option 1: Legacy Mode (Environment Variables) - Fastest Setup

This is the quickest way to get started. Perfect for single-tenant or testing.

  1. Set Environment Variables:

    # .env.local
    PAYPAL_CLIENT_ID=your_paypal_client_id
    PAYPAL_CLIENT_SECRET=your_paypal_client_secret
    PAYPAL_MODE=sandbox  # or "live" for production
     
    # Plan IDs (get these from PayPal or create using the script)
    PAYPAL_PLAN_PRODUCT_SLUG_MONTHLY=P-XXXXXXXXXXXXXXXXXXXXXX
    PAYPAL_PLAN_PRODUCT_SLUG_ANNUAL=P-XXXXXXXXXXXXXXXXXXXXXX
  2. Create PayPal Plans (if you haven't already):

    pnpm tsx scripts/create-paypal-plans.ts
  3. Test Subscription Flow:

    • Go to /portal/subscriptions
    • Click "Subscribe" on a product
    • Complete payment on PayPal sandbox
    • You'll be redirected back with an active subscription

Option 2: Modular Mode (Per-Organization) - Multi-Tenant

This allows each organization to use their own PayPal account.

  1. Set Encryption Key:

    # .env.local
    BILLING_ENCRYPTION_KEY=your-256-bit-encryption-key-here
  2. Configure Organization Billing Settings:

    import { encryptCredentials } from './libs/billing/src/encryption';
    import { db } from './libs/database/src/client';
    import { organizations } from './libs/database/src/schema';
    import { eq } from 'drizzle-orm';
     
    // Encrypt credentials
    const encryptedCreds = encryptCredentials({
      clientId: 'org_paypal_client_id',
      clientSecret: 'org_paypal_client_secret',
      webhookId: 'org_webhook_id' // optional, for webhook verification
    });
     
    // Store in organization metadata
    await db.update(organizations)
      .set({
        metadata: {
          billing: {
            providerType: 'paypal',
            encryptedCredentials: encryptedCreds,
            isSandbox: true
          }
        }
      })
      .where(eq(organizations.id, organizationId));
  3. Test Subscription Flow:

    • Same as Option 1, but uses organization's PayPal account
    • Each organization can have different payment providers

📋 Database Migration

Run the migration to add provider fields:

# Apply migration
psql $DATABASE_URL -f libs/database/migrations/0002_add_provider_fields.sql
 
# Or if using Drizzle Kit
pnpm drizzle-kit push:pg

🔧 How It Works

Subscription Creation Flow

User clicks "Subscribe"

Check if org has billing settings?

YES → Use PaymentFactory (modular)

    - Decrypt org credentials
    - Initialize provider (PayPal/Stripe/etc)
    - Create subscription
    - Redirect to payment URL
    
NO → Use legacy PayPal (env vars)

    - Use PAYPAL_CLIENT_ID from env
    - Create PayPal subscription
    - Redirect to PayPal

Success Callback Flow

PayPal redirects to /portal/subscriptions/success?subscription_id=xxx

Find subscription in database

Check if org has billing settings?

YES → Use PaymentFactory to verify
NO → Use legacy PayPal to verify

Update subscription status to "active"

Auto-assign licenses to org members

Redirect to /portal/subscriptions?success=true

🎯 Features

Backward Compatible: Existing PayPal integrations work without changes ✅ Multi-Tenant: Each organization can use their own payment provider ✅ Provider Agnostic: Easy to add Stripe, Razorpay, etc. ✅ Secure: Credentials encrypted at rest with AES-256-GCM ✅ Flexible: Supports both sandbox and production modes

🔐 Security Best Practices

  1. Never commit credentials to git:

    # Add to .gitignore
    .env.local
    .env.production
  2. Use strong encryption key:

    # Generate a secure key
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
  3. Rotate credentials regularly:

    • Update PayPal credentials every 90 days
    • Update encryption key annually
  4. Use sandbox for testing:

    • Always test with PAYPAL_MODE=sandbox first
    • Only switch to live after thorough testing

🐛 Troubleshooting

"Payment plan not configured" Error

Cause: Missing plan ID in environment variables

Solution:

# Check your product slug
echo "Product slug: my-product"
 
# Expected env var format
PAYPAL_PLAN_MY_PRODUCT_MONTHLY=P-xxx
PAYPAL_PLAN_MY_PRODUCT_ANNUAL=P-xxx

"No payment provider configured" Error

Cause: Organization has no billing settings and no env vars

Solution: Either:

  1. Set PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET in environment, OR
  2. Configure organization billing settings (see Option 2 above)

Subscription Not Activating

Cause: Webhook not received or subscription verification failed

Solution:

  1. Check PayPal webhook configuration
  2. Verify webhook URL is accessible (HTTPS required)
  3. Check application logs for errors

📚 Additional Resources

🚀 Next Steps

  1. Run the database migration
  2. Set up PayPal credentials (Option 1 or 2)
  3. Create subscription plans using the script
  4. Test the subscription flow in sandbox mode
  5. Implement billing settings UI for self-service configuration

✅ What's Working Now

  • ✅ PayPal subscription creation
  • ✅ Payment URL redirects
  • ✅ Success callback handling
  • ✅ Subscription activation
  • ✅ License auto-assignment
  • ✅ Backward compatibility with existing integrations
  • ✅ Multi-tenant support (when configured)

🔜 Coming Soon

  • Billing settings UI
  • Webhook verification
  • Stripe provider implementation
  • Razorpay provider implementation