Skip to content
Stripe Payment Integration: Complete Developer Guide

Stripe Payment Integration: Complete Developer Guide

DodaTech Updated Jun 20, 2026 9 min read

Stripe is a payment processing platform that provides APIs for accepting payments, managing subscriptions, handling refunds, and processing transactions securely with PCI-compliant infrastructure and developer-friendly tooling.

What You’ll Learn

By the end of this tutorial, you’ll implement Stripe payments end-to-end — from creating PaymentIntents and Checkout Sessions to handling webhooks, managing subscriptions, and testing with idempotency keys.

Why Stripe Matters

Every SaaS application needs payments. Stripe handles the complexity: PCI compliance, card storage, fraud detection, global payments, and recurring billing. Doda Browser uses Stripe to process Pro subscription payments across 40+ countries with automatic currency conversion and smart retry logic.

Stripe Payment Flow


flowchart LR
    subgraph "Stripe Checkout Flow"
        U[User] --> C[Your App]
        C --> S[Stripe API]
        S --> CS[Checkout Session]
        CS -- "Redirect" --> U
        U -- "Fill card details" --> S
        S -- "Webhook: checkout.session.completed" --> C
        C -- "Verify & fulfill" --> DB[(Database)]
    end
    style S fill:#6772e5,color:#fff

Setup

pip install stripe
export STRIPE_SECRET_KEY=sk_test_...
export STRIPE_PUBLISHABLE_KEY=pk_test_...

Creating a PaymentIntent

# payment_intent.py
# Create and confirm a PaymentIntent

import stripe
import os

stripe.api_key = os.environ['STRIPE_SECRET_KEY']

def create_payment(amount=2000, currency='usd'):
    """Create a PaymentIntent for a one-time payment.
    
    Args:
        amount: Amount in cents ($20.00 = 2000)
        currency: ISO currency code
    """
    intent = stripe.PaymentIntent.create(
        amount=amount,
        currency=currency,
        automatic_payment_methods={'enabled': True},
        description='Pro Plan Annual Subscription',
        metadata={
            'order_id': 'ORD-12345',
            'customer_email': 'alice@example.com'
        },
    )

    print(f"PaymentIntent created: {intent.id}")
    print(f"Amount: ${intent.amount / 100:.2f}")
    print(f"Status: {intent.status}")
    print(f"Client Secret: {intent.client_secret[:20]}...")

    return intent

# Test
intent = create_payment(2999, 'usd')

Expected output:

PaymentIntent created: pi_3Mqwerty19QfG5XG0gTzFz1L
Amount: $29.99
Status: requires_payment_method
Client Secret: pi_3Mqwerty19QfG5X...

Checkout Session

Checkout Sessions create a hosted payment page — no need to build a card form yourself.

# checkout_session.py
# Create a Stripe Checkout Session for subscription

import stripe
import os

stripe.api_key = os.environ['STRIPE_SECRET_KEY']

def create_checkout_session():
    """Create a Stripe Checkout Session for subscription purchase."""
    session = stripe.checkout.Session.create(
        success_url='https://myapp.com/success?session_id={CHECKOUT_SESSION_ID}',
        cancel_url='https://myapp.com/pricing',
        mode='subscription',
        line_items=[{
            'price': 'price_1MqwertyABC123',  # Create this in Stripe Dashboard
            'quantity': 1,
        }],
        customer_email='alice@example.com',
        metadata={
            'plan': 'pro_annual',
            'source': 'landing_page',
        },
        subscription_data={
            'metadata': {'plan_tier': 'pro'},
            'trial_period_days': 14,
        },
        allow_promotion_codes=True,
        automatic_tax={'enabled': True},
    )

    print(f"Checkout Session: {session.id}")
    print(f"URL: {session.url}")
    print(f"Mode: {session.mode}")

    # Redirect user to session.url
    return session

session = create_checkout_session()

Expected output:

Checkout Session: cs_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
URL: https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4...
Mode: subscription

Handling Webhooks

# webhooks.py
# Stripe webhook handler for payment events

from flask import Flask, request, jsonify
import stripe
import os
import json

app = Flask(__name__)
stripe.api_key = os.environ['STRIPE_SECRET_KEY']
endpoint_secret = os.environ['STRIPE_WEBHOOK_SECRET']

