Skip to main content

Stripe Checkout

James looked at the upgrade prompt in WhatsApp. "Upgrade to unlock all chapters." Below it: a placeholder URL that went nowhere.

"The tier gating works," he said. "Free learners get blocked at chapter six. The upgrade message appears. But the link is a dead end."

Emma pulled up the Stripe dashboard on her screen. "Stripe. You create a checkout page on their servers. They handle the card, the receipt, the PCI compliance. You get a webhook when the payment clears."

James thought for a moment. "Like a cash register. I ring up the item, the point-of-sale terminal handles the card, and the receipt updates the inventory."

"Exactly. Your server creates the session. Stripe handles the money. Their webhook tells your server the payment went through. You update the JSON. Three parties, one flow."


You are doing exactly what James is doing. Your tier gating works from Lesson 13, but the upgrade link is still a mock from Lesson 6. Time to replace it with a real Stripe Checkout session and a webhook that upgrades the tier when payment completes.

Three steps. First, you set up Stripe yourself (this is product work, not code). Second, you describe the integration to Claude Code. Third, you verify the full flow end-to-end with a test card payment.

Set Up Stripe (You, Not Claude Code)

Stripe account setup is a business decision. You choose the product name, the price, the currency. Claude Code does not make these decisions for you.

Step 1: Create a Stripe Account

Go to dashboard.stripe.com/register and create a free account. No credit card required for test mode.

Step 2: Enable Test Mode

In the Stripe Dashboard, toggle Test mode in the top-right corner. Every action you take in test mode uses fake money and fake cards. No real charges.

Step 3: Create a Product

Navigate to Product catalog and create a new product:

FieldValue
NameTutorClaw Paid Tier
Price9.99 (or whatever you choose)
Payment typeOne-time
CurrencyYour local currency

After saving, find the Price ID on the product page. It starts with price_. Copy it.

Step 4: Get Your API Keys

Navigate to Developers then API keys. You need two keys:

KeyStarts WithPurpose
Secret keysk_test_Your server uses this to create Checkout sessions
Publishable keypk_test_Not needed for this lesson (used for client-side Stripe.js)

Copy the secret key. You will add it to your environment variables.

Step 5: Install the Stripe CLI

The Stripe CLI forwards webhooks to your local server during development. Install it following the instructions at docs.stripe.com/stripe-cli.

After installing, authenticate with your Stripe account:

stripe login

Follow the browser prompt to link the CLI to your test-mode account.

Step 6: Set Environment Variables

Your TutorClaw server needs three environment variables. Add them to a .env file in your project root (or set them in your terminal session):

VariableValueSource
STRIPE_SECRET_KEYYour sk_test_ keyStripe Dashboard, API keys page
STRIPE_PRICE_ID_PAIDYour price_ IDStripe Dashboard, product page
STRIPE_WEBHOOK_SECRETSet this after Step 7 belowStripe CLI output
Never Commit Secrets

Add .env to your .gitignore if it is not already there. The sk_test_ key gives full access to your Stripe test account. Committing it to git is a security mistake even in test mode, because the habit carries over to production keys.

Describe the Integration to Claude Code

With Stripe set up, the coding work is a describe-steer-verify cycle. You tell Claude Code what changed about get_upgrade_url, and you describe a new webhook endpoint.

Describe the Updated get_upgrade_url

Open Claude Code in your tutorclaw-mcp project and describe the change:

Update get_upgrade_url to create a real Stripe Checkout session
instead of returning a mock URL. Use the stripe Python library.

The tool takes a learner_id. It reads the learner's tier from JSON
state. If the learner is already paid, return an error.

If the learner is on the free tier, create a Stripe Checkout session
with the price ID from the STRIPE_PRICE_ID_PAID environment variable.
Include the learner_id in the session metadata so the webhook handler
knows which learner paid. Return the checkout session URL.

The Stripe secret key comes from the STRIPE_SECRET_KEY environment
variable. Never hardcode keys in the source code.

Notice what you described: the behavior (create a checkout session), the data flow (learner_id in metadata), the security constraint (keys from environment variables), and the scope (replace the mock, keep everything else). You did not describe the Stripe API syntax. Claude Code handles that.

Review and Steer

When Claude Code returns the spec, check:

CheckWhat to Look For
Stripe libraryUses the stripe Python package (not raw HTTP calls)
Environment variablesReads keys from environment, not hardcoded strings
MetadataIncludes learner_id in the checkout session metadata
Success and cancel URLsThe session has both (Stripe requires them). Localhost URLs are fine for testing.
Error handlingReturns a clear error if the learner is already paid or if the Stripe API call fails

If anything looks off, steer it:

Make sure the learner_id is stored in the session metadata, not
just the client_reference_id. The webhook handler will read it from
metadata to find the learner in JSON state.

Once the spec looks right, approve the build.

Describe the Webhook Handler

The webhook is a separate endpoint on your server. When Stripe processes a payment, it sends an HTTP POST to this endpoint with the event details. Describe it to Claude Code:

Add a webhook endpoint at /webhook that handles Stripe events.

When a checkout.session.completed event arrives:
1. Verify the webhook signature using the STRIPE_WEBHOOK_SECRET
environment variable
2. Extract the learner_id from the session metadata
3. Read the learner's record from JSON state
4. Change the learner's tier from "free" to "paid"
5. Save the updated JSON state

Return a 200 status for valid events. Return a 400 for invalid
signatures. Ignore event types other than checkout.session.completed.

Review the Webhook Spec

Check:

CheckWhat to Look For
Signature verificationUses the Stripe library's webhook signature verification, not a custom check
Event filteringOnly processes checkout.session.completed, ignores other events
learner_id extractionReads from session metadata (matching what get_upgrade_url stores)
JSON updateReads the current file, updates the tier, writes it back
Response codes200 for success, 400 for bad signature

