Skip to main content

How to Build Stripe Payments in Flask & Record Sales in QuickBooks?

How to Build Stripe Payments in Flask & Record Sales in QuickBooks?

Accepting online payments is only half the job. A complete booking system should also record every successful payment in my accounting software without requiring manual data entry. In this tutorial, you'll learn how to integrate Stripe with a Flask application to securely accept online payments and automatically create sales receipts in QuickBooks Online. By the end, the Jersey City Pickleball Club booking system will process payments, keep financial records in sync, and provide a solid foundation for generating receipts and reports, as well as future business automation.


Prerequisite:

This tutorial is part of the Flask Landing Page and Reservation System Series.


📚 View the Complete Flask Landing Page and Reservation System Series

Previous Part                                                                                                                    ➡ Next Part

Preliminary:
Before I begin, please activate the virtual environment and install the required dependencies.
python -m venv venv
venv\Scripts\activate
pip install flask python-quickbooks intuit-oauth python-dotenv stripe
Then, we need to set up the file and folder structure, as below:
Files and folders

In this tutorial, I will focus on connecting the Stripe and QuickBooks API services. I have created a few files, including account.py, payments.py, quickbooks_oath.py, test_quickbooks.py, and a minor amendment to app.py.

What are Stripe and QuickBooks?
Stripe is a global technology company that provides payment processing software
QuickBooks is an accounting and financial management software developed by Intuit.

Why did I connect both APIs?

When a user books a court, it will link the booking to the payment in Stripe and record a sales receipt in QuickBooks concurrently. Therefore, the club will gain the following benefits:
  1. Improve cash flow by accepting secure online payments before the booking is confirmed.
  2. Reduce the risk of cash loss or theft, as all payments are processed electronically instead of handling cash at the venue.
  3. Lower operating costs by reducing the need for ticket-selling staff and manual data entry into the accounting system.
  4. Save time through automation, since payment information is automatically transferred to QuickBooks without re-entering data.
  5. Minimise human errors, such as incorrect payment amounts, duplicate entries, or missing transactions.
  6. Maintain accurate financial records, with every successful payment immediately recorded as a sales receipt.
  7. Simplify bookkeeping and reconciliation, making it easier to match bank deposits, Stripe payouts, and accounting records.
  8. Generate real-time financial reports, allowing managers to monitor revenue, sales trends, and cash flow at any time.
  9. Enhance the customer experience, as players receive instant payment confirmation and receipts after completing their booking.
  10. Provide a complete audit trail, linking each booking, Stripe payment, and QuickBooks sales receipt for easy tracking and dispute resolution.

From the customers' perspective, the online booking system offers greater convenience and flexibility. Players can reserve a court anytime and anywhere without travelling to the club. They avoid traffic congestion, parking difficulties, and long queues, while enjoying instant booking confirmation and secure online payment. As a result, the entire booking process becomes quicker, simpler, and more user-friendly.


Step 1: Retrieve the Stripe API and insert it into the .env file

Retrieve the Stripe API
First of all, log in to the Stripe dashboard. Then, in the top left corner of my account, 'Kelvin Loh. Click; there is a dropdown 'switch to sandbox,' and I click test mode. In the API keys section at the bottom right box, click 'Go To API Keys.' When the new page loads, copy the STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY as shown in the diagram above.

Paste them into the .env file
STRIPE_SECRET_KEY = 'paste here'
STRIPE_PUBLISHABLE_KEY= 'paste here'

Step 2: Create the Stripe checkout function
When the user clicks the payment button, it will redirect to the Stripe checkout page as above.

extracted app.py
from payments import payment_bp
app.register_blueprint(payment_bp)

dotenv.load_dotenv()

app.config['SECRET_KEY'] = 'your-secret-key'
app.config["STRIPE_SECRET_KEY"] = os.getenv("STRIPE_SECRET_KEY")
Since I have declared the Stripe API key in the .env file above, now I will bring it to app.py. This API is confidential and should not be accessed by the public. Therefore, I keep it separately.

I also made the file modular, so 'payment.py' will contain the logic for Stripe and register it in 'app.py' as a payment_pb. It will facilitate convenience when the code scales up in the future. 

The underlying logic
Create the Stripe checkout function logic