@app.route('/stripe/webhook', methods=['POST'])
def stripe_webhook():
    """Handle Stripe webhook events."""
    payload = request.get_data(as_text=True)
    sig_header = request.headers.get('Stripe-Signature')

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, endpoint_secret
        )
    except ValueError:
        return jsonify({'error': 'Invalid payload'}), 400
    except stripe.error.SignatureVerificationError:
        return jsonify({'error': 'Invalid signature'}), 400

    # Handle the event
    event_type = event['type']
    data = event['data']['object']

    print(f"Received event: {event_type}")

    handlers = {
        'checkout.session.completed': handle_checkout_completed,
        'payment_intent.succeeded': handle_payment_succeeded,
        'payment_intent.payment_failed': handle_payment_failed,
        'customer.subscription.updated': handle_subscription_updated,
        'customer.subscription.deleted': handle_subscription_deleted,
    }

    handler = handlers.get(event_type)
    if handler:
        handler(data)
    else:
        print(f"Unhandled event type: {event_type}")

    return jsonify({'status': 'ok'}), 200

def handle_checkout_completed(session):
    """Fulfill the order after successful checkout."""
    customer_email = session.get('customer_details', {}).get('email')
    subscription_id = session.get('subscription')
    metadata = session.get('metadata', {})

    print(f"Checkout completed for {customer_email}")
    print(f"Subscription: {subscription_id}")
    print(f"Plan: {metadata.get('plan')}")

    # Activate account, send welcome email, etc.
    # update_user_subscription(customer_email, subscription_id)

def handle_payment_succeeded(payment_intent):
    """Process successful payment."""
    amount = payment_intent['amount'] / 100
    currency = payment_intent['currency'].upper()
    print(f"Payment received: ${amount:.2f} {currency}")

def handle_payment_failed(payment_intent):
    """Notify customer of payment failure."""
    customer_email = payment_intent.get('receipt_email', 'unknown')
    print(f"Payment failed for {customer_email}")
    # Send notification, ask to update payment method

def handle_subscription_updated(subscription):
    """Handle subscription status changes."""
    status = subscription['status']
    customer = subscription['customer']
    print(f"Subscription {subscription['id']}: {status} for customer {customer}")

def handle_subscription_deleted(subscription):
    """Handle subscription cancellation."""
    print(f"Subscription {subscription['id']} cancelled")
    # Downgrade account, archive data, etc.

if __name__ == '__main__':
    # Test locally: stripe listen --forward-to localhost:5000/stripe/webhook
    app.run(port=5000, debug=True)

Handling Refunds

# refunds.py
# Process full and partial refunds

import stripe
import os

stripe.api_key = os.environ['STRIPE_SECRET_KEY']

def process_full_refund(payment_intent_id, reason='requested_by_customer'):
    """Process a full refund for a PaymentIntent."""
    try:
        refund = stripe.Refund.create(
            payment_intent=payment_intent_id,
            reason=reason,
            metadata={
                'refund_initiated_by': 'support_agent',
                'ticket_id': 'TKT-789',
            }
        )
        print(f"Refund processed: {refund.id}")
        print(f"Amount: ${refund.amount / 100:.2f}")
        print(f"Status: {refund.status}")
        return refund
    except stripe.error.InvalidRequestError as e:
        print(f"Refund failed: {e}")
        return None

def process_partial_refund(payment_intent_id, amount_cents):
    """Process a partial refund for a specific amount."""
    refund = stripe.Refund.create(
        payment_intent=payment_intent_id,
        amount=amount_cents,
        reason='requested_by_customer',
    )
    print(f"Partial refund of ${amount_cents / 100:.2f}: {refund.id}")
    return refund

# Test
process_full_refund('pi_3Mqwerty19QfG5XG0gTzFz1L')

Expected output:

Refund processed: re_1MqwertyABC123
Amount: $29.99
Status: succeeded

Subscription Management

# subscriptions.py
# Manage Stripe subscriptions: create, update, cancel

import stripe
import os

stripe.api_key = os.environ['STRIPE_SECRET_KEY']

def list_active_subscriptions():
    """List all active subscriptions."""
    subscriptions = stripe.Subscription.list(status='active', limit=10)
    for sub in subscriptions.auto_paging_iter():
        print(f"{sub.id}: {sub.customer} — "
              f"${sub.plan.amount / 100:.2f}/{sub.plan.interval}")
    return subscriptions

def update_subscription_plan(subscription_id, new_price_id):
    """Upgrade/downgrade a subscription to a new price."""
    subscription = stripe.Subscription.retrieve(subscription_id)
    updated = stripe.Subscription.modify(
        subscription_id,
        items=[{
            'id': subscription['items']['data'][0]['id'],
            'price': new_price_id,
        }],
        proration_behavior='always_invoice',  # Charge/credit prorated amount
    )
    print(f"Subscription {subscription_id} updated to price {new_price_id}")
    return updated

