Spaces:
Sleeping
Sleeping
# Stripe | |
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! --> | |
This guide will walk through a minimal example of working with a Stripe | |
one-time payment link and webhook for secure reconciliation of payments. | |
To get started we can import the stripe library and authenticate with a | |
**Stripe API key** that you can get from the stripe web UI. | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
from fasthtml.common import * | |
import os | |
``` | |
</details> | |
## Stripe Authentication | |
You can install stripe python sdk directly from pypi: | |
``` sh | |
pip install stripe | |
``` | |
Additionally, you need to install the stripe cli. You can find how to | |
install it on your specific system in their docs | |
[here](https://docs.stripe.com/get-started/development-environment?lang=python#setup-cli) | |
``` python | |
# uncomment and execute if needed | |
#!pip install stripe | |
``` | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
import stripe | |
``` | |
</details> | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY") | |
DOMAIN_URL = os.environ.get("DOMAIN_URL", "http://localhost:5001") | |
``` | |
</details> | |
You can get this API key from the Stripe Dashboard by going to [this | |
url](https://dashboard.stripe.com/test/apikeys). | |
<div> | |
> **Note** | |
> | |
> Note: Make sure you have `Test mode` turned on in the dashboard. | |
</div> | |
 | |
Make sure you are using a test key for this tutorial | |
``` python | |
assert 'test_' in stripe.api_key | |
``` | |
## Pre-app setup | |
<div> | |
> **Tip** | |
> | |
> Everything in the pre-app setup sections is a run once and not to be | |
> included in your web-app. | |
</div> | |
### Create a product | |
You can run this to programatically create a Stripe **Product** with a | |
**Price**. Typically, this is not something you do dynamically in your | |
FastHTML app, but rather something you set up one time. You can also | |
optionally do this on the Stripe Dashboard UI. | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
def _search_app(app_nm:str, limit=1): | |
"Checks for product based on app_nm and returns the product if it exists" | |
return stripe.Product.search(query=f"name:'{app_nm}' AND active:'True'", limit=limit).data | |
def create_price(app_nm:str, amt:int, currency="usd") -> list[stripe.Price]: | |
"Create a product and bind it to a price object. If product already exist just return the price list." | |
existing_product = _search_app(app_nm) | |
if existing_product: | |
return stripe.Price.list(product=existing_product[0].id).data | |
else: | |
product = stripe.Product.create(name=f"{app_nm}") | |
return [stripe.Price.create(product=product.id, unit_amount=amt, currency=currency)] | |
def archive_price(app_nm:str): | |
"Archive a price - useful for cleanup if testing." | |
existing_products = _search_app(app_nm, limit=50) | |
for product in existing_products: | |
for price in stripe.Price.list(product=product.id).data: | |
stripe.Price.modify(price.id, active=False) | |
stripe.Product.modify(product.id, active=False) | |
``` | |
</details> | |
<div> | |
> **Tip** | |
> | |
> To do recurring payment, you would use | |
> `recurring={"interval": "year"}` or `recurring={"interval": "month"}` | |
> when creating your stripe price. | |
</div> | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
app_nm = "[FastHTML Docs] Demo Product" | |
price_list = create_price(app_nm, amt=1999) | |
assert len(price_list) == 1, 'For this tutorial, we only have one price bound to our product.' | |
price = price_list[0] | |
``` | |
</details> | |
``` python | |
print(f"Price ID = {price.id}") | |
``` | |
Price ID = price_1R1ZzcFrdmWPkpOp9M28ykjy | |
### Create a webook | |
A webhook is simply a URL where your app listens for messages from | |
Stripe. It provides a way for Stripe, the payment processor, to notify | |
your application when something happens with a payment. Think of it like | |
a delivery notification: when a customer completes a payment, Stripe | |
needs to tell your application so you can update your records, send | |
confirmation emails, or provide access to purchased content. It is | |
simply a URL, | |
But your app needs to be sure every webhook event is actually coming | |
from Stripe. That is, it needs to authenticate the notification. To do | |
that, your app will need a **webhook signing secret**, which it uses to | |
confirm that the notifications were signed by Stripe. | |
This secret is different from your Stripe API key. The Stripe API key | |
lets you prove who you are to Stripe. The webhook signing secret lets | |
you be sure messages from Stripe are coming from Stripe. | |
You will need a webhook signing secret whether your app is is running | |
locally in test mode, or whether it is a real production app on running | |
on a server. Here is how you get the webhook signing secret in these two | |
cases. | |
#### Local Webhook | |
When your application runs locally during development it can be reached | |
only from your computer, so Stripe can’t make an HTTP request against | |
the webhook. To workaround this in development, the Stripe CLI tool | |
creates a secure tunnel which forwards these webhook notifications from | |
Stripe’s servers to your local application. | |
Run this command to start that tunnel: | |
``` bash | |
stripe listen --forward-to http://localhost:5001/webhook | |
``` | |
On success, that command will also tell you the webhook signing secret. | |
Take the secret it gives you and set it as an environment variable. | |
``` bash | |
export STRIPE_LOCAL_TEST_WEBHOOK_SECRET=<your-secret> | |
``` | |
#### Production Webhook | |
For a deployed app, you configure a permanent webhook connection in your | |
Stripe Dashboard. This establishes an official notification channel | |
where Stripe will send real-time updates about payments to your | |
application’s `/webhook` URL. | |
On the dashboard, you can configure which specific payment event | |
notifications will go to this webhook (e.g., completed checkouts, | |
successful payments, failed payments, etc..). Your app provides the | |
webhook signing secret to the Stripe library, to authenticate that these | |
notifications come from the Stripe service. This is essential for | |
production environments where your app needs to automatically respond to | |
payment activities without manual intervention. | |
To configure the permanent webhook connection, you need to do the | |
following steps: | |
1. Make sure you are in Test mode like before | |
2. Go to https://dashboard.stripe.com/test/webhooks | |
3. Click “+ Add endpoint” to create create a new webhook (or, if that | |
is missing, click “Create an event destination”). | |
4. On the primary screen shown below, “Listen to Stripe events”, fill | |
out the details. Your Endpoint URL will be | |
`https://YOURDOMAIN/webhook` | |
5. Save your webhook signing scret. On the “Listen to Stripe events” | |
screen, you can find it in the app sample code on the right hand | |
side as the “endpoint secret”. You can also retrieve it later from | |
the dashboard. | |
 | |