payments.py
from flask import (request, redirect, url_for, Blueprint, flash, 
current_app, render_template)
from flask_login import login_required, current_user 
import stripe

payment_bp = Blueprint("payment", __name__)

@payment_bp.route("/create-checkout-session", methods=["POST"])
@login_required
def create_checkout_session():
    stripe.api_key = current_app.config["STRIPE_SECRET_KEY"]
    
    court = request.form["booking_court_selection"]
    booking_date = request.form["booking_date"]
    time_slot = request.form["booking_time_slot"]

    checkout_session = stripe.checkout.Session.create(
        mode="payment",
        payment_method_types=["card"],

        line_items=[{
            "price_data": {
                "currency": "usd",
                "product_data": {
                    "name": f"{court} Court Booking"
                },
                "unit_amount": 2000
            },
            "quantity": 1
        }],
        
         metadata={

           "username": current_user.username,
           "court": court,
           "date": booking_date,
           "time_slot": time_slot
        },

        success_url=url_for(
            "payment.payment_success",
            _external=True
        ) + "?session_id={CHECKOUT_SESSION_ID}",

        cancel_url=url_for(
            "payment.payment_cancel",
            _external=True
        )
    )
    return redirect(checkout_session.url)
1. Route Definition
  • @payment_bp.route(...) registers the URL /create-checkout-session.
  • methods=["POST"] means this route only accepts POST requests, preventing users from accidentally creating a payment by visiting the URL directly.
  • @login_required ensures that only authenticated users can make a payment.
2.  Set the Stripe Secret Key

  • Stripe requires my secret API key to authenticate every request.
  • Rather than hard-coding it, the key is retrieved from my Flask configuration.
3. Retrieve Booking Information

  • These values are submitted from my booking form.
  • They will be used to create the payment description and stored with the payment.
4. Create a Checkout Session

  • This tells Stripe to create a brand-new checkout session.
  • Think of it as creating a temporary online checkout page for this specific booking.
Stripe Checkout
5. Payment Mode

  • Stripe supports several modes:
    • "payment" – one-time payment ✅
    • "subscription" – recurring payments
    • "setup" – save a payment method without charging
  • Since a court booking is paid once, it "payment" is the correct choice.
6. Accepted Payment Methods

  • Only card payments are accepted.
  • Stripe can also support additional payment methods depending on my account and region
7. Define the Product

  • A Checkout Session can contain one or more products.
  • Here I am charging for one court booking.
  1. Currency - The payment is charged in US dollars.
  2. Product Name - The booking description is generated dynamically.
  3. Price - Stripe expects the smallest currency unit.
  4. Quantity - Only one court reservation is being purchased.
8. Store Metadata

  • Metadata is one of Stripe's most useful features.
  • These values are not shown to the customer, but they are stored with the payment.
  • Later, when Stripe sends a webhook after a successful payment, my application can retrieve this metadata to

    1. create the booking in TinyDB,
    2. create a Sales Receipt in QuickBooks Online,
    3. send a confirmation email,
    4. generate a receipt.

  • This avoids needing hidden form fields or temporary server-side storage.
9. Success URL

  • After a successful payment, Stripe redirects the customer back to my application.
  • The placeholder {CHECKOUT_SESSION_ID} is automatically replaced by Stripe with the actual session ID.
  • I can then retrieve the session to verify the payment and display payment details.
10. Cancel URL

  • If the customer cancels the payment, Stripe redirects them here instead.
The underlying logic

payment_success_logic

payments.py
@payment_bp.route("/payment-success")
@login_required
def payment_success():
    stripe.api_key = current_app.config["STRIPE_SECRET_KEY"]
    session_id = request.args.get("session_id")
    session = stripe.checkout.Session.retrieve(session_id)

    if session.payment_status == "paid":

        bookings_table.insert({
            "username":  session.metadata["username"],
            "court": session.metadata["court"],
            "date": session.metadata["date"],
            "time_slot": session.metadata["time_slot"],
            "status": "Accepted",
            "payment": "Paid",
            "stripe_session": session.id
        })

        flash("Payment successful! Your booking is being confirmed.")
    return redirect(url_for("dashboard"))
1. Route Definition

  • This route handles requests to /payment-success.
  • The @login_required decorator ensures that only authenticated users can access this page. Since bookings belong to registered users, this prevents anonymous users from confirming bookings.
