Claude Code Payments: Stripe, Paddle, Lemon Squeezy, Polar
TL;DR
- Claude Code writes billing code that charges a test card on the first try, but by default it trusts the redirect instead of the webhook, parses the body before verifying it, processes retried events twice and hardcodes price IDs.
- One CLAUDE.md fixes that for any provider. Put the shared rules (verify on the raw body, deduplicate, grant access only from verified events, prices by ID from config, sandbox and live kept apart) in one skeleton, then add a short block for your provider.
- The biggest difference is who sells to your customer. Stripe is a payment processor, so tax and invoicing are your job unless you add Stripe Tax or use Stripe Managed Payments. Paddle, Lemon Squeezy and Polar are merchants of record that collect and remit sales tax for you.
- Webhook signing differs too: Stripe signs
timestamp.bodywith HMAC-SHA256 inStripe-Signature, Paddle signstimestamp:bodyinPaddle-Signature, Lemon Squeezy sends an HMAC-SHA256 hex digest of the body inX-Signature, and Polar uses the Standard Webhooks headers for secrets created from September 2026. - Every provider fact in this guide was checked against that provider's documentation or pricing page on 21 October 2026.
Claude Code can build a subscription billing flow in an afternoon, provided your CLAUDE.md says which rules are not up for negotiation. This guide gives you that file. It starts with the skeleton every billing integration shares, compares Stripe, Paddle, Lemon Squeezy and Polar side by side, and then lists only the rules specific to each provider, so you copy one shared block plus one provider block.
If you are new to project configuration, our guide to CLAUDE.md explains how Claude Code loads the file every session, and the Claude Code setup guide covers the rest of the stack.
Why Claude Code needs payment rules in CLAUDE.md
Payments punish the gap between "works in the demo" and "correct under retries". A checkout that redirects to a success page proves almost nothing: the customer can close the tab before the redirect, the redirect can be replayed, and the provider can deliver the same webhook three times. Claude cannot see any of that from SDK types, so it writes the shortest code that compiles.
Across all four providers, the same six failure patterns appear without a CLAUDE.md:
- Access granted on the success redirect. Claude flips the user to "paid" when the browser lands on
/success. The authoritative signal is the verified webhook. - Verification after parsing. Claude calls
req.json()first and then tries to verify a signature over re-serialised JSON. Every provider here signs the raw bytes, so this fails or, worse, gets "fixed" by removing the check. - No deduplication. Every provider retries webhooks. Without a processed-events table the second delivery grants a second license or sends a second receipt.
- Order-dependent handlers. Events can arrive out of order. A handler that assumes
createdprecedesupdatedwill eventually overwrite newer state with older state. - Hardcoded prices and IDs. Price IDs pasted into components break the moment you switch between test and live, which every provider here keeps as separate environments.
- Secrets in the wrong place. Server API keys end up in client components or in a file Claude committed.
Payment processor or merchant of record?
Settle this before Claude writes a line, because it changes which code should exist at all.
A payment processor (Stripe in its default mode) moves money for your business. Your business is the seller, so sales tax, VAT registration, invoices and many disputes are yours to handle, with optional products such as Stripe Tax to help.
A merchant of record (Paddle, Lemon Squeezy, Polar, and Stripe Managed Payments) is the legal seller to your customer. It calculates, collects and remits sales tax and VAT, issues the receipts and takes on more of the compliance and dispute work, in exchange for a higher fee.
The practical rule for CLAUDE.md: on a merchant of record, Claude must not write tax calculation, invoice generation or VAT number handling, because the provider owns them and duplicating them creates conflicting numbers. On a processor, those are real features that need real decisions, not quick helpers.
Which payment provider should you use with Claude Code?
All four have official SDKs and documentation Claude can follow once your CLAUDE.md pins the rules. Prices are the published headline rates on each provider's pricing page as of 21 October 2026; they change and carry extra fees for some cards and countries, so check the source before quoting them.
| Stripe | Paddle | Lemon Squeezy | Polar | |
|---|---|---|---|---|
| Role | Payment processor (merchant of record via Managed Payments) | Merchant of record | Merchant of record | Merchant of record |
| Headline fee | 2.9% + 30¢ (US domestic cards); Billing 0.7%; Stripe Tax 0.5% (no-code) | 5% + 50¢ per checkout transaction | 5% + 50¢ per transaction | Starter 5% + 50¢; paid plans from 3.8% + 40¢ ($20/month); +1.5% international cards |
| Server SDK (Node) | stripe |
@paddle/paddle-node-sdk |
@lemonsqueezy/lemonsqueezy.js |
@polar-sh/sdk |
| API style | REST, form-encoded, version pinned per client | REST, JSON | JSON:API (application/vnd.api+json) |
REST, JSON |
| Webhook signing | Stripe-Signature: HMAC-SHA256 of t.body, 5 minute default tolerance |
Paddle-Signature: HMAC-SHA256 of ts:body, 5 second default tolerance |
X-Signature: HMAC-SHA256 hex of the body |
Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature) |
| Test environment | Sandboxes and test mode | Separate sandbox account and API host | Test mode keys and test_mode checkouts |
Separate sandbox environment and API host |
| Choose it when | You want the most control, or Connect for marketplaces | You sell SaaS globally and want tax handled | You already run on it; plan for Stripe Managed Payments | You are a developer selling software, licenses or access |
Sources: Stripe pricing, Paddle pricing, Lemon Squeezy pricing, Polar fees. Signing details are sourced in each provider section. All read on 21 October 2026.
The shared CLAUDE.md skeleton for payments
This block is provider-neutral. Put it in CLAUDE.md as-is, then append the provider block from the sections further down.
# Payments rules
## Architecture
- src/lib/billing/provider.ts the ONLY file that imports the provider SDK
- src/lib/billing/entitlements.ts grantAccess / revokeAccess: the only code that changes a user's plan
- src/app/api/webhooks/billing/ the webhook handler
- src/db/billing-events.ts processed event IDs and the last state per subscription
## Credentials
- Server API keys and webhook secrets live in environment variables, server-only
- Sandbox and live credentials are separate variables; startup asserts they match the environment
- Price and product IDs come from config per environment, NEVER literals in components
## Checkout
- The client never sends an amount; the server builds checkouts from configured price IDs
- Every checkout carries our user ID (metadata, custom data or external customer ID)
- The success page shows "processing" until the webhook has granted access
## Webhooks (MANDATORY)
- Read the raw body as text BEFORE any JSON parsing; verify the signature on those bytes
- Compare signatures with a constant-time function; reject stale timestamps
- Deduplicate on the provider's event ID; a duplicate returns 2xx and does nothing
- Store the event's own timestamp per subscription; ignore events older than the stored state
- Return 2xx fast; heavy work goes to a queue
## Entitlements
- Access changes ONLY from verified webhook events, never from the redirect
- Cancellation at period end keeps access until the provider says it ended
## Hard rules
- NEVER log card data, full webhook payloads or API keys
- NEVER run scripts against live credentials without explicit confirmation
- On a merchant of record: NEVER write tax calculation or invoice generation
Each section maps to one of the six failure patterns. Three of them deserve more than a line.
Verify first, on the raw body
Every provider here computes its signature over the exact bytes it sent. Frameworks that parse JSON before your handler runs destroy those bytes, so the handler must read the body as text first. In the Next.js App Router that is await req.text(); in Express it means a raw body parser on the webhook route only. Stripe's documentation is explicit that any manipulation of the raw body makes verification fail (Stripe webhooks).
Deduplicate and tolerate disorder
Retries are guaranteed. Stripe retries for up to three days in live mode; Paddle makes up to 60 attempts over about two days; Lemon Squeezy retries up to three more times with exponential backoff; Polar retries up to 10 times. Stripe and Paddle both state that delivery order is not guaranteed. One table handles both problems:
// inside the verified webhook handler
const fresh = await db.billingEvents.insertIfAbsent({ id: event.id });
if (!fresh) return new Response('ok'); // duplicate delivery
const current = await db.subscriptions.find(event.subscriptionId);
if (current && current.lastEventAt >= event.occurredAt) return new Response('ok'); // stale
await applyEvent(event);
Grant access from one place
Claude scatters user.plan = 'pro' across route handlers unless one module owns entitlements. Make grantAccess and revokeAccess the only functions that change a plan, call them only from the webhook handler, and the redirect page becomes a pure "we are confirming your payment" screen.
Stripe rules for Claude Code
Stripe is the most configurable provider here and the one Claude knows best, which is exactly why it needs explicit rules:
- Pin the API version. Stripe recommends specifying the version in code rather than relying on the account default. Server SDKs use the version that was current when the SDK was released, and in Node you can set it with the
apiVersionoption; the version shown in Stripe's upgrade guide on 21 October 2026 was2026-09-30.endive(upgrades). Tell Claude the pinned version and that upgrading it is a deliberate task, not a side effect. - Verify with
Stripe-Signature. The header carries at=timestamp and av1=signature, an HMAC-SHA256 of the timestamp, a dot and the raw payload; the official libraries apply a 5 minute tolerance by default. Stripe also publishes webhook source IPs and recommends allowlisting as a second layer (webhooks). - Use idempotency keys correctly. All v1
POSTrequests accept anIdempotency-Key; keys can be up to 255 characters and can be pruned after 24 hours; reusing a key with different parameters returns an error. Stripe suggests random V4 UUIDs and warns against putting personal data such as email addresses in keys (idempotent requests). The rule for Claude: generate the key once per logical operation, store it with the order, and reuse it on every retry. A new UUID per attempt defeats the purpose. - Test webhooks locally with the CLI.
stripe listen --forward-to localhost:4242/webhookforwards events and prints a signing secret for local use, andstripe trigger payment_intent.succeededfires test events. - Connect the Stripe MCP server with OAuth. Stripe documents
claude mcp add --transport http stripe https://mcp.stripe.com/, then authenticating with/mcpinside Claude Code. Stripe states that from December 2026 its MCP server stops accepting full-access secret keys and restricted keys without the Agent tag, and that it asks for human confirmation before some write actions such as refunds (Stripe MCP). Use a sandbox while building. - Know what Managed Payments changes. With Stripe Managed Payments, Stripe is the merchant of record for digital products, handling tax in more than 80 countries, but it works only with Checkout and Payment Links, does not support Connect, and subscriptions must be created through Checkout or Payment Links (Managed Payments). If you choose it, tell Claude not to build Elements-based flows.
## Stripe
- SDK: stripe (Node), one client in src/lib/billing/provider.ts with apiVersion pinned
- STRIPE_SECRET_KEY (server), NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY (browser), STRIPE_WEBHOOK_SECRET
- Webhooks: stripe.webhooks.constructEvent(rawBody, sig, secret); default 5 min tolerance
- Idempotency-Key on every POST that creates or charges; generated once, stored, reused on retry
- No personal data in idempotency keys
- Grant access from checkout.session.completed / invoice.paid, revoke on customer.subscription.deleted
- Local: stripe listen --forward-to localhost:<port>/api/webhooks/billing
- MCP: remote server https://mcp.stripe.com with OAuth; sandbox while developing
A route-handler sketch that respects the order of operations:
// app/api/webhooks/billing/route.ts
import { stripe } from '@/lib/billing/provider';
export async function POST(req: Request) {
const raw = await req.text();
const sig = req.headers.get('stripe-signature') ?? '';
let event;
try {
event = stripe.webhooks.constructEvent(raw, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response('invalid signature', { status: 400 });
}
await enqueueBillingEvent({ id: event.id, type: event.type, occurredAt: event.created, data: event.data.object });
return new Response('ok');
}
Paddle rules for Claude Code
Paddle Billing is a merchant of record, and most Claude mistakes come from treating it like Stripe with different field names:
- Do not build what Paddle owns. Paddle collects the applicable tax, files sales tax returns and handles chargebacks and buyer support as part of its 5% + 50¢ fee (Paddle pricing). Tax helpers and invoice generators in your code duplicate state Paddle already holds.
- Checkouts take price IDs.
Checkout.open()needs at least one item, each with apriceIdandquantity, plus optionalcustomData(valid JSON, at least one key) that is copied to the subscription for recurring items. Initialise Paddle.js from@paddle/paddle-jswithinitializePaddle({ token })using a client-side token (Checkout.open). Put your user ID incustomDataso the webhook can find the user. - Sandbox is a separate account. Sandbox has its own credentials and data, its API lives at
https://sandbox-api.paddle.cominstead ofhttps://api.paddle.com, sandbox API keys contain_sdbxand sandbox client-side tokens start withtest_; using a credential against the wrong environment returnsforbidden(sandbox). Make the environment a single variable that selects host, key and token together. - Verify
Paddle-Signature. The header holdsts=and one or moreh1=values; the signature is an HMAC-SHA256 of the timestamp, a colon and the untouched raw body, keyed with the notification destination's secret. The Node SDK does it withawait paddle.webhooks.unmarshal(rawBody, secretKey, signature), and the default tolerance is five seconds (signature verification). - Respond in five seconds, dedupe on
event_id, order byoccurred_at. Paddle expects a 200 within five seconds, retries up to 60 times over about two days in live (3 times in about two minutes in sandbox), and does not guarantee delivery order (responding to webhooks). - Portal links are temporary.
POST /customers/{customer_id}/portal-sessions(customer IDs startctm_) returns an authenticated overview link plus per-subscription cancel and update links if you passsubscription_ids; the links carry temporary tokens and should not be cached (portal sessions). Store the Paddle customer ID from the first webhook rather than looking customers up by email.
## Paddle
- Server SDK: @paddle/paddle-node-sdk; browser: @paddle/paddle-js with initializePaddle({ token })
- PADDLE_ENV (sandbox|production) selects API host, PADDLE_API_KEY and client token together
- Checkout.open({ items: [{ priceId, quantity }], customData: { userId } }); price IDs from config
- Merchant of record: NO tax calculation, NO invoice generation in our code
- Webhooks: await paddle.webhooks.unmarshal(rawBody, PADDLE_WEBHOOK_SECRET, signature)
- Return 200 within 5 seconds; queue the work; dedupe on event_id; compare occurred_at
- Portal: create a portal session per request (POST /customers/{ctm_id}/portal-sessions); never cache links
Lemon Squeezy rules for Claude Code
Lemon Squeezy is a merchant of record with a strict JSON:API surface, and its company status matters for new projects:
- Know where the platform is heading. In a post dated 28 January 2026, Lemon Squeezy's CEO described Stripe Managed Payments as Stripe's merchant of record solution and said the goal is to give Lemon Squeezy users an easy way to migrate to it; the post gives no migration dates (2026 update). For an existing store, keep the integration behind your provider module so a future move touches one file. For a new project, compare it with Stripe Managed Payments first.
- Speak JSON:API. Every request needs
Accept: application/vnd.api+jsonandContent-Type: application/vnd.api+json, the body is wrapped indatawithtype,attributesandrelationships, and authentication is a Bearer API key; keys exist in live and test mode and are valid for a year (API requests). Field names are snake_case. - Build checkouts with relationships.
POST /v1/checkoutstakesstoreandvariantas relationships, and attributes such ascustom_price(an integer in cents),product_options.redirect_url,checkout_data.customfor your own data,test_modeandexpires_at(create checkout). Claude's default flat body ({ store_id, variant_id }) is rejected. - The License API is different. It is separate from the main API: requests need
Accept: application/json, andPOSTbodies areapplication/x-www-form-urlencoded, not JSON:API (License API). This is the rule Claude most reliably gets wrong, because it applies the main API's headers everywhere. - Verify
X-Signature. It is an HMAC-SHA256 hex digest of the raw body, keyed with the signing secret you set on the webhook; compare in constant time (signing requests). Your checkout's custom data comes back inmeta.custom_dataon order, subscription and license key events. A failed delivery is retried up to three more times with exponential backoff, then dropped (webhook requests), so log failures and reconcile from the API. - The official SDK returns errors as values.
@lemonsqueezy/lemonsqueezy.jsis configured withlemonSqueezySetup({ apiKey, onError })and its calls return{ data, error }(lemonsqueezy.js). Destructure and checkerroron every call.
## Lemon Squeezy
- SDK: @lemonsqueezy/lemonsqueezy.js, lemonSqueezySetup({ apiKey, onError }) once in provider.ts
- ALWAYS destructure { data, error } and throw on error
- Raw calls: Accept + Content-Type application/vnd.api+json, snake_case, data.type/attributes/relationships
- Checkout: relationships.store + relationships.variant; checkout_data.custom = { user_id }
- test_mode: true outside production; test-mode API key in non-production
- License API: Accept application/json, POST bodies form-encoded (URLSearchParams)
- Webhooks: HMAC-SHA256 hex of raw body vs X-Signature, timing-safe; user from meta.custom_data
- Only 3 retries: log every failed delivery and reconcile from the API on a schedule
Polar rules for Claude Code
Polar is a merchant of record built for developers selling software, licenses and access. Several of its current APIs differ from what Claude learned from older examples:
- Use an Organization Access Token, server-side only. Polar's API docs say to use an OAT for server-side calls and never expose it in client code; production lives at
https://api.polar.sh/v1and the fully isolated sandbox athttps://sandbox-api.polar.sh/v1, with separate tokens per environment and anenvironment: "sandbox"option in the TypeScript SDK (API introduction). - Checkouts take product IDs. A checkout session takes a
productsarray of product IDs, the first selected by default, with optional ad hocpricesper session. Link the buyer to your user withexternal_customer_id, which comes back ascustomer.external_idin webhooks, and addcheckout_id={CHECKOUT_ID}to thesuccess_urlto look the session up afterwards (checkout sessions, create checkout session). Metadata is limited to 50 pairs, with keys up to 40 characters and values up to 500. - Webhook verification depends on when the secret was made. Secrets generated on or after 8 September 2026 follow Standard Webhooks; older secrets use Polar HMAC (webhook endpoints). The SDK handles both: in TypeScript,
import { webhooks } from '@polar-sh/sdk/2026-10'and callwebhooks.validateEvent(body, headers, secret)with thewebhook-id,webhook-timestampandwebhook-signatureheaders. Polar times out after 10 seconds, recommends responding within 2, retries up to 10 times, and disables an endpoint after 10 consecutive failures (webhook delivery). - Drive access from customer state. Polar's event list includes
order.paid,subscription.active,subscription.canceled,subscription.revokedandcustomer.state_changed. Its docs point tocustomer.state_changedfor syncing active subscriptions and benefits,subscription.revokedfor removing access (end-of-period cancellations revoke when the period ends),order.paidrather thanorder.createdfor payment, and caution withsubscription.past_duebecause the customer can recover (webhook events). - Test webhooks with the CLI.
polar webhook listenforwards events to localhost andpolar webhook triggersends samples.
## Polar
- SDK: @polar-sh/sdk; Organization Access Token server-side only; environment: "sandbox" outside production
- Checkout: products: [PRODUCT_ID from config], external_customer_id: our user ID
- success_url includes checkout_id={CHECKOUT_ID}
- Webhooks: webhooks.validateEvent(rawBody, { webhook-id, webhook-timestamp, webhook-signature }, POLAR_WEBHOOK_SECRET)
- Respond within 2 seconds, queue the work; endpoint disables after 10 consecutive failures
- Access: sync from customer.state_changed; revoke on subscription.revoked; payment = order.paid
- subscription.past_due is NOT a revocation
Common Claude Code payment mistakes, and the rule that prevents each
Paste the rows that match your provider into CLAUDE.md; Claude follows before-and-after pairs more reliably than abstract rules.
| What Claude writes by default | Why it hurts | Rule to add |
|---|---|---|
Access granted on /success |
Closed tabs never upgrade; replayed URLs upgrade for free | Entitlements change only from verified webhooks |
await req.json() before verification |
Signature fails, or the check gets deleted | Read the raw body as text, verify, then parse |
signature === digest |
Timing side channel | Constant-time comparison with a length check |
| No processed-events table | Retries grant twice | Deduplicate on the provider event ID |
| A new idempotency key per Stripe retry | Duplicate charges on network errors | One key per logical operation, stored and reused |
calculateVAT() on Paddle, Lemon Squeezy or Polar |
Conflicts with the merchant of record's tax | No tax code on a merchant of record |
| A flat JSON body to Lemon Squeezy | Rejected by the JSON:API endpoint | data.type, attributes, relationships |
| JSON:API headers on the License API | Wrong content type for that API | Form-encoded POST, Accept: application/json |
| Price IDs in components | Break between sandbox and live | Price and product IDs from per-environment config |
Granting on Polar order.created |
Fires before payment | Use order.paid or customer state |
| Looking customers up by email | Ambiguous and slow | Store the provider customer ID from the first webhook |
Permission hooks for billing scripts
Billing projects collect scripts that touch money: seeding products, backfilling subscriptions, issuing refunds, replaying events. Allow the read-only ones and deny the rest so Claude has to ask:
{
"permissions": {
"allow": [
"Bash(stripe listen*)",
"Bash(stripe trigger*)",
"Bash(polar webhook listen*)",
"Bash(node scripts/billing/check-config.js*)"
],
"deny": [
"Bash(node scripts/billing/refund*)",
"Bash(node scripts/billing/backfill*)",
"Bash(node scripts/billing/seed-live*)"
]
}
}
Put this in .claude/settings.local.json; our guide to Claude Code permissions covers the rule syntax in depth.
Frequently asked questions
Which payment provider works best with Claude Code?
All four work well because each has an official SDK and clear documentation. Stripe gives the most control and suits marketplaces through Connect; Paddle suits SaaS sold globally with tax handled for you; Lemon Squeezy suits existing stores, with Stripe Managed Payments as its stated direction; Polar suits developers selling software, licenses and access.
Can Claude Code set up Stripe products and prices for me?
Yes. Connect the Stripe MCP server (claude mcp add --transport http stripe https://mcp.stripe.com/, then authenticate with /mcp) and Claude can create products and prices in a sandbox and record the IDs in your config. Keep it on a sandbox while building; Stripe asks for human confirmation on some write actions such as refunds.
Do I need to calculate sales tax if I use Paddle, Lemon Squeezy or Polar?
No. All three act as merchant of record, so they calculate, collect and remit sales tax and VAT. Tell Claude in CLAUDE.md not to write tax calculation or invoice generation, because duplicating it creates conflicting figures. With Stripe in its default mode, tax is your responsibility, with Stripe Tax as an option.
How do I stop Claude Code from granting access twice when a webhook retries?
Store each processed event ID in a table and return 2xx without doing anything when an ID repeats. Also store the timestamp of the latest event per subscription and ignore older events, because Stripe and Paddle do not guarantee delivery order.
What happened to the separate Stripe, Paddle, Lemon Squeezy and Polar guides?
They are merged into this page. Each provider's distinctive rules are in its own section above, under one shared payments skeleton that is easier to keep current. Every provider fact on this page was checked against current documentation on 21 October 2026.
Related guides
- Claude Code for deployment platforms: where billing secrets live on Vercel, Netlify, Railway, Render, Fly.io and Workers.
- Claude Code for transactional email: sending receipts and dunning mail from your billing webhooks.
- Claude Code MCP servers: connecting servers like Stripe's to Claude Code.
Build billing that holds up under retries
The pattern is the one that makes Claude Code reliable anywhere: decide the non-negotiable rules once, write them where Claude reads them every session, and keep the provider-specific part behind one module. For payments that means verifying on the raw body, deduplicating events, granting access only from webhooks, keeping sandbox and live apart, and letting a merchant of record own the tax.
Get Claudify. A complete Claude Code operating layer with 1,738 skills, 13 agents and 79 commands, plus persistent memory and quality hooks, ready to adapt to your stack.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify