Manage Polar payments, products, checkouts, subscriptions, orders, and customers via the REST API using curl. Use this skill when the user asks to list products, create a checkout session, check subscription status, list orders, manage customers, inspect webhook events, or perform any Polar payment-related operations. This project uses Polar with a multi-tier pricing model (Free/Pro/Team/Enterprise) across sandbox and production environments.
Manage Polar payments and subscriptions via the REST API using curl. Polar has no dedicated CLI, so all operations use authenticated HTTP requests.
app/actions/polar.ts or app/lib/polar-products.ts directlyapp/actions/polar-webhook.tsapp/lib/plan-features.tsconvex-opsAuthentication: Source the env vars from :
.env.localexport POLAR_ACCESS_TOKEN=$(grep POLAR_ACCESS_TOKEN .env.local | head -1 | cut -d'=' -f2- | tr -d '"' | tr -d "'")
To load all product IDs at once:
export POLAR_PRO_MONTHLY_ID=$(grep POLAR_PRO_MONTHLY_ID .env.local | cut -d'=' -f2-)
export POLAR_PRO_ANNUAL_ID=$(grep POLAR_PRO_ANNUAL_ID .env.local | cut -d'=' -f2-)
export POLAR_TEAM_MONTHLY_ID=$(grep POLAR_TEAM_MONTHLY_ID .env.local | cut -d'=' -f2-)
export POLAR_TEAM_ANNUAL_ID=$(grep POLAR_TEAM_ANNUAL_ID .env.local | cut -d'=' -f2-)
export POLAR_ENTERPRISE_MONTHLY_ID=$(grep POLAR_ENTERPRISE_MONTHLY_ID .env.local | cut -d'=' -f2-)
export POLAR_ENTERPRISE_ANNUAL_ID=$(grep POLAR_ENTERPRISE_ANNUAL_ID .env.local | cut -d'=' -f2-)
Tokens can be managed at: https://sandbox.polar.sh/dashboard/settings (sandbox) or https://dashboard.polar.sh/settings (production).
Base URLs:
| Environment | Base URL |
|---|---|
| Sandbox | https://sandbox-api.polar.sh |
| Production | https://api.polar.sh |
This project uses sandbox mode for dev/preview. All examples below use the sandbox URL. Add a trailing / to paths (the API returns 307 redirects without it).
Rate limits: 100 requests/minute (sandbox), 500 requests/minute (production).
| Tier | Monthly | Annual | Target | Billing |
|---|---|---|---|---|
| Free | $0 | $0 | Individuals (no Polar product) | N/A |
| Pro | $8/mo | $72/yr | Power users / small teams | Per-user |
| Team | $29/seat/mo | $264/seat/yr | Organizations (5-100 seats) | Seat-based |
| Enterprise | $99/mo | $990/yr | Large organizations (100+ seats) | Per-org |
Free tier has no Polar product -- it's the default state when no subscription exists.
Team seat-based pricing: Team products use amount_type: "seat_based" with volume tiers. Checkouts for Team plans pass seats: <count> to set the initial seat count.
Each paid tier has a monthly and annual product in Polar. The env vars are:
| Env Var | Tier | Interval |
|---|---|---|
POLAR_PRO_MONTHLY_ID | Pro | Monthly |
POLAR_PRO_ANNUAL_ID | Pro | Yearly |
POLAR_TEAM_MONTHLY_ID | Team | Monthly |
POLAR_TEAM_ANNUAL_ID | Team | Yearly |
POLAR_ENTERPRISE_MONTHLY_ID | Enterprise | Monthly |
POLAR_ENTERPRISE_ANNUAL_ID | Enterprise | Yearly |
Sandbox and production have separate product IDs (same names, different UUIDs). The IDs are set in:
.env.local -- sandbox IDs for local devPOLAR_SERVER ("sandbox" or "production")app/lib/polar-products.ts -- PLAN_PRODUCT_MAP (tier+cycle -> product ID) and resolvePlanFromProductId() (product ID -> tier+cycle)app/lib/plan-features.ts -- PLAN_FEATURES defines feature limits per tier, getPlanFeatures(), hasFeature(), isAtLeastTier()app/actions/polar.ts -- createPolarCheckoutSession, getPolarCheckoutSession, getPolarCustomer, getCustomerPortalUrl/api/polar/webhook (TanStack Start API route) -- validates events using @polar-sh/sdk/webhooks validateEvent(), then delegates to app/actions/polar-webhook.tsapp/actions/polar-webhook.ts -- standalone handler processes subscription.created, subscription.active, subscription.updated, subscription.canceled, subscription.revoked eventsapi.organization.updateOrgPlan in Convex to update orgSettings.plan, orgSettings.polarSubscriptionId, orgSettings.polarCustomerId, and auto-derives orgSettings.featureFlagsWhen a checkout is created, clerkOrgId is passed in the checkout metadata. When the subscription webhook fires, the handler reads metadata.clerkOrgId to find and update the correct org in Convex.
All requests use:
Authorization: Bearer $POLAR_ACCESS_TOKEN
# List all products
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/products/" | jq '.items[] | {id, name, is_archived, recurring_interval, prices: [.prices[] | {amount: .price_amount, interval: .recurring_interval}]}'
# Get a specific product by ID (e.g., Team Monthly)
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/products/$POLAR_TEAM_MONTHLY_ID/" | jq
# List only active (non-archived) products
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/products/?is_archived=false" | jq
# Create a checkout session for Team Monthly plan
curl -sL -X POST \
-H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"products\": [\"$POLAR_TEAM_MONTHLY_ID\"],
\"success_url\": \"https://sentio.sh/org/test-org/welcome?checkout_id={CHECKOUT_ID}\",
\"customer_email\": \"[email protected]\",
\"customer_name\": \"Test User\",
\"metadata\": {
\"plan\": \"team\",
\"billingCycle\": \"monthly\",
\"clerkOrgId\": \"<org-id>\"
}
}" \
"https://sandbox-api.polar.sh/v1/checkouts/" | jq '{id, url, status}'
# Create a checkout for Pro Annual plan
curl -sL -X POST \
-H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"products\": [\"$POLAR_PRO_ANNUAL_ID\"],
\"success_url\": \"https://sentio.sh/org/test-org/welcome?checkout_id={CHECKOUT_ID}\",
\"customer_email\": \"[email protected]\"
}" \
"https://sandbox-api.polar.sh/v1/checkouts/" | jq '.url'
# Get a checkout session by ID
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/checkouts/<checkout-id>/" | jq
# List all orders
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/orders/" | jq
# List orders with pagination
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/orders/?page=1&limit=20" | jq
# Get a specific order
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/orders/<order-id>/" | jq
# List all subscriptions
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/subscriptions/" | jq
# Filter active subscriptions
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/subscriptions/?active=true" | jq
# Get a specific subscription (includes product info and metadata)
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/subscriptions/<subscription-id>/" | jq '{id, status, product: .product.name, metadata, customer_id}'
# Cancel a subscription
curl -sL -X DELETE \
-H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/subscriptions/<subscription-id>/" | jq
# List all customers
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/customers/" | jq
# Search customers by email
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/customers/[email protected]" | jq
# Get a specific customer
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/customers/<customer-id>/" | jq
# List all benefits
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/benefits/" | jq
# Get a specific benefit
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/benefits/<benefit-id>/" | jq
# List webhook endpoints
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/webhooks/endpoints/" | jq
# List webhook deliveries (recent events)
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/webhooks/deliveries/" | jq
# Get metrics overview
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/metrics/?start_date=2024-01-01&end_date=2026-12-31&interval=month" | jq
Look up the customer:
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/customers/[email protected]" | jq
Check their subscriptions:
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/subscriptions/?customer_id=<customer-id>" | jq '.items[] | {id, status, product: .product.name, metadata}'
Check their orders:
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/orders/?customer_id=<customer-id>" | jq
Review recent webhook deliveries:
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/webhooks/deliveries/?limit=5" | jq
Cross-reference with Convex org settings:
npx convex run organization:getOrgSettingsByClerkOrgId '{ "clerkOrgId": "<org-id>"}'
Decide which tier and billing cycle to test. Available products:
| Plan | Monthly Env Var | Annual Env Var |
|---|---|---|
| Pro | POLAR_PRO_MONTHLY_ID | POLAR_PRO_ANNUAL_ID |
| Team | POLAR_TEAM_MONTHLY_ID | POLAR_TEAM_ANNUAL_ID |
| Enterprise | POLAR_ENTERPRISE_MONTHLY_ID | POLAR_ENTERPRISE_ANNUAL_ID |
Verify the product exists:
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/products/$POLAR_TEAM_MONTHLY_ID/" | jq '{name, prices: [.prices[] | {amount: .price_amount, currency: .price_currency, interval: .recurring_interval}]}'
Create a checkout session:
curl -sL -X POST \
-H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"products\": [\"$POLAR_TEAM_MONTHLY_ID\"],
\"success_url\": \"https://sentio.sh/org/test-org/welcome?checkout_id={CHECKOUT_ID}\",
\"customer_email\": \"[email protected]\",
\"customer_name\": \"Test User\",
\"metadata\": {
\"plan\": \"team\",
\"billingCycle\": \"monthly\",
\"clerkOrgId\": \"test-org-id\"
}
}" \
"https://sandbox-api.polar.sh/v1/checkouts/" | jq '.url'
Open the returned URL to complete the test checkout.
Team plans use seat-based pricing. To test a Team checkout with a specific seat count:
Create a checkout with seats parameter:
curl -sL -X POST \
-H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"products\": [\"$POLAR_TEAM_MONTHLY_ID\"],
\"seats\": 5,
\"success_url\": \"https://sentio.sh/org/test-org/welcome?checkout_id={CHECKOUT_ID}\",
\"customer_email\": \"[email protected]\",
\"customer_name\": \"Test User\",
\"metadata\": {
\"plan\": \"team\",
\"billingCycle\": \"monthly\",
\"clerkOrgId\": \"test-org-id\"
}
}" \
"https://sandbox-api.polar.sh/v1/checkouts/" | jq '.url'
Open the returned URL to complete the checkout. The subscription will be created with the specified seat count.
Verify the subscription was created with the correct seat count:
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/subscriptions/?active=true" | jq '.items[-1] | {id, status, product: .product.name, seats}'
After a checkout completes, verify the webhook updated the org:
Find the subscription:
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/subscriptions/?active=true" | jq '.items[-1] | {id, status, product: .product.name, metadata}'
Check webhook delivery:
curl -sL -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \
"https://sandbox-api.polar.sh/v1/webhooks/deliveries/?limit=3" | jq '.items[] | {event_type: .event, success: .success, created_at}'
Verify the Convex org was updated:
npx convex run organization:getOrgSettingsByClerkOrgId '{"clerkOrgId": "<clerkOrgId-from-metadata>"}'
# Should show plan: "team" (or whatever tier was purchased)
All list endpoints support pagination:
# Page 1, 20 items per page
?page=1&limit=20
# Page 2
?page=2&limit=20
Response includes pagination.total_count and pagination.max_page.
Replace the base URL:
https://sandbox-api.polar.shhttps://api.polar.shAnd use the production POLAR_ACCESS_TOKEN. Production product IDs are different from sandbox -- get them from Vercel production env vars:
vercel env pull --environment=production /tmp/prod-env && grep POLAR_ /tmp/prod-env
POLAR_ACCESS_TOKEN env var/ to API paths (avoids 307 redirects)jq for readable JSON-sL flags on curl (silent + follow redirects)Content-Type: application/json header on POST/PATCH requestsclerkOrgId in checkout metadata for subscription -> org linking