2. Configure the Stripe API
  • Before communicating with Stripe, my application authenticates itself using the secret API key stored in the Flask configuration.
3. Retrieve the Checkout Session ID

  • After a successful payment, Stripe redirects the browser to a URL similar to:

               '/payment-success?session_id=cs_test_a1B2C3D4...'

  • This line extracts the session_id from the URL.
Stripe transactions page that shows payment succeeded
Stripe dashboard
4. Retrieve the Checkout Session

  • Rather than trusting information sent by the browser, my application asks Stripe for the official Checkout Session.
  • This session contains trusted information such as:

    • Payment status
    • Customer information
    • Metadata
    • Amount paid
    • Currency
    • Session ID
  • Using Stripe as the source of truth helps prevent users from tampering with payment details.
5. Verify the Payment

  • My application checks whether Stripe confirms the payment was successful.
  • If the payment is not marked as "paid", no booking is created.
  • This prevents unpaid reservations from being stored
6. Save the Booking

  • Once payment has been verified, the booking is inserted into my TinyDB database.
  1. Username - Instead of reading the username from the submitted form, I retrieve it from the metadata stored by Stripe.
  2. Court - The selected court is restored from the Checkout Session.
  3. Date - The booking date is retrieved from Stripe.
  4. Time Slot - This restores the reserved playing time.
  5. Booking Status - Since payment has been completed successfully, the booking is immediately marked as Accepted.
  6. Payment Status - This records that payment has already been received.
  7. Stripe Session ID - Saving the Stripe Session ID is very useful.

  • It allows you to:
    • look up the payment later,
    • process refunds,
    • retrieve receipts,
    • investigate disputes,
  • link the booking to QuickBooks sales receipts. 
7. Notify the User
  • A flash message is stored in the user's session.
8. Redirect to the Dashboard
  • Finally, the user is redirected to the dashboard where they can view their confirmed booking.
The underlying logic
Payment cancel logic

payments.py
@payment_bp.route("/payment-cancel")
@login_required
def payment_cancel():
    stripe.api_key = current_app.config["STRIPE_SECRET_KEY"]
    flash("Payment cancelled. Your booking was not completed.")
    return redirect(url_for("search"))

1. Route Definition
  • This route is executed when Stripe redirects the customer to the cancel URL.
  • If the customer clicks Back, Cancel, or closes the checkout before paying, Stripe redirects them to this route.
2. Configure the Stripe API

  • This sets my Stripe secret key.
  • In this particular function, however, it isn't actually used because no request is made to Stripe.
3. Display a Flash Message
  • A flash message is stored in the user's session.
  • This reassures the user that:

    • no booking has been confirmed,
    • no payment has been processed.
    • they are free to try again.
4. Redirect Back to the Booking Page

  • Finally, the user is redirected back to the search page.
  • From there, they can:

    • choose another court,
    • select a different time slot,
    • restart the payment process.

Step 3: Install and configure stripe.exe and create a webhook
What is a webhook?
A webhook is an automatic push of data from Stripe to my Flask app the moment a specific event occurs (payment success).

Where to install the stripe.exe?
Go to https://github.com/stripe/stripe-cli/releases?utm_source=chatgpt.com and choose the right OS to install stripe.exe. Since I use Windows, I will download 'stripe_1.44.0_windows_x86_64.zip' to my local computer, extract the zip file, and move it to my jcpc-content folder.

downloap stripe.exe

How to retrieve the STRIPE_WEBHOOK_SECRET?
Once it is in my folder, I need to test it and log in to it. The following CML:
& "D:\Learning App\Website\JCPC-content\stripe.exe" --version
& "D:\Learning App\Website\JCPC-content\stripe.exe" login
& "D:\Learning App\Website\JCPC-content\stripe.exe" listen --forward-to localhost:5000/stripe-webhook
Then it will pop up a window; log in accordingly and click the Allow Access button
Stripe access page

Reenter the Stripe listener, and it will display the STRIPE_WEBHOOK_SECRET as follows:
Next, paste the password into the .env file
STRIPE_WEBHOOK_SECRET=paste it here
extracted app.py
import dotenv
import os
dotenv.load_dotenv()
app.config["STRIPE_WEBHOOK_SECRET"] = os.getenv("STRIPE_WEBHOOK_SECRET")
Now, the Stripe webhook secret is brought to the app and later passed to payment.py for further configuration.
The underlying logic
Stripe webhook logic

