Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.solvapay.com/llms.txt

Use this file to discover all available pages before exploring further.

Table of Contents

Installation

Install the required packages:
npm install @solvapay/server @solvapay/next @solvapay/react @solvapay/react-supabase
# or
pnpm add @solvapay/server @solvapay/next @solvapay/react @solvapay/react-supabase

Basic Setup

1. Environment Variables

Create a .env.local file:
SOLVAPAY_SECRET_KEY=sk_...
NEXT_PUBLIC_SOLVAPAY_PRODUCT=prd_YOUR_PRODUCT_ID

2. Initialize SolvaPay Client

Create a shared SolvaPay instance:
// lib/solvapay.ts
import { createSolvaPay } from '@solvapay/server'

export const solvaPay = createSolvaPay({
  apiKey: process.env.SOLVAPAY_SECRET_KEY,
})

Protecting API Routes

Basic API Route Protection

Use the payable.next() adapter to protect Next.js App Router API routes:
// app/api/tasks/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { solvaPay } from '@/lib/solvapay'

const payable = solvaPay.payable({
  product: process.env.NEXT_PUBLIC_SOLVAPAY_PRODUCT!,
})

// Your business logic
async function createTask(req: NextRequest) {
  const body = await req.json()
  const { title } = body

  return { success: true, task: { title } }
}

// Protect the route
export const POST = payable.next(createTask)

Using Next.js Helpers

The @solvapay/next package provides route-wrapper helpers that handle auth, body parsing, error formatting, and cache invalidation. Every wrapper returns Promise<NextResponse>return it directly from your route handler.
Breaking change (SDK 1.1): wrappers such as checkPurchase, createPaymentIntent, processPaymentIntent, activatePlan, cancelRenewal, reactivateRenewal, createCheckoutSession, createCustomerSession, syncCustomer, listPlans, getMerchant, getProduct, getPaymentMethod, getCustomerBalance, and trackUsage now always return Promise<NextResponse>. Earlier versions returned NextResponse | data, so if you still have result instanceof NextResponse ? result : NextResponse.json(result) in your handlers, delete that branch.Unchanged: getAuthenticatedUser, getCustomerReference, and syncCustomer helpers that return raw user/customer data stay as-is.
// app/api/check-purchase/route.ts
import { checkPurchase } from '@solvapay/next'

export async function GET(request: Request) {
  return checkPurchase(request)
}

Available Helper Functions

Check Purchase

// app/api/check-purchase/route.ts
import { checkPurchase } from '@solvapay/next'

export async function GET(request: Request) {
  return checkPurchase(request)
}

Create Payment Intent

// app/api/create-payment-intent/route.ts
import { createPaymentIntent } from '@solvapay/next'

export async function POST(request: Request) {
  const { planRef, productRef } = await request.json()
  return createPaymentIntent(request, { planRef, productRef })
}

Process Payment

// app/api/process-payment/route.ts
import { processPaymentIntent } from '@solvapay/next'

export async function POST(request: Request) {
  const { paymentIntentId, productRef, planRef } = await request.json()
  return processPaymentIntent(request, { paymentIntentId, productRef, planRef })
}

Create Checkout Session

// app/api/create-checkout-session/route.ts
import { createCheckoutSession } from '@solvapay/next'

export async function POST(request: Request) {
  const { planRef, productRef } = await request.json()
  return createCheckoutSession(request, { planRef, productRef })
}

Create Customer Portal Session

// app/api/create-customer-session/route.ts
import { createCustomerSession } from '@solvapay/next'

export async function POST(request: Request) {
  return createCustomerSession(request)
}

Sync Customer

// app/api/sync-customer/route.ts
import { syncCustomer } from '@solvapay/next'

export async function POST(request: Request) {
  return syncCustomer(request)
}

Cancel Renewal

// app/api/cancel-renewal/route.ts
import { cancelRenewal } from '@solvapay/next'