You also need to configure which events should generate webhook | |
notifications: | |
1. Click “+ Select events” to open the secondary control screen, | |
“Select events to send”, which is shown below. In on our case we | |
want to listen for `checkout.session.completed`. | |
2. Click the “Add Events” button, to confirm which events to send. | |
 | |
<div> | |
> **Tip** | |
> | |
> For subscriptions you may also want to enable additional events for | |
> your webhook such as: `customer.subscription.created`, | |
> `customer.subscription.deleted`, and others based on your use-case. | |
> | |
>  | |
</div> | |
Finally, click “Add Endpoint”, to finish configuring the endpoint. | |
## App | |
<div> | |
> **Tip** | |
> | |
> Everything after this point is going to be included in your actual | |
> application. The application created in this tutorial can be found | |
> [here](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/explains/stripe_otp.py) | |
</div> | |
### Setup to have the right information | |
In order to accept a payment, you need to know who is making the | |
payment. | |
There are many ways to accomplish this, for example using | |
[oauth](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/explains/oauth.ipynb) | |
or a form. For this example we will start by hardcoding an email address | |
into a session to simulate what it would look like with oauth. | |
We save the email address into the session object, under the key `auth`. | |
By putting this logic into beforeware, which runs before every request | |
is processed, we ensure that every route handler will be able to read | |
that address from the session object. | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
def before(sess): sess['auth'] = '[email protected]' | |
bware = Beforeware(before, skip=['/webhook']) | |
app, rt = fast_app(before=bware) | |
``` | |
</details> | |
We will need our webhook secret that was created. For this tutorial, we | |
will be using the local development environment variable that was | |
created above. For your deployed production environment, you will need | |
to get the secret for your webhook from the Stripe Dashboard. | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
WEBHOOK_SECRET = os.getenv("STRIPE_LOCAL_TEST_WEBHOOK_SECRET") | |
``` | |
</details> | |
### Payment Setup | |
We need 2 things first: | |
1. A button for users to click to pay | |
2. A route that gives stripe the information it needs to process the | |
payment | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
@rt("/") | |
def home(sess): | |
auth = sess['auth'] | |
return Titled( | |
"Buy Now", | |
Div(H2("Demo Product - $19.99"), | |
P(f"Welcome, {auth}"), | |
Button("Buy Now", hx_post="/create-checkout-session", hx_swap="none"), | |
A("View Account", href="/account"))) | |
``` | |
</details> | |
We are only allowing card payments (`payment_method_types=['card']`). | |
For additional options see the [Stripe docs](https://docs.stripe.com/). | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
@rt("/create-checkout-session", methods=["POST"]) | |
async def create_checkout_session(sess): | |
checkout_session = stripe.checkout.Session.create( | |
line_items=[{'price': price.id, 'quantity': 1}], | |
mode='payment', | |
payment_method_types=['card'], | |
customer_email=sess['auth'], | |
metadata={'app_name': app_nm, | |
'AnyOther': 'Metadata',}, | |
# CHECKOUT_SESSION_ID is a special variable Stripe fills in for you | |
success_url=DOMAIN_URL + '/success?checkout_sid={CHECKOUT_SESSION_ID}', | |
cancel_url=DOMAIN_URL + '/cancel') | |
return Redirect(checkout_session.url) | |
``` | |
</details> | |
<div> | |
> **Tip** | |
> | |
> For subscriptions the mode would typically be `subscription` instead | |
> of `payment` | |
</div> | |
This section creates two key components: a simple webpage with a “Buy | |
Now” button, and a function that handles what happens when that button | |
is clicked. | |
When a customer clicks “Buy Now,” the app creates a Stripe checkout | |
session (essentially a payment page) with product details, price, and | |
customer information. Stripe then takes over the payment process, | |
showing the customer a secure payment form. After payment is completed | |
or canceled, Stripe redirects the customer back to your app using the | |
success or cancel URLs you specified. This approach keeps sensitive | |
payment details off your server, as Stripe handles the actual | |
transaction. | |
### Post-Payment Processing | |
After a customer initiates payment, there are two parallel processes: | |
1. **User Experience Flow**: The customer is redirected to Stripe’s | |
checkout page, completes payment, and is then redirected back to | |
your application (either the success or cancel page). | |
2. **Backend Processing Flow**: Stripe sends webhook notifications to | |
your server about payment events, allowing your application to | |
update records, provision access, or trigger other business logic. | |
This dual-track approach ensures both a smooth user experience and | |
reliable payment processing. | |
The webhook notification is critical as it’s a reliable way to confirm | |
payment completion. | |
#### Backend Processing Flow | |
Create a database schema with the information you’d like to store. | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
# Database Table | |
class Payment: | |
checkout_session_id: str # Stripe checkout session ID (primary key) | |
email: str | |
amount: int # Amount paid in cents | |
payment_status: str # paid, pending, failed | |
created_at: int # Unix timestamp | |
metadata: str # Additional payment metadata as JSON | |
``` | |
</details> | |
Connect to the database | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
db = Database("stripe_payments.db") | |
payments = db.create(Payment, pk='checkout_session_id', transform=True) | |
``` | |
</details> | |
In our webhook we can execute any business logic and database updating | |
we need to. | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
@rt("/webhook") | |
async def post(req): | |
payload = await req.body() | |
# Verify the event came from Stripe | |
try: | |
event = stripe.Webhook.construct_event( | |
payload, req.headers.get("stripe-signature"), WEBHOOK_SECRET) | |
except Exception as e: | |
print(f"Webhook error: {e}") | |
return | |
if event and event.type == "checkout.session.completed": | |
event_data = event.data.object | |
if event_data.metadata.get('app_name') == app_nm: | |
payment = Payment( | |
checkout_session_id=event_data.id, | |
email=event_data.customer_email, | |
amount=event_data.amount_total, | |
payment_status=event_data.payment_status, | |
created_at=event_data.created, | |
metadata=str(event_data.metadata)) | |
payments.insert(payment) | |
print(f"Payment recorded for user: {event_data.customer_email}") | |
# Do not worry about refunds yet, we will cover how to do this later in the tutorial | |
elif event and event.type == "charge.refunded": | |
event_data = event.data.object | |
payment_intent_id = event_data.payment_intent | |
sessions = stripe.checkout.Session.list(payment_intent=payment_intent_id) | |
if sessions and sessions.data: | |
checkout_sid = sessions.data[0].id | |
payments.update(Payment(checkout_session_id= checkout_sid, payment_status="refunded")) | |
print(f"Refund recorded for payment: {checkout_sid}") | |
``` | |
</details> | |
The webhook route is where Stripe sends automated notifications about | |
payment events. When a payment is completed, Stripe sends a secure | |
notification to this endpoint. The code verifies this notification is | |
legitimate using the webhook secret, then processes the event data - | |
extracting information like the customer’s email and payment status. | |
This allows your application to automatically update user accounts, | |
trigger fulfillment processes, or record transaction details without | |
manual intervention. | |
Note that in this route, our code extracts the user’s email address from | |
the Stripe event, *not from the session object*. That is the because | |
this route will be hit by a request from Stripe’s servers, not from the | |
user’s browser. | |
<div> | |
> **Tip** | |
> | |
> When doing a subscription, often you would add additional event types | |
> in an if statement to update your database appropriately with the | |
> subscription status. | |
> | |
> ``` python | |
> if event.type == "payment_intent.succeeded": | |
> ... | |
> elif event.type == "customer.subscription.created": | |
> ... | |
> elif event.type == "customer.subscription.deleted": | |
> ... | |
> ``` | |
</div> | |
#### User Experience Flow | |
The `/success` route is where Stripe will redirect the user *after* the | |
payment completes successfully, which will also be after Stripe has | |
called the webhook to inform your app of the transaction. | |
Stripe knows to send the user here, because you provided Stripe with | |
this route when you created a checkout session. | |
But you want to verify this is the case. So in this route, you should | |
verify the user’s payment status, by checking your database for the | |
entry which your app saved when it received that webhook notification. | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
@rt("/success") | |
def success(sess, checkout_sid:str): | |
# Get payment record from database (saved in the webhook) | |
payment = payments[checkout_sid] | |
if not payment or payment.payment_status != 'paid': | |
return Titled("Error", P("Payment not found")) | |
return Titled( | |
"Success", | |
Div(H2("Payment Successful!"), | |
P(f"Thank you for your purchase, {sess['auth']}"), | |
P(f"Amount Paid: ${payment.amount / 100:.2f}"), | |
P(f"Status: {payment.payment_status}"), | |
P(f"Transaction ID: {payment.checkout_session_id}"), | |
A("Back to Home", href="/"))) | |
``` | |
</details> | |
There is also a `/cancel` route, where Stripe will redirect the user if | |
they canceled the checkout. | |
<details open class="code-fold"> | |
<summary>Exported source</summary> | |
``` python | |
@rt("/cancel") | |
def cancel(): | |
return Titled( | |
"Cancelled", | |
Div(H2("Payment Cancelled"), | |
P("Your payment was cancelled."), | |
A("Back to Home", href="/"))) | |
``` | |
</details> | |
This image shows Stripe’s payment page that customers see after clicking | |
the “Buy Now” button. When your app redirects to the Stripe checkout | |
URL, Stripe displays this secure payment form where customers enter | |
their card details. For testing purposes, you can use Stripe’s test card | |
number (4242 4242 4242 4242) with any future expiration date and any | |
3-digit CVC code. This test card will successfully process payments in | |
test mode without charging real money. The form shows the product name | |
and price that were configured in your Stripe session, providing a | |
seamless transition from your app to the payment processor and back | |
again after completion. | |
 | |
Once you have processed the payments you can see each record in the | |
sqlite database that was stored in the webhook. | |
Next, we can see how to add the refund route | |
In order to use a refund capability we need an account management page | |
where users can request refunds for their payments. | |
When you initiate a refund, you can see the status of the refund in your | |
Stripe dasbhoard at | |
[`https://dashboard.stripe.com/payments`](https://dashboard.stripe.com/payments), | |
or | |
[`https://dashboard.stripe.com/test/payments`](https://dashboard.stripe.com/test/payments) | |
if you are in `Test mode` | |
It will look like this with a `Refunded icon`: | |
 | |
# Recap | |
In this tutorial, we learned how to implement and test a complete Stripe | |
payment flow including: | |
1. Creating test products and prices | |
2. Setting up a payment page and checkout session | |
3. Webhook handling for secure payment verification | |
4. Building success/cancel pages for the user experience | |
5. Adding refund functionality | |
6. Creating an account management page to view payment history | |
When migrating this payment system to production, you’ll need to create | |
actual products, prices and webhooks in your Stripe dashboard rather | |
than test ones. You’ll also need to replace your test API keys with live | |
Stripe API keys. | |