Approve the build when the spec looks right.

Test the Full Flow

Testing a Stripe integration locally requires the Stripe CLI to forward webhook events to your server. This is the standard local development workflow for any webhook-based integration.

Step 7: Start the Stripe CLI Listener

In a separate terminal, start the webhook forwarding:

stripe listen --forward-to localhost:8000/webhook

The CLI prints a webhook signing secret that starts with whsec_. Copy this value and set it as your STRIPE_WEBHOOK_SECRET environment variable. This secret is different from your API key. The API key authenticates your server's requests to Stripe. The webhook secret verifies that incoming webhook requests actually came from Stripe.

Step 8: Start Your TutorClaw Server

In another terminal, start the server with the environment variables loaded:

uv run tutorclaw

Confirm the server starts without errors.

Step 9: Call get_upgrade_url

Use Claude Code or a direct HTTP call to invoke get_upgrade_url for a free-tier learner. The tool should return a URL that starts with https://checkout.stripe.com/.

Open that URL in your browser. You should see a Stripe Checkout page with your product name and price.

Step 10: Pay With the Test Card

Stripe provides test card numbers that simulate successful payments. Enter:

FieldValue
Card number4242 4242 4242 4242
ExpiryAny future date
CVCAny 3 digits
NameAny name

Click Pay. Stripe processes the test payment instantly.

Step 11: Watch the Webhook

Switch to the terminal running stripe listen. You should see output showing the events Stripe sent, including checkout.session.completed.

The Stripe CLI forwarded that event to your server's /webhook endpoint. Your handler verified the signature, extracted the learner_id from the metadata, and updated the JSON state.

Step 12: Verify the Tier Change

Open your learner's JSON state file (in the data/ directory). The learner's tier should now be "paid" instead of "free".

This is the complete monetization flow: learner hits a paywall, gets a checkout URL, pays with a card, webhook fires, tier upgrades, content unlocks. One payment, one webhook, one JSON update.

Verify Existing Tests Still Pass

Run the test suite to make sure nothing broke:

uv run pytest

The existing tests should still pass. The mock behavior for already-paid learners is the same (return an error). The difference is that free-tier learners now get a real Stripe URL instead of a placeholder.

Two Secrets, Two Directions

If you get confused about which key is which, remember the direction of traffic. The API secret key (sk_test_) authenticates requests going OUT from your server to Stripe. The webhook signing secret (whsec_) verifies requests coming IN from Stripe to your server. Outbound: API key. Inbound: webhook secret.

The Three-Party Flow

Step back and look at what you built. Three parties are involved in every payment:

PartyRole
Your serverCreates the checkout session, handles the webhook, updates the tier
StripeHosts the payment page, processes the card, sends the webhook
The learnerClicks the link, enters card details, completes payment

Your server never sees a credit card number. Stripe's hosted Checkout page handles all of that. Your server only needs to create the session (outbound API call) and handle the result (inbound webhook).

The webhook is the critical piece. The checkout URL alone does not confirm payment: the learner might close the browser, the redirect might fail, network errors happen. The webhook is Stripe's server-to-server confirmation that money moved. Your handler trusts it (after verifying the signature) and updates the tier.

Try With AI

Exercise 1: Test a Failed Payment

Stripe provides test card numbers that simulate failures. Ask Claude Code about them:

What Stripe test card numbers simulate a declined payment? I want to
test what happens when a learner's card is declined during checkout.
What event does Stripe send in that case, and should my webhook
handler do anything with it?

What you are learning: A payment integration is not complete until you know how failures behave. Successful payments are the happy path. Declined cards, expired cards, and insufficient funds are the realistic path.

Exercise 2: Inspect the Webhook Payload

Ask Claude Code to add logging so you can see what Stripe sends:

Add logging to the webhook handler that prints the full event type
and the session metadata when a checkout.session.completed event
arrives. I want to see exactly what Stripe sends so I can verify the
learner_id is in the metadata.

Run another test payment after adding the logging. Check the server output.

What you are learning: Webhook payloads are your debugging tool for payment integrations. When something goes wrong (tier not updating, wrong learner upgraded), the payload tells you exactly what Stripe sent and what your handler processed.

Exercise 3: Handle Double Payments

What happens if Stripe sends the same webhook event twice? (This can happen during network retries.) Ask Claude Code:

What happens if the webhook handler receives a checkout.session.completed
event for a learner who is already on the paid tier? Is the current
handler idempotent? If not, what should it do differently?

What you are learning: Webhook handlers must be idempotent: processing the same event twice should produce the same result as processing it once. Stripe guarantees at-least-once delivery, not exactly-once. Your handler must handle duplicates gracefully.


James watched the terminal. The stripe listen output showed checkout.session.completed. He opened the JSON file. The tier field read "paid".

"One payment. One webhook. One JSON update." He closed the file. "The product makes money."

Emma nodded. "That is the entire monetization flow. A learner hits the paywall, gets a URL, pays, and unlocks everything. Three parties, three steps, done."

"It felt simpler than I expected."

"Payment integration is simple when you let the payment provider handle the hard part. Card validation, receipt generation, PCI compliance, refund handling: all Stripe's problem." She paused. "I once deployed a Stripe integration with test keys still set in production. Eight hours before anyone noticed. No real charges were processed, but the embarrassment was real. The fix was one environment variable, but the incident review was four hours long."

"How do you prevent that?"

"Environment variable checklist before every deploy. And a staging environment that uses test keys deliberately, so production is the only place live keys exist." She pointed at his screen. "Your tier changes. Your tests pass. Next lesson: test the full payment flow end-to-end from WhatsApp. A free learner hits the paywall, pays, and immediately gets access to paid content without restarting anything."