payment.py
@payment_bp.route("/stripe-webhook", methods=["POST"])
def stripe_webhook():

    payload = request.data
    sig_header = request.headers.get("Stripe-Signature")

    endpoint_secret = current_app.config["STRIPE_WEBHOOK_SECRET"]

    try:
        event = stripe.Webhook.construct_event(
            payload,
            sig_header,
            endpoint_secret
        )

    except Exception as e:
        return str(e), 400

    if event["type"] == "checkout.session.completed":

        session = event["data"]["object"]

        save_booking(session)
        
        create_sales_receipt(
            customer_name=
                session["metadata"]["username"],

            court=
                session["metadata"]["court"],

            amount=20
        )
    return "", 200
1. Define the Webhook Route
  • This creates an endpoint called /stripe-webhook.

  • Unlike normal routes, users never visit this URL. Instead, Stripe sends an HTTP POST request to this endpoint whenever one of the subscribed events occurs.
2. Read the Webhook Payload
  • When Stripe sends a webhook, it includes:
    • payload – the JSON data describing the event.
    • Stripe-Signature – a digital signature proving that the request came from Stripe.
3. Retrieve the Webhook Secret

  • It is generated by Stripe and stored in my Flask configuration.
  • Its purpose is to verify that the webhook is genuine and has not been altered.
4. Verify the Event

  • This is one of the most important lines in the entire Stripe integration.
  • Stripe performs three checks:

    • Is the request really from Stripe?
    • Has the payload been modified?
    • Does the signature match my webhook secret?
  • If all checks pass, Stripe returns a verified event object.
5. Handle Invalid Requests
  • If verification fails:
    • the request is rejected,
    • my application returns HTTP 400 (Bad Request),
    • no booking is created,
    • no Sales Receipt is generated.
  • This protects my application from fake webhook requests.
6. Check the Event Type
  • Stripe supports many different events, including:
    • checkout.session.completed
    • payment_intent.succeeded
    • invoice.paid
    • charge.refunded
  • My application only reacts when a Checkout Session has been completed successfully.

7. Retrieve the Checkout Session
  • The event contains many pieces of information.
  • It contains:
    • session ID
    • payment status
    • amount paid
    • customer details
    • metadata 
8. Save the Booking 
  • This calls my helper function.
  • Because the metadata was attached when the Checkout Session was created, all the booking information is available without asking the customer to submit it again.
9. Create the QuickBooks Sales Receipt (account.py)

  • After saving the booking, your application immediately records the transaction in QuickBooks Online.
  • It sends:

    • customer name
    • court booked
    • amount charged
  • to the QuickBooks API, which creates a Sales Receipt automatically.
  • This keeps my booking system and accounting records synchronised
10. Return HTTP 200
  • If my server instead returned an error such as 500, Stripe would retry sending the webhook several times according to its retry policy.
The underlying logic
Save booking logic
payment.py (helper function)
def save_booking(session):
    existing_booking = bookings_table.search(
        Query().stripe_session == session["id"]
    )

    if existing_booking:
        print("Booking already exists")
        return

    bookings_table.insert({
        "username": session["metadata"]["username"],
        "court": session["metadata"]["court"],
        "date": session["metadata"]["date"],
        "time_slot": session["metadata"]["time_slot"],
        "payment": "Paid",
        "status": "Accepted",
        "stripe_session": session["id"]
    })

1. Function Definition

  • The function receives session as an argument.
  • This session is the Stripe Checkout Session object from my webhook

2. Check Whether Booking Already Exists

  • This searches my TinyDB table for an existing booking with the same Stripe Session ID.
  • Breaking it down:
  • bookings_table.search()
  • TinyDB searches the database and returns matching records.
      1. Query().stripe_session

  • This tells TinyDB:

    • Look at the stripe_session field.
      2. == session["id"]
  • Compare it with the current Stripe Checkout Session ID.
  • The duplicate check prevents this.
3. If Booking Exists, Stop

  • If TinyDB finds a matching record: Booking already exists
  • Stops execution: No new booking is inserted.

