Owner: Engineering Team | Last Updated: 2026-01-30 | Status: Current
The WWAI Web App payment system handles plan selection, checkout, subscription management, and retention flows. It integrates three external services:
The frontend is built with Next.js and uses Formik/Yup for form validation, NextAuth for authentication, and Axios for API calls.
+------------------+ +-------------------+ +--------------------+
| Pricing Page | | Backend API | | Stripe Checkout |
| (Checkout.tsx) | | (Django) | | (Hosted Page) |
+--------+---------+ +---------+---------+ +----------+---------+
| | |
1. User selects plan | |
2. Fills email + password | |
| | |
3. POST /api/payments/ | |
new-chekout/ --------------->| |
| | |
4. Receives {url, | |
checkout_session_id} <--------| |
| | |
5. Stores session data | |
in sessionStorage | |
| | |
6. Calls signIn('credentials') | |
| | |
7. Redirects to Stripe ---------|--------------------------->|
Checkout URL | |
| | 8. User pays
| | |
| 10. Stripe webhook ----------> |
| updates subscription 9. Redirect to
| | thank-you page
|<--------------------------|----------------------------|
| Step | Actor | Action |
|---|---|---|
| 1 | User | Selects a plan on the pricing/checkout page |
| 2 | User | Enters email and password (new users); form validated by Formik/Yup |
| 3 | Frontend | POSTs to ${NEXT_PUBLIC_BACKEND_URL}/api/payments/new-chekout/ |
| 4 | Backend | Returns { url, checkout_session_id } where url is the Stripe Checkout session URL |
| 5 | Frontend | Stores checkout_session_id in sessionStorage under key sessionId |
| 6 | Frontend | Calls signIn('credentials', { email, password, redirect: false }) via NextAuth |
| 7 | Frontend | Redirects the browser to the Stripe Checkout URL via router.push(checkoutSessionUrl) |
| 8 | User | Completes payment on Stripe-hosted checkout page |
| 9 | Stripe | Redirects user to the thank-you page |
| 10 | Stripe | Sends webhook to backend; backend updates the user's subscription record |
Note on the endpoint URL: The path
/api/payments/new-chekout/contains a typo ("chekout" instead of "checkout"). This is intentional to document the real codebase. The typo exists in production and must be used as-is when calling this endpoint.
POST ${NEXT_PUBLIC_BACKEND_URL}/api/payments/new-chekout/
Request body:
{
"plan": "WALTER_PRO_M",
"email": "user@example.com",
"password": "optional-for-new-users"
}
| Field | Type | Required | Description |
|---|---|---|---|
plan |
string |
Yes | Plan identifier (see Plan Identifiers below) |
email |
string |
Yes | User email address |
password |
string |
No | Password for new user registration |
Response:
{
"url": "https://checkout.stripe.com/c/pay/cs_live_...",
"checkout_session_id": "cs_live_..."
}
| Field | Type | Description |
|---|---|---|
url |
string |
Stripe Checkout session URL to redirect the user to |
checkout_session_id |
string |
Stripe session ID stored client-side for reference |
GET ${NEXT_PUBLIC_BACKEND_URL}/api/payments/pricing/
Response:
{
"results": [
{
// BackendPlan object with current pricing data
}
]
}
Used by Checkout.tsx to dynamically fetch and display current plan prices. No authentication required.
GET ${NEXT_PUBLIC_BACKEND_URL}/api/user/billing-portal?update=${update}&utm_source=${utmSource}
Headers:
Authorization: Bearer ${session?.access_token}
Query parameters:
| Parameter | Type | Description |
|---|---|---|
update |
string |
Update context flag |
utm_source |
string |
UTM tracking source |
Response:
{
"url": "https://billing.stripe.com/p/session/..."
}
The frontend redirects to this URL via router.push(data.url). The Stripe Customer Portal allows users to:
Source: settings/Subscription.tsx
POST /api/churnkey-generate
Request body:
{
"customerId": "cus_abc123"
}
Response:
{
"user_hash": "hmac-sha256-hash-string"
}
This is a Next.js API route (app/api/churnkey-generate/route.ts) that generates an HMAC-SHA256 hash to authenticate the user for the Churnkey retention portal.
The plan field sent to the checkout API uses the following identifiers:
| Plan | Monthly | Yearly |
|---|---|---|
| Starter | WALTER_STARTER_M |
WALTER_STARTER_Y |
| Pro | WALTER_PRO_M |
WALTER_PRO_Y |
| Unlimited | WALTER_UNLIMITED_M |
WALTER_UNLIMITED_Y |
The naming convention is WALTER_{TIER}_{PERIOD} where period is M (monthly) or Y (yearly).
data/plan.tsx)PLAN_CREDIT)| Plan | Credits |
|---|---|
| Free | 300 |
| Starter | 10,000 |
| Pro | 55,000 |
| Unlimited | Unlimited |
| Special | 15,000 |
| Trial | 2,000 |
PLAN_REQUEST_LIMIT)| Plan | Max Words per Request |
|---|---|
| Free | 300 |
| Starter | 500 |
| Pro | 1,200 |
| Unlimited | 1,700 |
| Special | 1,200 |
| Trial | 500 |
CURRENCY_LIST)| Currency | Code |
|---|---|
| US Dollar | USD |
| Canadian Dollar | CAD |
| Euro | EUR |
| British Pound | GBP |
| Australian Dollar | AUD |
Each currency entry includes its display symbol.
Churnkey provides retention flows when a user attempts to cancel, pause, or downgrade their subscription. It is rendered as an iframe within the app.
POST /api/churnkey-generate with the user's Stripe customerIduser_hash and used to authenticate the Churnkey session| Variable | Context | Description |
|---|---|---|
CHURNKEY_LIVE_KEY |
Production (server-only) | Used for HMAC generation in production |
CHURNKEY_TEST_KEY |
Non-production (server-only) | Used for HMAC generation in development/staging |
The route selects the key based on the environment: production uses CHURNKEY_LIVE_KEY, all other environments use CHURNKEY_TEST_KEY.
The checkout flow persists state in the browser's sessionStorage:
| Key | Value | Purpose |
|---|---|---|
chosenPlanCheckout |
Plan identifier string | Tracks which plan the user selected |
checkoutEmail |
Email address string | Preserves email during checkout flow |
sessionId |
Stripe checkout session ID | Links client state to the Stripe session |
Checkout errors are handled with:
toast.error(e?.response?.data?.error || t('somethingWentWrong'))
logError(e, {
tags: { component: 'Checkout', action: 'handleStartCheckoutSession' },
context: { userId, email }
})
t('somethingWentWrong')| Variable | Description |
|---|---|
NEXT_PUBLIC_BACKEND_URL |
Backend API base URL |
NEXT_PUBLIC_WALTER_STARTER_M |
Starter monthly plan ID |
NEXT_PUBLIC_WALTER_STARTER_M_PRICE |
Starter monthly price |
NEXT_PUBLIC_WALTER_STARTER_Y |
Starter yearly plan ID |
NEXT_PUBLIC_WALTER_STARTER_Y_PRICE |
Starter yearly price |
NEXT_PUBLIC_WALTER_PRO_M |
Pro monthly plan ID |
NEXT_PUBLIC_WALTER_PRO_M_PRICE |
Pro monthly price |
NEXT_PUBLIC_WALTER_PRO_Y |
Pro yearly plan ID |
NEXT_PUBLIC_WALTER_PRO_Y_PRICE |
Pro yearly price |
NEXT_PUBLIC_WALTER_UNLIMITED_M |
Unlimited monthly plan ID |
NEXT_PUBLIC_WALTER_UNLIMITED_M_PRICE |
Unlimited monthly price |
NEXT_PUBLIC_WALTER_UNLIMITED_Y |
Unlimited yearly plan ID |
NEXT_PUBLIC_WALTER_UNLIMITED_Y_PRICE |
Unlimited yearly price |
| Variable | Description |
|---|---|
CHURNKEY_LIVE_KEY |
Production Churnkey API key for HMAC generation |
CHURNKEY_TEST_KEY |
Test/sandbox Churnkey API key for HMAC generation |
| File | Purpose |
|---|---|
components/elements/Checkout.tsx |
Checkout page with plan selection, form, and Stripe redirect |
data/plan.tsx |
Plan definitions, credit limits, request limits, currency list |
settings/Subscription.tsx |
Billing portal redirect and subscription management UI |
app/api/churnkey-generate/route.ts |
Next.js API route for Churnkey HMAC hash generation |
| Date | Author | Change |
|---|---|---|
| 2026-01-30 | Admin | Rewrote page with full checkout flow, API payloads, plan data, Churnkey details, and environment variables |
| 2026-01-30 | Admin | Initial creation |
Prev: Web App - Authentication | Next: Web App - User Settings | Up: WalterWrites