Files
scan-receipts/STRIPE_SETUP_GUIDE.md
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

12 KiB
Raw Permalink Blame History

Stripe Setup & Integration Guide (ScanReceipts Pro)

This comprehensive guide walks you through configuring Stripe for payment processing, subscription management, webhooks, and license generation for Receipt Scanner to Excel / DATEV (ScanReceipts).


1. Overview & Pricing Architecture

ScanReceipts offers three Pro access tiers:

Tier / Plan Price (EUR) Billing Type Trial / Terms Environment Variable
Weekly Pass 4,99 € Recurring (weekly) 3-Day Free Trial STRIPE_WEEKLY_PRICE_ID
Annual Pass 39,99 € Recurring (yearly) Full 12 Months Access (~3,33 €/Mo) STRIPE_ANNUAL_PRICE_ID
Lifetime License 59,99 € One-Time Payment Perpetual access & all future updates STRIPE_LIFETIME_PRICE_ID

💡 Inline Fallback: If you do not create custom Price IDs in Stripe, the application will automatically create ad-hoc inline line items (price_data) with the exact prices and trial settings above. Supplying explicit Price IDs is recommended for production accounting and analytics.


2. Prerequisites & Stripe Account Setup

  1. Sign Up or Log In: Go to the Stripe Dashboard.
  2. Activate Test Mode: Toggle the "Test mode" switch in the top-right corner of the Stripe Dashboard (the UI will indicate test mode with an orange/yellow banner).
  3. Ensure your default currency is set to EUR (€).