4. Insert New Booking
  • If no existing booking is found, the code continues: A new record is added to TinyDB.

  • Save Username: Retrieves the username stored in Stripe metadata.
  • Save Court
  • Save Date
  • Save Time Slot
  • Save Payment Status
    • Since this function is called only after the payment has already been confirmed by Stripe.

  • Save Booking Status: The booking is officially confirmed.
  • Store Stripe Reference: This stores the Stripe Checkout Session ID. Later you can use this ID to:
    • retrieve payment details,
    • issue refunds,
    • display receipts,
    • troubleshoot payment problems.
---------------------------------------------------------------------------------------------------------------------------------------------------

🎁 Get Your FREE Flask Cheat Sheet

Get Your FREE Flask Cheat Sheet

Get more Flask, Python automation, Docker, and HTMX tutorials delivered to your inbox.

✓ Practical coding tutorials
✓ Automation tips for SMEs
✓ New project ideas and templates

Download my FREE Flask Cheat Sheet (PDF)


---------------------------------------------------------------------------------------------------------------------------------------------------


Step 4: Connect to QuickBooks API
Go to https://developer.intuit.com/ and log in to the dashboard.
QuickBooks API

First of all, I go to the top right corner to select MyHub. Then, in the dropdown selection, I will select Workspace. Next, I will click the sample workspace box and click the new workspace. Then it will direct me to the Create a New App page. I will name the application "Jersey City Pickleball Club." Then, it authorises adding permissions for both accounting and payment, and I need to confirm the authorisation. Once all is done, it will provide me both the Client ID and client secret. So, I need to copy both keys and paste them into the .env file.

I also set the redirect URL as follows:
QuickBooks redirect

Back to the dashboard, I click the "Get Production Key" box, then toggle to the development tab, and click the link in the redirect URL. Finally, delete the provided URL replace with the new URL "http://localhost:5000/quickbooks-callback."

extracted .env file 
QB_CLIENT_ID='paste it here'
QB_CLIENT_SECRET='paste it here'
QB_REDIRECT_URI=http://localhost:5000/quickbooks-callback
The steps are the same as above; I need to bring them to app.py and later on pass them to quickbooks_oauth.py.
The underlying logic
QuickBooks Oath

quickbooks_oath.py
from flask import redirect, current_app, request, Blueprint
from urllib.parse import urlencode
import requests
from requests.auth import HTTPBasicAuth

quickbooks_bp = Blueprint("quickbooks", __name__)

@quickbooks_bp.route("/quickbooks-login")
def quickbooks_login():
    params = {
        "client_id": current_app.config["QB_CLIENT_ID"],
        "scope": "com.intuit.quickbooks.accounting",
        "redirect_uri": current_app.config["QB_REDIRECT_URI"],
        "response_type": "code",
        "state": "test123"
    }

    url = (
        "https://appcenter.intuit.com/connect/oauth2?"
        + urlencode(params)
    )

    return redirect(url)

 1. Define the Route
  • This creates a Flask route, and I visit  'http://localhost:5000/quickbooks-login'
  • The route name "login" can be slightly confusing. It does not log a user into my Flask application. Instead, it means "Start the QuickBooks authorisation login process."
2. Build OAuth Parameters
This dictionary contains the information that Intuit requires before it can authorise my application.
  • The Client ID identifies my application. My Flask application tells QuickBooks: "I am this registered application."
  • Scope: The scope defines what my application is allowed to access. means my application requests permission to access accounting features, such as:
    • Customers
    • Items
    • Accounts
    • Sales Receipts
    • Invoices
    • Payments
        For my project, this permission is required because you want to create Sales Receipts             after Stripe payments.
  • Redirect URI: This tells QuickBooks: "After the user approves access, send the user back to this URL."

            This must exactly match the redirect URI registered in my Intuit Developer Dashboard.

  • Response Type: This tells OAuth that you want an authorisation code. The authorisation code is temporary and cannot be used to directly call the API. 
  • State Parameter: The state parameter is used for security.

    It helps prevent CSRF attacks by allowing my application to verify that the callback request came from the authorisation process you started.

3. Build the Authorisation URL
  • This creates the final URL that the browser will visit. urlencode() converts my dictionary: "client_id=abc123&scope=com.intuit.quickbooks.accounting"

 Underlying logic

QuickBooks callback logic