export async function POST(request: Request) {
  const { purchaseRef, reason } = await request.json()
  return cancelRenewal(request, { purchaseRef, reason })
}

Reactivate Renewal

// app/api/reactivate-renewal/route.ts
import { reactivateRenewal } from '@solvapay/next'

export async function POST(request: Request) {
  const { purchaseRef } = await request.json()
  return reactivateRenewal(request, { purchaseRef })
}

Activate Plan

// app/api/activate-plan/route.ts
import { activatePlan } from '@solvapay/next'

export async function POST(request: Request) {
  const { productRef, planRef } = await request.json()
  return activatePlan(request, { productRef, planRef })
}
Response body: { status, purchaseRef?, checkoutUrl?, creditBalance?, ... }. Usage-based plans now activate eagerly at the plan step (no balance gate). See Purchase lifecycle management for all status values.

List Plans

// app/api/list-plans/route.ts
import { listPlans } from '@solvapay/next'

export async function GET(request: Request) {
  return listPlans(request)
}
Response body: { plans, productRef }. Pass ?productRef=prd_... as a query parameter.

Merchant, Product, and Payment Method

// app/api/merchant/route.ts
import { getMerchant } from '@solvapay/next'

export async function GET(request: Request) {
  return getMerchant(request)
}
// app/api/product/route.ts
import { getProduct } from '@solvapay/next'

export async function GET(request: Request) {
  return getProduct(request)
}
// app/api/payment-method/route.ts
import { getPaymentMethod } from '@solvapay/next'

export async function GET(request: Request) {
  return getPaymentMethod(request)
}
  • getMerchant returns { name, iconUrl, logoUrl, termsUrl, privacyUrl, ... } — use it to brand checkout and mandate copy.
  • getProduct returns the product with public plans for the current authenticated customer.
  • getPaymentMethod returns { kind: 'card', brand, last4, expMonth, expYear } | { kind: 'none' }, mirrored from the last successful payment_intent.succeeded webhook. Safe to poll alongside check-purchase.

Customer Balance

// app/api/customer-balance/route.ts
import { getCustomerBalance } from '@solvapay/next'

export async function GET(request: Request) {
  return getCustomerBalance(request)
}

Track Usage

// app/api/track-usage/route.ts
import { trackUsage } from '@solvapay/next'

export async function POST(request: Request) {
  const body = await request.json()
  return trackUsage(request, body)
}

Server Components

In Server Components, reach for the *Core primitives from @solvapay/server instead of the route-wrapper helpers — the wrappers always return NextResponse, which isn’t useful when you want to read data.
// app/dashboard/page.tsx
import { checkPurchaseCore, isErrorResult } from '@solvapay/server';
import { cookies } from 'next/headers';
import { solvaPay } from '@/lib/solvapay';

export default async function DashboardPage() {
  const cookieStore = await cookies();
  const headers = new Headers();

  const authToken = cookieStore.get('auth-token');
  if (authToken) {
    headers.set('authorization', `Bearer ${authToken.value}`);
  }

  const request = new Request('http://localhost', { headers });
  const result = await checkPurchaseCore(request, { solvaPay });

  if (isErrorResult(result)) {
    return <div>Error checking purchase</div>;
  }

  return (
    <div>
      <h1>Dashboard</h1>
      {result.hasPaidPurchase ? (
        <p>You have an active purchase!</p>
      ) : (
        <p>Please subscribe to access premium features.</p>
      )}
    </div>
  );
}

Client Components

Use React hooks and components for client-side payment flows:
// app/checkout/page.tsx
'use client';

import { PaymentForm } from '@solvapay/react';
import { useRouter } from 'next/navigation';

export default function CheckoutPage() {
  const router = useRouter();

  return (
    <PaymentForm
      planRef="pln_premium"
      productRef={process.env.NEXT_PUBLIC_SOLVAPAY_PRODUCT!}
      onSuccess={() => {
        router.push('/dashboard');
      }}
      onError={(error) => {
        console.error('Payment error:', error);
      }}
    />
  );
}

