Loading...
Loading...
Add Polar billing to a TypeScript/JavaScript app using the @polar-sh/sdk package. Use this skill whenever the user wants to add a Checkout endpoint, a Customer Portal endpoint, or a Webhooks endpoint for Polar to any framework — Next.js, Express, Hono, Astro, SvelteKit, Remix, TanStack Start, Nuxt, Fastify, Elysia, Deno, Supabase Edge Functions, Cloudflare Workers, Bun, etc.
npx skill4agent add polarsource/skills polar-integration@polar-sh/sdkRequestResponsenpm install @polar-sh/sdk
# or pnpm / yarn / bun@polar-sh/<framework>@polar-sh/nextjs@polar-sh/express@polar-sh/hono@polar-sh/sdkprocess.envDeno.envimport.meta.envPOLAR_ACCESS_TOKENPOLAR_WEBHOOK_SECRETPOLAR_SERVERsandboxproductionimport { Polar } from "@polar-sh/sdk";
const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: (process.env.POLAR_SERVER as "sandbox" | "production") ?? "production",
});POST /v1/checkouts/polar.checkouts.create(...)polar.checkouts.create()camelCasesnake_case| SDK field (camelCase) | API field (snake_case) | Required | Notes |
|---|---|---|---|
| | yes | Array of product UUIDs. The first is selected by default. |
| | no | Existing Polar customer ID. |
| | no | Your own user ID. If no Polar customer matches, one is created with this external ID. |
| | no | |
| | no | |
| | no | |
| | no | |
| | no | |
| | no | |
| | no | Copied to the created customer. |
| | no | Default |
| | no | Copied to the resulting order/subscription. |
| | no | Map of custom field slug → value. |
| | no | |
| | no | Default |
| | no | Default |
| | no | Predefined number of seats (seat-based pricing only). |
| | no | Seat-based pricing only. |
| | no | Cents. Only used for |
| | no | Upgrade an existing free subscription. |
| | no | See below. |
| | no | Back-button URL on the checkout page. |
| | no | Set when embedding the checkout in an iframe. |
| | no | IETF BCP 47 ( |
| | no | ISO 4217 (e.g. |
| | no | Override product trial. |
| | no | Default |
| | no | Map of product ID → ad-hoc prices (override catalog prices). |
{ id, url, client_secret, expires_at, status, ... }urlclient_secret@polar-sh/checkoutsuccessUrl{CHECKOUT_ID}{CHECKOUT_ID}?checkout_id={CHECKOUT_ID}polar.checkouts.get(checkoutId)theme?theme=light|darkresult.urlpolar.checkouts.create()import { Polar } from "@polar-sh/sdk";
const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: "production",
});
const SUCCESS_URL = "https://example.com/success?checkoutId={CHECKOUT_ID}";
const THEME: "light" | "dark" | undefined = undefined;
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const products = url.searchParams.getAll("products");
if (products.length === 0) {
return Response.json(
{ error: "Missing products in query params" },
{ status: 400 },
);
}
const json = (key: string) =>
url.searchParams.has(key)
? JSON.parse(url.searchParams.get(key) ?? "{}")
: undefined;
const str = (key: string) => url.searchParams.get(key) ?? undefined;
try {
const result = await polar.checkouts.create({
products,
successUrl: SUCCESS_URL,
customerId: str("customerId"),
externalCustomerId: str("customerExternalId"),
customerEmail: str("customerEmail"),
customerName: str("customerName"),
customerBillingAddress: json("customerBillingAddress"),
customerTaxId: str("customerTaxId"),
customerIpAddress: str("customerIpAddress"),
customerMetadata: json("customerMetadata"),
metadata: json("metadata"),
discountId: str("discountId"),
allowDiscountCodes: url.searchParams.has("allowDiscountCodes")
? url.searchParams.get("allowDiscountCodes") === "true"
: undefined,
seats: url.searchParams.has("seats")
? Number.parseInt(url.searchParams.get("seats") ?? "1", 10)
: undefined,
});
const redirectUrl = new URL(result.url);
if (THEME) redirectUrl.searchParams.set("theme", THEME);
return Response.redirect(redirectUrl.toString(), 302);
} catch (error) {
console.error(error);
return new Response(null, { status: 500 });
}
}?products=<product_id>polar.checkouts.create()metadataembedOriginlocaleexternalCustomerIdapp/api/checkout/route.tsNextRequestNextResponserequest: Requestimport type { Request, Response } from "express";
app.get("/checkout", async (req: Request, res: Response) => {
const url = new URL(`${req.protocol}://${req.get("host")}${req.originalUrl}`);
// ... same body using `url.searchParams` ...
// res.redirect(redirectUrl.toString());
// res.status(400).json({ error: "..." });
});fastify.get("/checkout", async (request, reply) => {
const url = new URL(`${request.protocol}://${request.hostname}${request.url}`);
// ... same body ...
// reply.redirect(redirectUrl.toString());
});app.get("/checkout", async (c) => {
const url = new URL(c.req.url);
// ... same body ...
return c.redirect(redirectUrl.toString());
});app.get("/checkout", ({ request, redirect }) => {
const url = new URL(request.url);
// ... same body, `return redirect(redirectUrl.toString())` ...
});server/api/checkout.get.tsdefineEventHandlergetQuerysendRedirect(event, url, 302)src/routes/api/checkout/+server.tsGET({ request, url })src/pages/api/checkout.tsexport const GET: APIRoute = async ({ request }) => { ... }POST /v1/customer-sessions/polar.customerSessions.create(...)polar.customerSessions.create()customerIdexternalCustomerId| SDK field | API field | Required | Notes |
|---|---|---|---|
| | one of these two | Existing Polar customer UUID. |
| | one of these two | Your own user ID. |
| | no | Back-button URL on the portal page. |
| | no | Only for orgs with |
| | no | Your member ID, alternative to |
{ id, customer_portal_url, token, expires_at, customer, customer_id, return_url, ... }customer_portal_urltokencustomerimport { Polar } from "@polar-sh/sdk";
const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: "production",
});
const RETURN_URL = "https://example.com/account";
// Replace with your auth: read a cookie/JWT/session and return your user id.
async function getExternalCustomerId(request: Request): Promise<string | null> {
const session = await getSession(request); // your auth helper
return session?.userId ?? null;
}
export async function GET(request: Request): Promise<Response> {
const externalCustomerId = await getExternalCustomerId(request);
if (!externalCustomerId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const { customerPortalUrl } = await polar.customerSessions.create({
externalCustomerId,
returnUrl: RETURN_URL,
});
return Response.redirect(customerPortalUrl, 302);
} catch (error) {
console.error(error);
return new Response(null, { status: 500 });
}
}externalCustomerIdcustomerIdpolar.customerSessions.create({ ... })externalCustomerIdcookies()next/headersreq.userc.get('user')polar.customerSessions.createwebhook-idwebhook-timestampwebhook-signaturevalidateEvent@polar-sh/sdk/webhooksimport { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";
const WEBHOOK_SECRET = process.env.POLAR_WEBHOOK_SECRET!;
export async function POST(request: Request): Promise<Response> {
const body = await request.text(); // must be the raw body, not parsed JSON
let event: ReturnType<typeof validateEvent>;
try {
event = validateEvent(
body,
{
"webhook-id": request.headers.get("webhook-id") ?? "",
"webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
"webhook-signature": request.headers.get("webhook-signature") ?? "",
},
WEBHOOK_SECRET,
);
} catch (error) {
if (error instanceof WebhookVerificationError) {
return Response.json({ received: false }, { status: 403 });
}
throw error;
}
switch (event.type) {
case "checkout.created":
case "checkout.updated":
// event.data is the Checkout
break;
case "order.created":
case "order.updated":
case "order.paid":
case "order.refunded":
// event.data is the Order — fulfill, send receipt, etc.
break;
case "subscription.created":
case "subscription.updated":
case "subscription.active":
case "subscription.canceled":
case "subscription.uncanceled":
case "subscription.revoked":
// event.data is the Subscription — flip entitlements in your DB
break;
case "refund.created":
case "refund.updated":
break;
case "product.created":
case "product.updated":
break;
case "benefit.created":
case "benefit.updated":
break;
case "benefit_grant.created":
case "benefit_grant.updated":
case "benefit_grant.revoked":
// grant or revoke the user's access to a benefit
break;
case "customer.created":
case "customer.updated":
case "customer.deleted":
case "customer.state_changed":
break;
case "organization.updated":
break;
}
return Response.json({ received: true });
}eventevent.datacasevalidateEventexpress.json()webhook-idevent.data.idorder.createdorder.paidapp/api/polar/webhook/route.tsrequest.text()app.post("/polar/webhook", express.raw({ type: "application/json" }), async (req, res) => {
const body = (req.body as Buffer).toString("utf8");
// ... validateEvent / switch ...
res.json({ received: true });
});request.rawBody@fastify/raw-bodyawait c.req.text()c.req.header("webhook-id")await request.text()Request+server.tsawait event.request.text()APIRouteawait request.text()await readRawBody(event)@polar-sh/<framework>@polar-sh/express@polar-sh/sdk@polar-sh/nextjs@polar-sh/better-auth