def cancel_subscription(subscription_id, at_period_end=True):
    """Cancel a subscription."""
    if at_period_end:
        sub = stripe.Subscription.modify(
            subscription_id,
            cancel_at_period_end=True,
        )
        print(f"Subscription {subscription_id} will cancel at period end")
    else:
        sub = stripe.Subscription.delete(subscription_id)
        print(f"Subscription {subscription_id} cancelled immediately")

    return sub

def reactivate_subscription(subscription_id):
    """Reactivate a subscription set to cancel."""
    sub = stripe.Subscription.modify(
        subscription_id,
        cancel_at_period_end=False,
    )
    print(f"Subscription {subscription_id} reactivated")
    return sub

# Usage
# cancel_subscription('sub_1MqwertyABC123', at_period_end=True)

Testing with Idempotency

# idempotency_test.py
# Use idempotency keys to prevent duplicate charges

import stripe
import os
import uuid

stripe.api_key = os.environ['STRIPE_SECRET_KEY']

def charge_with_idempotency(amount, currency='usd'):
    """Create a payment with idempotency to prevent duplicates."""
    idempotency_key = str(uuid.uuid4())

    try:
        intent = stripe.PaymentIntent.create(
            amount=amount,
            currency=currency,
            confirm=True,
            payment_method='pm_card_visa',
            idempotency_key=idempotency_key,
        )
        print(f"Payment created: {intent.id} (key: {idempotency_key})")

        # If we retry with the same key, Stripe returns the same result
        duplicate = stripe.PaymentIntent.create(
            amount=amount,
            currency=currency,
            confirm=True,
            payment_method='pm_card_visa',
            idempotency_key=idempotency_key,  # Same key!
        )
        print(f"Duplicate result: {duplicate.id} (same as original)")

        return intent
    except stripe.error.StripeError as e:
        print(f"Stripe error: {e}")
        return None

charge_with_idempotency(1000)

Expected output:

Payment created: pi_3Mqwerty19QfG5XG0gTzFz2M (key: abc123...)
Duplicate result: pi_3Mqwerty19QfG5XG0gTzFz2M (same as original)

Test Card Numbers

CardNumberScenario
Visa4242424242424242Success
Visa (debit)4000056655665556Success
Mastercard5555555555554444Success
Amex378282246310005Success
Requires auth40000025000031553D Secure required
Declined4000000000000002Generic decline
Insufficient funds4000000000009995Insufficient funds
Expired card4000000000000069Expired card

Common Errors

1. Not Handling Webhook Signature Verification

Without verifying the Stripe-Signature header, anyone can send fake webhooks to your endpoint. Always use stripe.Webhook.construct_event() with your endpoint secret.

2. Forgetting Idempotency Keys

Network issues can cause your request to reach Stripe but the response to be lost. Without idempotency keys, retrying creates duplicate charges. Use idempotency_key on all POST requests.

3. Confusing Cents with Dollars

Stripe amounts are in the smallest currency unit (cents for USD). 2000 = $20.00. A common bug is passing 20.00 instead of 2000.

4. Not Testing with Test Mode

Test mode webhooks go to test endpoints, live mode only to live endpoints. Mixing them up causes missing webhooks in production. Use separate endpoints or check the livemode field.

5. Ignoring Webhook Event Order

Webhooks can arrive out of order. A payment_intent.succeeded might arrive before checkout.session.completed. Design your webhook handler to be idempotent and handle events independently.

6. Storing Raw Card Numbers

Stripe returns card details with the last 4 digits only. Never store full card numbers — you only need the PaymentMethod ID and the last 4 digits for receipts.

Practice Questions

1. What is the difference between a PaymentIntent and a Checkout Session?

PaymentIntent is the API primitive for tracking a payment from creation to completion. Checkout Session is a higher-level object that renders a hosted payment page and creates a PaymentIntent (or Subscription) behind the scenes.

2. Why are Stripe amounts in cents?

Stripe uses the smallest currency unit to avoid floating-point precision issues. $20.00 is 2000 cents. This eliminates rounding errors common with floats.

3. How do you ensure a refund can’t be processed twice?

Use idempotency keys on refund requests. The same idempotency_key always returns the same result, preventing duplicate refunds.

4. What happens when a subscription payment fails?