Middleware Setup

Authentication Middleware

Set up authentication middleware (implemented in proxy.ts) to extract user information and make it available to API routes:
// proxy.ts
import { NextRequest, NextResponse } from 'next/server'
import { getUserIdFromRequest } from '@solvapay/auth'

export async function proxy(request: NextRequest) {
  // Extract user ID from auth token, session, etc.
  const authToken = request.headers.get('authorization')?.replace('Bearer ', '')

  if (authToken) {
    // Verify token and extract user ID
    // This is a simplified example - use your actual auth logic
    const userId = await extractUserIdFromToken(authToken)

    if (userId) {
      // Add user ID to request headers for SolvaPay
      const requestHeaders = new Headers(request.headers)
      requestHeaders.set('x-user-id', userId)

      return NextResponse.next({
        request: {
          headers: requestHeaders,
        },
      })
    }
  }

  return NextResponse.next()
}

export const config = {
  matcher: '/api/:path*',
}

Supabase Auth Middleware

If using Supabase, use the provided middleware helper (exported from @solvapay/next/middleware and used from proxy.ts):
// proxy.ts
import { createSupabaseAuthMiddleware } from '@solvapay/next/middleware'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
)

export const proxy = createSupabaseAuthMiddleware({
  supabase,
  // Optional: customize which routes to protect
  protectedPaths: ['/api/protected'],
})

Payment Flow Integration

1. Set Up Provider

Wrap your app with SolvaPayProvider:
// app/layout.tsx
import { SolvaPayProvider } from '@solvapay/react';
import { createSupabaseAuthAdapter } from '@solvapay/react-supabase';
import { supabase } from '@/lib/supabase';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  // Optional: Use Supabase auth adapter
  const supabaseAdapter = createSupabaseAuthAdapter({ client: supabase });

  return (
    <html>
      <body>
        <SolvaPayProvider
          config={{
            // Optional: Custom API routes (defaults work out of the box)
            api: {
              checkPurchase: '/api/check-purchase',
              createPayment: '/api/create-payment-intent',
              processPayment: '/api/process-payment',
            },
            // Optional: Supabase auth adapter
            auth: { adapter: supabaseAdapter },
          }}
        >
          {children}
        </SolvaPayProvider>
      </body>
    </html>
  );
}

2. Create API Routes

Set up the required API routes using Next.js helpers — each is a one-liner because every wrapper returns Promise<NextResponse>:
// app/api/check-purchase/route.ts
import { checkPurchase } from '@solvapay/next'

export async function GET(request: Request) {
  return checkPurchase(request)
}
// app/api/create-payment-intent/route.ts
import { createPaymentIntent } from '@solvapay/next'

export async function POST(request: Request) {
  const { planRef, productRef } = await request.json()
  return createPaymentIntent(request, { planRef, productRef })
}
// app/api/process-payment/route.ts
import { processPaymentIntent } from '@solvapay/next'

export async function POST(request: Request) {
  const { paymentIntentId, productRef, planRef } = await request.json()
  return processPaymentIntent(request, { paymentIntentId, productRef, planRef })
}

3. Use Payment Components

Use React components and hooks in your pages:
// app/checkout/page.tsx
'use client';

import { PaymentForm, usePurchase } from '@solvapay/react';
import { useRouter } from 'next/navigation';

export default function CheckoutPage() {
  const router = useRouter();
  const { hasPaidPurchase, loading } = usePurchase();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (hasPaidPurchase) {
    return <div>You already have an active purchase!</div>;
  }

  return (
    <div>
      <h1>Subscribe to Premium</h1>
      <PaymentForm
        planRef="pln_premium"
      productRef={process.env.NEXT_PUBLIC_SOLVAPAY_PRODUCT!}
        onSuccess={() => {
          router.push('/dashboard');
        }}
      />
    </div>
  );
}