@quickbooks_bp.route("/quickbooks-callback")
def quickbooks_callback():
    code = request.args.get("code")
    realm_id = request.args.get("realmId")

    response = requests.post(
        "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",

        auth=HTTPBasicAuth(
            current_app.config["QB_CLIENT_ID"],
            current_app.config["QB_CLIENT_SECRET"]
        ),

        data={
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri":
                current_app.config["QB_REDIRECT_URI"]
        },

        headers={
            "Accept": "application/json"
        }
    )
    tokens = response.json()

    print(tokens)
    print("Realm ID:", realm_id)
    return "QuickBooks Connected"
1. Define the Callback Route 
  • This route handles the callback from QuickBooks after the user clicks "Allow" on the authorisation page.
  • Once permission is granted, QuickBooks redirects the browser to this URL.

2. Retrieve the Authorisation Code
  • QuickBooks sends an authorisation code as part of the URL.
  • The authorisation code is temporary and can only be used once.
3. Retrieve the Realm ID
  • The Realm ID uniquely identifies the QuickBooks Online company that the user authorised.
  • My application needs this value whenever it sends requests to the QuickBooks Accounting API.
4. Exchange the Authorisation Code

  • Instead of using the authorisation code directly, my application sends it to Intuit's OAuth server.
  • In return, Intuit provides:

    • Access Token
    • Refresh Token
    • Token expiry information
5. Authenticate My Application

  • Here, my application proves its identity.
  • It sends:

    • Client ID
    • Client Secret
  • These values come from my Intuit Developer account.
  • This tells Intuit: "I am the application that requested this authorisation code."

6. Send the Request Data
  • The request body contains three important pieces of information.
  1. Grant Type—This tells Intuit: "I am exchanging an authorisation code for OAuth tokens."
  2. Authorisation Code—This is the code you extracted from the callback URL.
  3. Redirect URI—This must exactly match:
    • the redirect URI registered in Intuit,
    • the redirect URI used during the login step.

    This prevents another application from stealing my authorisation code.

7. Request JSON
  • This tells Intuit that my application expects the response in JSON format.
8. Convert the Response
  • If everything succeeds, Intuit returns something, and these values are converted into a Python dictionary. 
9. Display the Tokens
  • During development, this prints the token information in the terminal.
  • In a production application, you should not print or expose these tokens because they grant access to my QuickBooks account.

  • Instead, store them securely (for example, in a database or encrypted configuration) and refresh them when the access token expires.

10. Display the Realm ID
  • This prints the QuickBooks company identifier.
  • You will use this value when calling the QuickBooks API.
11. Return a Confirmation
  • If everything succeeds, the browser displays: QuickBooks Connected
  • This confirms that the OAuth process has completed successfully.

Step 5: Verify the callback
The process of callback
Verify callback

The result of the callback
Result of callback
  • Copy the refresh_token, access_token and Realm ID into the .env file
extracted .env
QB_REALM_ID= paste it here
QB_ACCESS_TOKEN= 'paste it here'
QB_REFRESH_TOKEN='paste it here'
Same as above, I need to bring it to app.py and pass it to account.py


Step 6: Record a sale in QuickBooks
The underlying logic
Sales receipt logic
account.py
import requests
from flask import current_app

def create_sales_receipt(
        customer_id,
        item_id,
        amount):
    
    access_token = current_app.config["QB_ACCESS_TOKEN"]

    realm_id = current_app.config["QB_REALM_ID"]
    # print("Token:", access_token[:20])
    # print("Realm:", realm_id)

    url = (
        f"https://sandbox-quickbooks.api.intuit.com/v3/"
        f"company/{realm_id}/salesreceipt"
    )

    payload = {
        "CustomerRef": {
            "value": customer_id
        },

        "Line": [
            {
                "Amount": amount,
                "DetailType": "SalesItemLineDetail",
                "SalesItemLineDetail": {
                    "ItemRef": {
                        "value": item_id
                    }
                }
            }
        ]
    }

    response = requests.post(
        url,
        json=payload,
        headers={
            "Authorization": f"Bearer {access_token}",
            "Accept": "application/json"
        }
    )
    return response.json()
1. Function Definition
  • This function accepts three parameters:
    • customer_id – the QuickBooks Customer ID.
    • item_id – the QuickBooks Item (product or service) being sold.
    • amount – the total amount charged.
  • This tells QuickBooks:
    • Create a £20 (or $20 if my QuickBooks company uses USD) Sales Receipt for Customer 1 using Item 2.