Stripe retries based on the payment retry rules (typically 3 retries over 5 days). After all retries fail, the subscription becomes past_due. You receive webhook events for each failure.

5. Challenge: Build a subscription billing system that: (1) creates a Checkout Session for a monthly pro plan, (2) handles the checkout.session.completed webhook to activate the account, (3) listens for invoice.payment_failed to send an email notification, (4) provides an API endpoint to upgrade/downgrade plans with proration.

Implement with Flask/FastAPI. Use Stripe webhooks for async events. Store subscription ID in your database. For upgrades, use stripe.Subscription.modify with proration behavior. Return prorated amounts to the frontend for confirmation.

Mini Project: Stripe Dashboard Monitor

# dashboard.py
# Monitor Stripe account metrics

import stripe
import os
from datetime import datetime, timedelta

stripe.api_key = os.environ['STRIPE_SECRET_KEY']

def stripe_dashboard():
    """Print current Stripe account metrics."""
    print("═" * 50)
    print("Stripe Account Dashboard")
    print("═" * 50)

    # Balance
    balance = stripe.Balance.retrieve()
    available = sum(b['amount'] for b in balance['available'])
    pending = sum(b['amount'] for b in balance['pending'])
    print(f"\nBalance:")
    print(f"  Available: ${available / 100:.2f}")
    print(f"  Pending:   ${pending / 100:.2f}")

    # Recent charges
    charges = stripe.Charge.list(limit=5)
    print(f"\nRecent Charges (last 5):")
    for charge in charges:
        created = datetime.fromtimestamp(charge['created'])
        print(f"  {charge['id'][:20]:<22} "
              f"${charge['amount'] / 100:>7.2f} "
              f"{charge['currency'].upper():<4} "
              f"{'✅' if charge['paid'] else '❌'} "
              f"{created.strftime('%m/%d %H:%M')}")

    # Active subscriptions
    subs = stripe.Subscription.list(status='active', limit=1)
    total_subs = 0
    for _ in subs.auto_paging_iter():
        total_subs += 1
        if total_subs >= 1000:
            break
    print(f"\nActive subscriptions: {total_subs}+")
    print("═" * 50)

if __name__ == '__main__':
    stripe_dashboard()

Expected output:

══════════════════════════════════════════════════
Stripe Account Dashboard
══════════════════════════════════════════════════

Balance:
  Available: $12543.20
  Pending:   $2341.50

Recent Charges (last 5):
  ch_3Mqwerty19QfG5X   $29.99 USD ✅ 06/19 14:32
  ch_3Mqwerty20QfG6Y   $49.99 USD ✅ 06/19 13:15
  ch_3Mqwerty21QfG7Z   $29.99 USD ✅ 06/19 11:45
  ch_3Mqwerty22QfG8A   $9.99  USD ✅ 06/19 10:00
  ch_3Mqwerty23QfG9B   $29.99 USD ✅ 06/19 09:22

Active subscriptions: 450+

══════════════════════════════════════════════════

FAQ

What is the difference between test mode and live mode?
Test mode uses test API keys (sk_test_) and test card numbers. No real money moves. Live mode uses live keys (sk_live_) and processes real transactions. Switch by changing the API key.
How do I handle 3D Secure authentication?
Stripe automatically handles 3D Secure for cards that require it. The Checkout Session handles the redirect. For PaymentIntent, use payment_intent.next_action.redirect_to_url to redirect the user.
Can I store customer payment methods for future use?
Yes. When creating a Checkout Session, set mode='subscription' or pass setup_future_usage='off_session' on the PaymentIntent. Stripe saves the payment method and returns a PaymentMethod ID.
How do I charge a customer off-session?
Create a PaymentIntent with the customer’s saved PaymentMethod ID and set off_session=True. Stripe handles the charge. If 3D Secure is required, the payment fails and you need to ask the customer to authenticate.
What currencies does Stripe support?
Stripe supports 135+ currencies. Use lowercase ISO codes (usd, eur, gbp, jpy). For zero-decimal currencies (JPY, KRW), amounts are in whole units, not cents.

Related Concepts

What’s Next

You now know Stripe integration! Next, master webhooks for any event-driven integration, then explore OAuth 2.0 for secure API authentication patterns.

  • Practice daily — Set up Stripe test mode and run the code examples
  • Build a project — Build a SaaS subscription page with Stripe Checkout, webhooks, and a dashboard
  • Explore related topics — Check out Stripe Connect for marketplace payments, Stripe Billing for advanced invoicing, and fraud prevention with Radar

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro