Skip to main content

Endpoint

GET /api/polar/checkout

Overview

Creates a Polar checkout session for DROP service subscription. Redirects to Polar checkout page.

Request

Requires authentication via Bearer token.

Query Parameters

plan
string
required
Plan identifier: "pro-lite" or "pro"
billingCycle
string
required
Billing cycle: "monthly" or "yearly"

Response

Redirects to Polar checkout page (302 redirect).

Implementation Details

Code Reference

export async function GET(request: NextRequest) {
  const corsResponse = handleCors(request)
  if (corsResponse) return corsResponse

  const blocked = await protectRoute(request, { requested: 2 })
  if (blocked) return blocked

  const auth = await requireAuth(request)
  if ('error' in auth) return auth.error

  const searchParams = request.nextUrl.searchParams
  const plan = searchParams.get('plan')
  const billingCycle = searchParams.get('billingCycle')
  
  if (!plan || !billingCycle) {
    return errorResponse('Missing plan or billingCycle', 400, request.headers.get('origin'))
  }

  const productId = PRODUCT_IDS[plan]?.[billingCycle]
  
  if (!productId) {
    return errorResponse('Invalid plan or billingCycle', 400, request.headers.get('origin'))
  }

  const user = await prisma.user.findUnique({
    where: { id: auth.userId },
    select: { id: true, email: true },
  })

  if (!user) {
    return errorResponse('User not found', 404, request.headers.get('origin'))
  }

  const entitlement = await prisma.userServiceEntitlement.findUnique({
    where: {
      userId_service: {
        userId: auth.userId,
        service: 'DROP',
      },
    },
    select: { polarCustomerId: true },
  })

  const metadata = {
    plan,
    billingCycle,
    userId: user.id
  }

  let validCustomerId = null
  if (entitlement?.polarCustomerId) {
    try {
      const response = await fetch(`https://api.polar.sh/v1/customers/${entitlement.polarCustomerId}`, {
        headers: {
          'Authorization': `Bearer ${process.env.POLAR_ACCESS_TOKEN}`,
          'Content-Type': 'application/json'
        }
      })
      if (response.ok) {
        validCustomerId = entitlement.polarCustomerId
      }
    } catch (error) {
    }
  }

  const checkoutParams = new URLSearchParams({
    products: productId,
    metadata: JSON.stringify(metadata),
    customerEmail: user.email,
    ...(validCustomerId && { customerId: validCustomerId })
  })

  const checkoutHandler = Checkout({
    accessToken: process.env.POLAR_ACCESS_TOKEN!,
    successUrl: `${process.env.NEXT_PUBLIC_APP_URL || 'https://nulldrop.xyz'}/settings?tab=premium&success=true`,
    server: (process.env.POLAR_SERVER as "sandbox" | "production") || "production",
  })

  const modifiedRequest = new NextRequest(
    `${request.url.split('?')[0]}?${checkoutParams.toString()}`,
    request
  )

  return checkoutHandler(modifiedRequest)
}

Status Codes

302
Found
Redirect to Polar checkout page
400
Bad Request
Missing or invalid plan/billingCycle parameters
401
Unauthorized
Missing or invalid authentication token
404
Not Found
User not found

Example Request

curl -X GET "https://auth.nullpass.xyz/api/polar/checkout?plan=pro&billingCycle=monthly" \
  -H "Authorization: Bearer YOUR_TOKEN"

Available Plans

  • pro-lite: Pro Lite plan
    • monthly: Monthly billing
    • yearly: Yearly billing
  • pro: Pro plan
    • monthly: Monthly billing
    • yearly: Yearly billing

Environment Variables

DROP_PRO_LITE_MONTHLY
string
required
Polar product ID for Pro Lite monthly
DROP_PRO_LITE_YEARLY
string
required
Polar product ID for Pro Lite yearly
DROP_PRO_MONTHLY
string
required
Polar product ID for Pro monthly
DROP_PRO_YEARLY
string
required
Polar product ID for Pro yearly
POLAR_ACCESS_TOKEN
string
required
Polar API access token
POLAR_SERVER
string
default:"production"
Polar server: "sandbox" or "production"
NEXT_PUBLIC_APP_URL
string
App URL for success redirect (default: https://nulldrop.xyz)

Notes

  • Existing Polar customer ID is reused if available
  • Metadata includes plan, billingCycle, and userId
  • Success URL redirects to app settings page
  • Only works for DROP service