Skip to main content

Polar Deep Dive

This page covers the complete Polar integration, including checkout creation, subscription management, customer portal, and webhook processing.

Overview

Polar is a modern payment platform designed for software and digital products. The integration supports both one-time payments and subscriptions through Polar's checkout system, with webhook-driven lifecycle management. Polar uses organization-scoped products and the @polar-sh/sdk for API interactions.

Route Table

MethodPathAuthDescription
POST/api/polar/checkoutSession requiredCreate checkout session (subscription or one-time)
GET/api/polar/checkoutSession requiredRetrieve checkout session status
POST/api/polar/webhookSignature requiredProcess incoming webhook events

Checkout Creation (POST)

Request Body

interface PolarCheckoutRequest {
productId: string; // Polar product ID
mode?: 'one_time' | 'subscription'; // Defaults to "subscription"
successUrl: string; // Redirect URL after success
cancelUrl: string; // Redirect URL after cancel
metadata?: {
planId?: string;
planName?: string;
billingInterval?: string;
[key: string]: any;
};
}

Example Request

curl -X POST /api/polar/checkout \
-H "Content-Type: application/json" \
-H "Cookie: session=..." \
-d '{
"productId": "prod_1234567890abcdef",
"mode": "subscription",
"successUrl": "https://example.com/success",
"cancelUrl": "https://example.com/cancel",
"metadata": { "planId": "pro_plan", "planName": "Pro Plan" }
}'

How It Works

The checkout route handles two flows:

Subscription Mode:

  1. Authenticates the user and resolves the Polar customer
  2. Sanitizes metadata (removes undefined values -- Polar rejects them)
  3. Calls polarProvider.createSubscription() which creates a checkout session
  4. Returns the checkout URL from the subscription result

One-Time Payment Mode:

  1. Authenticates the user and resolves the Polar customer
  2. Uses the Polar SDK directly to create a checkout
  3. Returns the checkout URL

Metadata Sanitization

Polar requires that all metadata values are non-null and non-undefined:

const sanitizedMetadata: Record<string, any> = {
userId: session.user.id || ''
};
if (metadata.planId) sanitizedMetadata.planId = metadata.planId;
if (metadata.planName) sanitizedMetadata.planName = metadata.planName;
// Only include defined values
Object.entries(metadata).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
sanitizedMetadata[key] = value;
}
});

Success Response (200)

{
"data": {
"id": "checkout_1234567890abcdef",
"url": "https://polar.sh/checkout/checkout_1234567890abcdef"
},
"status": 200,
"message": "Checkout session created successfully"
}

Retrieving a Checkout Session (GET)

Query Parameters

ParameterRequiredDescription
checkout_idYesPolar checkout session ID

Success Response (200)

{
"checkout": { "...full Polar checkout object..." },
"status": "complete",
"customer": "customer_1234567890abcdef",
"subscription": "subscription_1234567890abcdef"
}

Subscription Management

Creating Subscriptions

The PolarProvider.createSubscription() method creates a checkout for the subscription:

const checkout = await this.polar.checkouts.create({
products: [priceId],
organizationId: this.organizationId,
customerId: customerId,
successUrl: metadata?.successUrl,
metadata: sanitizedMetadata
});

Cancelling Subscriptions

Polar supports two cancellation strategies:

// Cancel at period end (soft cancel)
await cancelSubscriptionAtPeriodEnd({ polar, subscriptionId });

// Cancel immediately (hard cancel)
await cancelSubscriptionImmediately({ polar, subscriptionId });

The provider validates the subscription state before cancellation:

const validateResult = validateSubscriptionId(subscriptionId);
if (!validateResult.isValid) {
throw new PolarFatalError(validateResult.error);
}

Reactivating Subscriptions

Subscriptions scheduled for cancellation can be reactivated:

if (isScheduledForCancellation(subscription)) {
const result = await reactivatePolarSubscription({
polar, subscriptionId, subscription
});
}

Updating Subscriptions

Plan changes are handled through polar.subscriptions.update():

const updated = await this.polar.subscriptions.update({
id: subscriptionId,
productId: newProductId
});

Webhook Processing

Signature Verification

Polar uses the @polar-sh/sdk/webhooks validateEvent function for verification. The webhook requires three headers:

HeaderDescription
webhook-signatureHMAC SHA256 signature (format: v1,<hex_signature>)
webhook-timestampUnix timestamp of the event
webhook-idUnique webhook delivery ID
const webhookResult = await polarProvider.handleWebhook(
body, // Parsed JSON
signatureHeader, // Full "v1,..." signature
bodyText, // Raw body for verification
timestampHeader,
webhookIdHeader
);

Event Types

Polar EventInternal Mapping
checkout.succeededPayment succeeded
checkout.failedPayment failed
subscription.createdSubscription created
subscription.updatedSubscription updated
subscription.canceledSubscription cancelled
invoice.paidSubscription payment succeeded
invoice.payment_failedSubscription payment failed

Webhook Router

Events are dispatched through a dedicated router module:

await routeWebhookEvent(webhookResult.type, webhookResult.data);

The router maps event types to handler functions that update the database via WebhookSubscriptionService and send email notifications.

Payload Validation

The webhook endpoint validates the payload structure before processing:

if (!validateWebhookPayload(body)) {
return NextResponse.json({ error: 'Invalid webhook payload' }, { status: 400 });
}

Customer Management

The provider follows the standard three-step resolution pattern:

  1. Check user metadata for the Polar customer ID
  2. Query the PaymentAccount database table
  3. Create a new customer via the Polar SDK
const customer = await this.polar.customers.create({
organizationId: this.organizationId,
email: params.email,
name: params.name,
metadata: params.metadata
});

Error Handling

StatusErrorCause
400Product ID is requiredMissing productId in request
400Checkout ID is requiredGET request missing checkout_id
400No signature providedWebhook missing signature header
401UnauthorizedNo authenticated session
500Failed to create checkoutCheckout URL not available
500Configuration errorPolar provider not configured
503Payment setup incompleteOrganization has not completed payment setup in Polar

The checkout endpoint includes special detection for payment setup errors:

if (error.message.includes('Payments are currently unavailable') ||
error.message.includes('needs to complete their payment setup')) {
statusCode = 503;
fallbackMessage = 'Polar payment setup incomplete...';
}

Configuration Requirements

VariableRequiredDescription
POLAR_ACCESS_TOKENYesPolar API access token
POLAR_WEBHOOK_SECRETYesWebhook signing secret
POLAR_ORGANIZATION_IDYesPolar organization ID

Security Considerations

  • Webhook signatures are verified using the validateEvent function from the official SDK
  • Raw body text is preserved for signature verification (JSON re-serialization could alter the body)
  • Three separate headers are checked: signature, timestamp, and webhook ID
  • Metadata is sanitized server-side to prevent injection of undefined values
  • Error responses use safeErrorResponse to prevent information leakage