2. Retrieve the Access Token

  • The Access Token proves that my Flask application has permission to use the QuickBooks API. 
3. Retrieve the Realm ID

  • The Realm ID uniquely identifies my QuickBooks company.
  • It tells Intuit: "Create this Sales Receipt in this QuickBooks company."

4. Build the API Endpoint 

  •  Sales are recorded on the Sales Transaction page in QuickBooks
  • This constructs the URL for the Sales Receipt API
5. Create the JSON Payload

  • This dictionary represents the Sales Receipt that will be sent to QuickBooks
  1. Customer Reference - QuickBooks needs to know who made the purchase.
  2. Sales Line - A Sales Receipt can contain multiple products or services. In my booking system, there is only one: Court Booking
  3. Amount - This specifies the amount charged. Unlike Stripe, QuickBooks expects the normal currency amount, not the smallest currency unit.
  4. Detail Type - This tells QuickBooks that the line represents a sale of an item or service.
  5. Item Reference - Every sales line must reference an existing QuickBooks Item.
6. Send the HTTP POST Request
  • This sends the Sales Receipt to QuickBooks.
  1. URL - The destination is the Sales Receipt API endpoint.
  2. JSON Data - The payload is automatically converted to JSON.
  3. Authorisation Header - The word "Bearer" tells QuickBooks that you're using an OAuth 2.0 access token. Without this header, QuickBooks would reject the request with an authentication error.
  4. Accept Header - This requests that QuickBooks return its response in JSON format.
7. Return the Response

  • The API response is converted into a Python dictionary.
  • My application can then:
    • save the Sales Receipt ID,
    • display the receipt number,
    • link the booking to the accounting record,
    • or log any errors returned by the API.
QuickBooks dashboard
Final wrap-up:

In this tutorial, you have built a complete payment and accounting workflow by integrating Stripe and QuickBooks Online with Flask. The system can securely process court booking payments, confirm reservations through Stripe webhooks, save booking records, and automatically create Sales Receipts in QuickBooks. By connecting these APIs, the Jersey City Pickleball Club can reduce manual work, minimise errors, improve financial tracking, and provide customers with a faster and more convenient booking experience. This approach demonstrates how modern web applications can automate real-world business processes by connecting payment platforms, accounting systems, and backend services together.

Published: July 2026
Last Updated: July 2026

---------------------------------------------------------------------------------------------------------------------------------------------------

Thanks for reading! 

If you haven't subscribed yet, join my newsletter to receive future Python and Flask tutorials.

---------------------------------------------------------------------------------------------------------------------------------------------------

Need a similar system for your business? 
I build custom Flask web applications and Python automation solutions for SMEs and solopreneurs. 

---------------------------------------------------------------------------------------------------------------------------------------------------

About the Author 
Kelvin Loh is a Python developer focused on Flask, desktop applications, and business automation solutions. He shares practical tutorials and real-world coding projects to help developers and small businesses build useful applications.





Comments

Popular Posts

How to Build an Audiobook Workflow Desktop System with Python?

In this tutorial, we will build a simple audiobook player using Python and CustomTkinter. You will learn how to convert text into speech using gTTS and play it with PyGame. We will also implement play, pause, and stop controls, like those in a real audio player. By the end, you will have a clean and functional desktop audiobook app. Prerequisite: This tutorial is part of the standalone tutorial. 📚 View the standalone tutorial Preliminary   Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies. python -m venv venv venv\Scripts\activate pip install customtkinter pillow gTTS pygame CTkMessagebox pypdf Then, the following steps include setting up the file structure, app.py, and two additional folders: the uploads and media folders. The media folder contains the icons necessary to build the app; there are read, pause, and stop icons, as shown on the diagram. Step 1: Build up the app interface I have 4 sections here: the...

How to Set Up PgAdmin and Adminer Using Docker Compose?

If you are new to database management with Docker, this tutorial will guide you through setting up both PgAdmin and Adminer using Docker containers. By containerising these tools, you can quickly launch lightweight and portable database management environments without installing them directly on your operating system. This approach also makes it easier to manage configurations, updates, and multiple projects across different devices.