← All posts
·19 min read

Claude Code Payments: Stripe, Paddle, Lemon Squeezy, Polar

Claude CodePaymentsStripeAPI
Claude Code for payments: one CLAUDE.md, four billing providers

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.body with HMAC-SHA256 in Stripe-Signature, Paddle signs timestamp:body in Paddle-Signature, Lemon Squeezy sends an HMAC-SHA256 hex digest of the body in X-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:

  1. 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.
  2. 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.
  3. No deduplication. Every provider retries webhooks. Without a processed-events table the second delivery grants a second license or sends a second receipt.
  4. Order-dependent handlers. Events can arrive out of order. A handler that assumes created precedes updated will eventually overwrite newer state with older state.
  5. 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.
  6. 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 apiVersion option; the version shown in Stripe's upgrade guide on 21 October 2026 was 2026-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 a t= timestamp and a v1= 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 POST requests accept an Idempotency-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/webhook forwards events and prints a signing secret for local use, and stripe trigger payment_intent.succeeded fires test events.
  • Connect the Stripe MCP server with OAuth. Stripe documents claude mcp add --transport http stripe https://mcp.stripe.com/, then authenticating with /mcp inside 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 a priceId and quantity, plus optional customData (valid JSON, at least one key) that is copied to the subscription for recurring items. Initialise Paddle.js from @paddle/paddle-js with initializePaddle({ token }) using a client-side token (Checkout.open). Put your user ID in customData so 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.com instead of https://api.paddle.com, sandbox API keys contain _sdbx and sandbox client-side tokens start with test_; using a credential against the wrong environment returns forbidden (sandbox). Make the environment a single variable that selects host, key and token together.
  • Verify Paddle-Signature. The header holds ts= and one or more h1= 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 with await paddle.webhooks.unmarshal(rawBody, secretKey, signature), and the default tolerance is five seconds (signature verification).
  • Respond in five seconds, dedupe on event_id, order by occurred_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 start ctm_) returns an authenticated overview link plus per-subscription cancel and update links if you pass subscription_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+json and Content-Type: application/vnd.api+json, the body is wrapped in data with type, attributes and relationships, 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/checkouts takes store and variant as relationships, and attributes such as custom_price (an integer in cents), product_options.redirect_url, checkout_data.custom for your own data, test_mode and expires_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, and POST bodies are application/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 in meta.custom_data on 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.js is configured with lemonSqueezySetup({ apiKey, onError }) and its calls return { data, error } (lemonsqueezy.js). Destructure and check error on 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/v1 and the fully isolated sandbox at https://sandbox-api.polar.sh/v1, with separate tokens per environment and an environment: "sandbox" option in the TypeScript SDK (API introduction).
  • Checkouts take product IDs. A checkout session takes a products array of product IDs, the first selected by default, with optional ad hoc prices per session. Link the buyer to your user with external_customer_id, which comes back as customer.external_id in webhooks, and add checkout_id={CHECKOUT_ID} to the success_url to 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 call webhooks.validateEvent(body, headers, secret) with the webhook-id, webhook-timestamp and webhook-signature headers. 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.revoked and customer.state_changed. Its docs point to customer.state_changed for syncing active subscriptions and benefits, subscription.revoked for removing access (end-of-period cancellations revoke when the period ends), order.paid rather than order.created for payment, and caution with subscription.past_due because the customer can recover (webhook events).
  • Test webhooks with the CLI. polar webhook listen forwards events to localhost and polar webhook trigger sends 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

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
Featured on Dofollow.Tools AI Toolz Dir Claudify - Featured on Startup Fame