Complete Example

Here’s a complete Next.js application with SolvaPay integration:

Project Structure

app/
├── layout.tsx
├── page.tsx
├── dashboard/
│   └── page.tsx
├── checkout/
│   └── page.tsx
└── api/
    ├── tasks/
    │   └── route.ts
    ├── check-purchase/
    │   └── route.ts
    ├── create-payment-intent/
    │   └── route.ts
    ├── process-payment/
    │   └── route.ts
    ├── cancel-renewal/
    │   └── route.ts
    ├── reactivate-renewal/
    │   └── route.ts
    ├── activate-plan/
    │   └── route.ts
    └── list-plans/
        └── route.ts
lib/
└── solvapay.ts
proxy.ts

Root Layout

// app/layout.tsx
import { SolvaPayProvider } from '@solvapay/react';
import { createSupabaseAuthAdapter } from '@solvapay/react-supabase';
import { supabase } from '@/lib/supabase';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const supabaseAdapter = createSupabaseAuthAdapter({ client: supabase });

  return (
    <html>
      <body>
        <SolvaPayProvider config={{ auth: { adapter: supabaseAdapter } }}>
          {children}
        </SolvaPayProvider>
      </body>
    </html>
  );
}

Protected API Route

// app/api/tasks/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { solvaPay } from '@/lib/solvapay'

const payable = solvaPay.payable({
  product: process.env.NEXT_PUBLIC_SOLVAPAY_PRODUCT!,
})

async function createTask(req: NextRequest) {
  const body = await req.json()
  const { title } = body

  return { success: true, task: { title, id: Date.now().toString() } }
}

export const POST = payable.next(createTask)

Checkout Page

// app/checkout/page.tsx
'use client';

import { PaymentForm } from '@solvapay/react';
import { useRouter } from 'next/navigation';

export default function CheckoutPage() {
  const router = useRouter();

  return (
    <div className="container mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Subscribe to Premium</h1>
      <PaymentForm
        planRef="pln_premium"
        productRef={process.env.NEXT_PUBLIC_SOLVAPAY_PRODUCT!}
        onSuccess={() => router.push('/dashboard')}
      />
    </div>
  );
}

Dashboard Page

// app/dashboard/page.tsx
'use client';

import { usePurchase } from '@solvapay/react';
import Link from 'next/link';

export default function DashboardPage() {
  const { hasPaidPurchase, loading, activePurchase } = usePurchase();

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <div className="container mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Dashboard</h1>

      {hasPaidPurchase ? (
        <div>
          <p className="text-green-600">You have an active purchase!</p>
          {activePurchase && (
            <p>Product: {activePurchase.productName}</p>
          )}
        </div>
      ) : (
        <div>
          <p className="text-gray-600">Please subscribe to access premium features.</p>
          <Link href="/checkout" className="text-blue-600 underline">
            Go to Checkout
          </Link>
        </div>
      )}
    </div>
  );
}

Cache Management

The @solvapay/next package includes purchase caching to reduce API calls:
import { clearPurchaseCache, getPurchaseCacheStats } from '@solvapay/next'

// Clear cache for a specific user
await clearPurchaseCache(userId)

// Clear all caches
await clearAllPurchaseCache()

// Get cache statistics
const stats = await getPurchaseCacheStats()
console.log(stats)

Best Practices

  1. Use Environment Variables: Store API keys and configuration in .env.local.
  2. Separate API Routes: Keep API routes separate from page routes for better organization.
  3. Error Handling: Helpers always return NextResponse (including error responses). For Server Component / RSC code paths where you need raw data, use the *Core primitives from @solvapay/server and check with isErrorResult.
  4. Type Safety: Use TypeScript for better type safety.
  5. Cache Management: Use purchase caching to reduce API calls and improve performance.
  6. Middleware: Set up authentication middleware to extract user information early.

Next Steps