3. Obtaining API Keys

  1. Navigate to Developers > API keys (https://dashboard.stripe.com/test/apikeys).
  2. Publishable key: Copy the key starting with pk_test_....
  3. Secret key: Click "Reveal live key token" (or create a new restricted/standard key) starting with sk_test_....
  4. Add these keys to your .env.local file:
    STRIPE_PUBLISHABLE_KEY=pk_test_51...
    STRIPE_SECRET_KEY=sk_test_51...
    

4. Creating Products & Prices in the Stripe Dashboard

To track subscriptions and revenue analytics cleanly in Stripe, create the three Pro products:

4.1 Product: Weekly Pass (Wochen-Pass)

  1. In Stripe Dashboard, go to Product catalog (https://dashboard.stripe.com/test/products) and click + Add product.
  2. Name: Receipt Scanner Pro - Wochen-Pass
  3. Description: Unbegrenzte Belege, Dual-Sheet Excel & Buchhaltungs-CSV (inkl. 3 Tage Trial)
  4. Pricing:
    • Pricing model: Standard pricing
    • Price: 4,99 EUR
    • Billing period: Weekly (Wöchentlich)
  5. Click Save product.
  6. Under the Pricing section of this product, copy the Price ID (starts with price_...).
  7. Add to .env.local:
    STRIPE_WEEKLY_PRICE_ID=price_1Q...
    

4.2 Product: Annual Pass (Jahres-Pass)

  1. Click + Add product.
  2. Name: Receipt Scanner Pro - Jahres-Pass
  3. Description: Volle 12 Monate unbegrenzte Belege, Dual-Sheet Excel & Prioritäts-Support
  4. Pricing:
    • Pricing model: Standard pricing
    • Price: 39,99 EUR
    • Billing period: Yearly (Jährlich)
  5. Click Save product.
  6. Copy the Price ID (price_...).
  7. Add to .env.local:
    STRIPE_ANNUAL_PRICE_ID=price_1Q...
    

4.3 Product: Lifetime License (Lebenslange Lizenz)

  1. Click + Add product.
  2. Name: Receipt Scanner Pro - Lifetime Lizenz
  3. Description: Lebenslanger unbegrenzter Zugriff ohne Folgekosten inklusive aller Updates
  4. Pricing:
    • Pricing model: Standard pricing
    • Price: 59,99 EUR
    • Billing period: One-time (Einmalig)
  5. Click Save product.
  6. Copy the Price ID (price_...).
  7. Add to .env.local:
    STRIPE_LIFETIME_PRICE_ID=price_1Q...
    

5. Webhook Configuration (Production & Staging)

When a customer completes checkout or cancels a subscription, Stripe sends webhook events to your server.

  1. Navigate to Developers > Webhooks (https://dashboard.stripe.com/test/webhooks).
  2. Click + Add destination (or + Add endpoint).
  3. Endpoint URL:
    • Production: https://your-domain.com/api/webhooks/stripe
    • Staging/Preview: https://staging.your-domain.com/api/webhooks/stripe
  4. Select events to listen to:
    • checkout.session.completed (Creates license in PostgreSQL & triggers Discord notification)
    • customer.subscription.updated (Updates expiration date and active status)
    • customer.subscription.deleted (Downgrades user license to cancelled/free)
    • invoice.payment_succeeded (Renews active subscription period)
    • invoice.payment_failed (Marks license as past due)
  5. Click Add endpoint.
  6. In the newly created webhook page, find Signing secret and click Reveal.
  7. Copy the signing secret (starts with whsec_...) and add to .env.local:
    STRIPE_WEBHOOK_SECRET=whsec_...
    

5.1 Webhook verification & license-granting policy (security notes)

The webhook endpoint (/api/webhooks/stripe) enforces, in order:

  1. Signature verification is the sole entry gate. Every request is verified with stripe.webhooks.constructEvent(rawBody, sig, STRIPE_WEBHOOK_SECRET) before any business logic runs. A missing/invalid signature gets a 400 — nothing else executes. Never disable or bypass this check.
  2. Licenses are only granted for confirmed payments.
    • One-time payments (Lifetime, mode: "payment"): only when payment_status === "paid".
    • Subscriptions (Weekly/Annual, mode: "subscription"): the subscription is retrieved from Stripe and only active / trialing statuses activate a license; expiresAt is derived from subscription.current_period_end (never from the server clock).
    • Unconfirmed events are acknowledged with 200 + received: true and logged, but create no license and no Stripe retry.
  3. Amount cross-check (defense in depth). For one-time payments the paid amount_total (minor units) is compared against the shared price catalog (src/lib/billing/pricing.ts, e.g. Lifetime = 5999). On mismatch the event is acknowledged and logged but no license is created. Subscription trials legitimately carry amount_total = 0, so subscription amounts are not cross-checked (status gating covers them).
  4. Plan validation. The plan from checkout metadata is validated with isPlanId/resolvePlan; unknown values fall back to the annual plan (same contract as the checkout route) and are logged as a warning.

⚠️ Testing note: stripe trigger checkout.session.completed generates a synthetic session without plan metadata and without a subscription, so it is (correctly) treated as unconfirmed and will not create a license. To test license creation end-to-end, run a real checkout through the app (stripe listen --forward-to localhost:3000/api/webhooks/stripe) and pay with Stripe's test card 4242 4242 4242 4242.

Provider limitation: Stripe is currently the only integrated payment provider (Paddle is not a dependency and no Paddle route exists). If a second provider is added later, it must follow the same verification pattern: cryptographic signature check as the sole gate, payment-status confirmation before granting, amount cross-check against the shared catalog, and idempotent license creation.


6. Local Testing with the Stripe CLI

You can test the entire checkout, webhook, and licensing pipeline on your local machine (http://localhost:3000) using the official Stripe CLI.

6.1 Install Stripe CLI

  • Windows (Scoop):
    scoop install stripe
    
  • macOS (Homebrew):
    brew install stripe/stripe-cli/stripe
    
  • Direct Binary Download: Download the latest release executable from GitHub Releases and add it to your PATH.

6.2 Authenticate CLI

Run:

stripe login

Follow the in-terminal link to authenticate with your Stripe account.

6.3 Forward Webhooks to Local Server

Start your Next.js development server:

npm run dev

In a separate terminal, forward Stripe events to your local webhook route:

stripe listen --forward-to localhost:3000/api/webhooks/stripe

Stripe CLI will print a local webhook signing secret in your terminal:

> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Copy this secret and set STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxx... in .env.local (and restart npm run dev if needed).

6.4 Triggering Test Events

You can trigger synthetic Stripe events directly from the CLI:

  1. Test Checkout Session Completion:
    stripe trigger checkout.session.completed
    
  2. Test Subscription Cancellation:
    stripe trigger customer.subscription.deleted
    
  3. Test Subscription Renewal Payment:
    stripe trigger invoice.payment_succeeded
    
  4. Test Failed Payment:
    stripe trigger invoice.payment_failed
    

Check your terminal logs and database to see the license generated and logged.


7. Discord Sales Alert Bot (Optional)

ScanReceipts includes real-time sales notifications via Discord Webhook:

  1. In your Discord server, open Server Settings > Integrations > Webhooks.
  2. Click New Webhook, name it (e.g. Receipt Scanner Sales), select a channel, and click Copy Webhook URL.
  3. Add the URL to your .env.local:
    DISCORD_SALES_WEBHOOK_URL=https://discord.com/api/webhooks/123456789/abcdef...
    
  4. Whenever a checkout completes, the bot will post an emerald green embed containing the plan purchased, formatted amount in EUR, customer email, and timestamp.

8. License Verification API Reference

GET /api/license/verify

Verify an active license key or Stripe checkout session ID.

Query Parameters:

  • key or licenseKey: The generated license key (e.g., RS-PRO-A1B2-C3D4)
  • sessionId or session_id: The Stripe checkout session ID (cs_test_... or cs_live_...)

Example Request:

curl "http://localhost:3000/api/license/verify?key=RS-PRO-A1B2-C3D4"

Success Response (HTTP 200):

{
  "valid": true,
  "plan": "lifetime",
  "status": "active",
  "expiresAt": null,
  "licenseKey": "RS-PRO-A1B2-C3D4",
  "source": "database"
}

9. Environment Variables Reference (.env.local)

Here is the complete template for your environment configuration:

# ==============================================
# Next.js Application URL
# ==============================================
NEXT_PUBLIC_APP_URL=http://localhost:3000

# ==============================================
# Stripe API Keys & Secrets
# ==============================================
STRIPE_SECRET_KEY=sk_test_51...
STRIPE_PUBLISHABLE_KEY=pk_test_51...
STRIPE_WEBHOOK_SECRET=whsec_...

# ==============================================
# Optional Stripe Price IDs (Dashboard Created)
# ==============================================
STRIPE_WEEKLY_PRICE_ID=price_...
STRIPE_ANNUAL_PRICE_ID=price_...
STRIPE_LIFETIME_PRICE_ID=price_...

# ==============================================
# Discord Sales Notification Webhook (Optional)
# ==============================================
DISCORD_SALES_WEBHOOK_URL=https://discord.com/api/webhooks/...

# ==============================================
# Database Persistence (PostgreSQL)
# ==============================================
DATABASE_URL=postgresql://receipt_user:receipt_password@localhost:5432/receipt_scanner

10. Troubleshooting & FAQ

Issue: "Invalid signature" error in webhook logs

  • Ensure STRIPE_WEBHOOK_SECRET matches the signing secret displayed by Stripe CLI (stripe listen) during local development, or the signing secret in Stripe Dashboard under Webhooks for production.
  • Ensure the raw request body is read cleanly without intermediate JSON serialization before signature validation (ScanReceipts handles this via await req.text()).

Issue: Stripe Checkout redirects to 404 or localhost in production

  • Set NEXT_PUBLIC_APP_URL to your production domain (e.g. https://scanreceipts.io).

Issue: Database is offline during checkout

  • ScanReceipts uses a Local-First Architecture. If PostgreSQL is unreachable or DATABASE_URL is omitted, the checkout and verification routes gracefully fall back to zero-friction local validation without crashing or returning errors to the user.