Skip to main content
The Checkout API creates payment sessions for users to purchase subscriptions or make one-time payments.

Create Checkout Session

POST /api/payments/checkout Creates a checkout session for a specific plan.

Implementation

From src/app/api/payments/checkout/route.ts:15-51:
export async function POST(req: Request) {
  // 1. Verify authentication
  const session = await auth.api.getSession({ headers: await headers() })
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // 2. Validate request body
  const { plan, successUrl, cancelUrl } = checkoutSchema.parse(body)

  // 3. Get active payment adapter (Stripe, Polar, or Lemon Squeezy)
  const adapter = getPaymentAdapter()

  // 4. Create checkout session
  const checkoutSession = await adapter.createCheckout({
    plan,
    userId: session.user.id,
    email: session.user.email,
    successUrl,
    cancelUrl
  })

  return NextResponse.json(checkoutSession)
}

Authentication

This endpoint requires authentication. User must have an active session.

Request

plan
string
required
Plan name to purchase. Must be one of:
  • starter
  • pro
  • enterprise
The free plan is not purchasable via checkout.
successUrl
string
URL to redirect to after successful payment.Default: /dashboard?checkout=success (from src/config/payments.ts:240)
cancelUrl
string
URL to redirect to if user cancels checkout.Default: /dashboard?checkout=canceled (from src/config/payments.ts:241)

Response

url
string
Checkout session URL to redirect the user to
sessionId
string
Payment provider’s session ID for tracking

Example

'use client'

export function UpgradeButton() {
  const handleUpgrade = async () => {
    const response = await fetch('/api/payments/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        plan: 'pro',
        successUrl: `${window.location.origin}/dashboard?upgrade=success`,
        cancelUrl: `${window.location.origin}/pricing`
      })
    })

    const { url } = await response.json()
    
    // Redirect to checkout
    window.location.href = url
  }

  return (
    <button onClick={handleUpgrade}>
      Upgrade to Pro
    </button>
  )
}

Response Example

{
  "url": "https://checkout.stripe.com/c/pay/cs_test_abc123...",
  "sessionId": "cs_test_abc123def456"
}

Plan Configuration

Plans are defined in src/config/payments.ts:52-235. Each plan includes:
Pricing:
  • Monthly: $9.90/month
  • Yearly: $99/year (save 17%)
Features:
  • Up to 10 projects
  • Advanced analytics
  • Email support
  • Premium templates
  • Custom integrations
Trial: 14 days
Pricing:
  • Monthly: $99.90/month
  • Yearly: $999/year (save 17%)
Features:
  • Everything in Pro
  • Dedicated account manager
  • Custom contracts
  • SLA guarantees
  • Advanced security
  • Unlimited seats
  • Custom integrations
  • On-premise deployment
Seat-based billing: Yes (unlimited)Trial: 30 days

Multi-Provider Support

ShipFree supports multiple payment providers. The active provider is determined by environment configuration:
// Set in .env
PAYMENT_PROVIDER=stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

// Price IDs in .env
NEXT_PUBLIC_STRIPE_PRICE_STARTER_MONTHLY=price_...
NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY=price_...
The adapter is selected automatically based on PAYMENT_PROVIDER environment variable.

Validation

Request validation schema from src/app/api/payments/checkout/route.ts:9-13:
const checkoutSchema = z.object({
  plan: z.enum(getAvailablePlans() as [string, ...string[]]),
  successUrl: z.string().optional(),
  cancelUrl: z.string().optional()
})

Error Handling

401
Unauthorized
User is not authenticated. Session token is missing or invalid.
{ "error": "Unauthorized" }
400
Bad Request
Invalid request body. Plan name is invalid or missing.
{ "error": "Invalid request body" }
500
Internal Server Error
Payment provider error or server error.
{
  "error": "Failed to create checkout session"
}

Post-Checkout Flow

After successful payment:
  1. User completes payment on provider’s checkout page
  2. Provider redirects to successUrl
  3. Webhook is sent to /api/webhooks/payments (see Webhooks)
  4. Database is updated with subscription/payment records
  5. User gains access to plan features

Next Steps

Customer Portal

Manage subscriptions and billing

Webhooks

Handle payment events

Database Schema

Payment-related tables

Subscription API

Retrieve subscription data