Loading...
Loading...
> Payment processing optimized for digital products and indie hackers.
Polar.sh is a modern payment platform designed specifically for digital products, open-source creators, and indie hackers. It offers simple integration, built-in discount codes, and lower fees than traditional processors. Perfect for selling boilerplates, courses, and digital downloads.
DESC: Go to polar.sh and create an account. Set up your organization and complete onboarding.
DESC: Go to Settings → Developer Settings and create an access token with full permissions.
DESC: Go to Products and create your digital product. Copy the Product ID from the product details page.
DESC: Add these to your .env.local file
1$# Polar.sh API Keys2$POLAR_ACCESS_TOKEN="your_access_token_here"3$NEXT_PUBLIC_POLAR_PRODUCT_ID="your_product_id"45$# Optional: Webhook secret for order notifications6$POLAR_WEBHOOK_SECRET="your_webhook_secret"
DESC: In Marketing → Discounts, create a discount code with usage limits.
1$# The discount ID is found in the discount details2$# You can set auto-expiring discounts (e.g., first 100 customers)3$FABRK_DISCOUNT_ID="1161689c-dbc2-4e53-8c18-43f4af7aaa3f"
Tip: Use usage-limited discounts for launch promotions!
Pre-built component that handles checkout flow
1"use client";23import { useState } from "react";4import { Button } from "@/components/ui/button";5import { Loader2 } from "lucide-react";6import { toast } from "sonner";78export function PolarCheckoutButton({9 customerEmail,10 discountId,11 children = "Get Started - $199",12}) {13 const [isLoading, setIsLoading] = useState(false);1415 const handleCheckout = async () => {16 setIsLoading(true);1718 try {19 const response = await fetch("/api/polar/checkout", {20 method: "POST",21 headers: { "Content-Type": "application/json" },22 body: JSON.stringify({23 customerEmail,24 discountId,25 metadata: { timestamp: new Date().toISOString() },26 }),27 });2829 const data = await response.json();3031 if (!response.ok) {32 throw new Error(data.details || "Failed to create checkout");33 }3435 // Redirect to Polar.sh checkout36 window.location.href = data.checkoutUrl;37 } catch (error) {38 toast.error(error.message || "Failed to start checkout");39 setIsLoading(false);40 }41 };4243 return (44 <Button onClick={handleCheckout} disabled={isLoading}>45 {isLoading ? (46 <>47 <Loader2 className="mr-2 h-4 w-4 animate-spin" />48 Loading...49 </>50 ) : (51 children52 )}53 </Button>54 );55}
Server-side checkout session creation with rate limiting
1// src/app/api/polar/checkout/route.ts2import { NextRequest, NextResponse } from "next/server";3import { createCheckoutSession, isPolarConfigured } from "@/lib/polar";4import { checkRateLimitAuto, getClientIdentifier, RateLimiters } from "@/lib/security/rate-limit";5import { env } from "@/lib/env";67export async function POST(request: NextRequest) {8 // Rate limit: 10 requests/minute9 const identifier = getClientIdentifier(request);10 const rateLimit = await checkRateLimitAuto(identifier, RateLimiters.strict);1112 if (!rateLimit.success) {13 return NextResponse.json(14 { error: "Too many attempts. Please try again later." },15 { status: 429 }16 );17 }1819 // Mock checkout for development20 if (!isPolarConfigured()) {21 const baseUrl = env.client.NEXT_PUBLIC_APP_URL || "http://localhost:3000";22 return NextResponse.json({23 checkoutUrl: `${baseUrl}/purchase/success?mock=true`,24 _mock: true,25 });26 }2728 try {29 const { customerEmail, discountId, metadata } = await request.json();30 const baseUrl = env.client.NEXT_PUBLIC_APP_URL || "http://localhost:3000";3132 const checkout = await createCheckoutSession({33 customerEmail,34 successUrl: `${baseUrl}/purchase/success`,35 discountId,36 metadata: { source: "website", ...metadata },37 });3839 return NextResponse.json({40 checkoutUrl: checkout.url,41 checkoutId: checkout.id,42 });43 } catch (error) {44 return NextResponse.json(45 { error: "Failed to create checkout" },46 { status: 500 }47 );48 }49}
Core functions for interacting with Polar API
1// src/lib/polar.ts2import { Polar } from "@polar-sh/sdk";3import { env } from "@/lib/env";45export const isPolarConfigured = () => !!env.server.POLAR_ACCESS_TOKEN;67export const polar = new Polar({8 accessToken: env.server.POLAR_ACCESS_TOKEN || "not-configured",9});1011export const PRODUCT_ID = env.client.NEXT_PUBLIC_POLAR_PRODUCT_ID;1213export async function createCheckoutSession(params: {14 customerEmail?: string;15 successUrl: string;16 discountId?: string;17 metadata?: Record<string, string>;18}) {19 if (!PRODUCT_ID) {20 throw new Error("Polar product ID not configured");21 }2223 const checkout = await polar.checkouts.create({24 products: [PRODUCT_ID],25 discountId: params.discountId,26 customerEmail: params.customerEmail,27 successUrl: params.successUrl,28 metadata: params.metadata,29 });3031 return checkout;32}3334export async function getProduct() {35 if (!PRODUCT_ID) throw new Error("Product ID not configured");36 return polar.products.get({ id: PRODUCT_ID });37}
When to use Polar
When POLAR_ACCESS_TOKEN is not set, the checkout API returns a mock response that redirects to your success page. This lets you test the full purchase flow without real payments.
// Mock response when Polar isn't configured
{
"checkoutUrl": "http://localhost:3000/purchase/success?mock=true",
"_mock": true
}Create discounts that auto-expire after N uses. Perfect for "First 100 customers get 25% off" promotions.
Set expiration dates for launch week or holiday promotions.
Pass a special discount ID when user tries to leave. See exit-intent-popup.tsx.
src/lib/polar.tsSDK client and helperssrc/app/api/polar/checkout/route.tsCheckout API endpointsrc/components/polar/checkout-button.tsxCheckout button componentsrc/components/polar/discount-counter.tsxUsage counter display