- Hero mockup: enhanced 3D perspective and shadow - Testimonials: illustrated SVG avatars - Growth pricing card: visual prominence (scale, gradient, badge) - Most Popular badge: repositioned to avoid overlapping heading - Nav: added Log In link next to Start Free Trial - Fixed btn-primary text colour on anchor tags (white on blue) - Fixed cursor: default on all non-interactive elements - Disabled user-select on non-form content to prevent text caret
269 lines
6.9 KiB
Markdown
269 lines
6.9 KiB
Markdown
# TenderRadar Billing API Examples
|
|
|
|
Quick reference for testing billing endpoints. Replace `AUTH_TOKEN` with a real JWT token from `/api/auth/login`.
|
|
|
|
## Setup
|
|
|
|
```bash
|
|
# 1. Register a user
|
|
curl -X POST http://localhost:3456/api/auth/register \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"email": "test@example.com",
|
|
"password": "testpass123",
|
|
"company_name": "Test Corp"
|
|
}'
|
|
|
|
# Response includes: user object and JWT token
|
|
# Save the token for subsequent calls:
|
|
export AUTH_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
```
|
|
|
|
## 1. Create Checkout Session
|
|
|
|
Start the subscription flow by creating a checkout session:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:3456/api/billing/checkout \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
-d '{
|
|
"plan": "starter",
|
|
"successUrl": "https://app.example.com/billing/success",
|
|
"cancelUrl": "https://app.example.com/billing/cancel"
|
|
}'
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"sessionId": "cs_test_abc123...",
|
|
"url": "https://checkout.stripe.com/pay/cs_test_abc123..."
|
|
}
|
|
```
|
|
|
|
**Next steps:**
|
|
1. Open the `url` in a browser
|
|
2. Complete the payment form (test card: 4242 4242 4242 4242)
|
|
3. User is redirected to `successUrl`
|
|
4. Stripe sends webhook to `/api/billing/webhook`
|
|
|
|
## 2. Get Subscription Status
|
|
|
|
Check the current subscription status (after completing checkout):
|
|
|
|
```bash
|
|
curl -X GET http://localhost:3456/api/billing/subscription \
|
|
-H "Authorization: Bearer $AUTH_TOKEN"
|
|
```
|
|
|
|
**Response (with active subscription):**
|
|
```json
|
|
{
|
|
"subscription": {
|
|
"id": 1,
|
|
"user_id": 5,
|
|
"stripe_customer_id": "cus_ABC123...",
|
|
"stripe_subscription_id": "sub_ABC123...",
|
|
"plan": "starter",
|
|
"status": "active",
|
|
"trial_start": "2026-02-14T12:49:00.000Z",
|
|
"trial_end": "2026-02-28T12:49:00.000Z",
|
|
"current_period_start": "2026-02-14T12:49:00.000Z",
|
|
"current_period_end": "2026-03-14T12:49:00.000Z",
|
|
"cancel_at_period_end": false,
|
|
"created_at": "2026-02-14T12:49:00.000Z",
|
|
"updated_at": "2026-02-14T12:49:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response (no subscription):**
|
|
```json
|
|
{
|
|
"subscription": null,
|
|
"message": "No active subscription. User is on free tier."
|
|
}
|
|
```
|
|
|
|
## 3. Create Customer Portal Session
|
|
|
|
Allow users to manage their subscription (upgrade, downgrade, cancel):
|
|
|
|
```bash
|
|
curl -X POST http://localhost:3456/api/billing/portal \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
-d '{
|
|
"returnUrl": "https://app.example.com/account/billing"
|
|
}'
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"url": "https://billing.stripe.com/session/cs_test_abc123..."
|
|
}
|
|
```
|
|
|
|
**Usage:**
|
|
1. Open the `url` in a browser (user must be logged into Stripe or their payment method)
|
|
2. User can upgrade/downgrade plans, update payment method, or cancel
|
|
3. After managing subscription, redirected back to `returnUrl`
|
|
|
|
## Test Scenarios
|
|
|
|
### Scenario 1: New User Signup → Checkout
|
|
|
|
```bash
|
|
# 1. Register
|
|
TOKEN=$(curl -s -X POST http://localhost:3456/api/auth/register \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"email": "newuser@example.com",
|
|
"password": "pass123",
|
|
"company_name": "New Corp"
|
|
}' | jq -r '.token')
|
|
|
|
# 2. Create checkout session
|
|
curl -X POST http://localhost:3456/api/billing/checkout \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-d '{
|
|
"plan": "growth",
|
|
"successUrl": "https://app.example.com/success",
|
|
"cancelUrl": "https://app.example.com/cancel"
|
|
}' | jq '.url'
|
|
|
|
# 3. Open URL in browser and complete payment
|
|
# (Use test card 4242 4242 4242 4242, any future expiry, any 3-digit CVC)
|
|
|
|
# 4. Check subscription status (after webhook processes)
|
|
curl -X GET http://localhost:3456/api/billing/subscription \
|
|
-H "Authorization: Bearer $TOKEN"
|
|
```
|
|
|
|
### Scenario 2: Upgrade Plan
|
|
|
|
```bash
|
|
# 1. User is on "starter" plan, wants to upgrade to "pro"
|
|
|
|
# 2. Create new checkout session (Stripe recognizes customer, handles proration)
|
|
curl -X POST http://localhost:3456/api/billing/checkout \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-d '{
|
|
"plan": "pro",
|
|
"successUrl": "https://app.example.com/success",
|
|
"cancelUrl": "https://app.example.com/cancel"
|
|
}'
|
|
|
|
# 3. Complete payment
|
|
# 4. Webhook updates subscription to "pro" plan
|
|
```
|
|
|
|
### Scenario 3: Manage via Customer Portal
|
|
|
|
```bash
|
|
# User goes to billing page in app and clicks "Manage Subscription"
|
|
|
|
# Create portal session
|
|
curl -X POST http://localhost:3456/api/billing/portal \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-d '{
|
|
"returnUrl": "https://app.example.com/account/billing"
|
|
}' | jq '.url'
|
|
|
|
# Open URL in browser
|
|
# User can downgrade, upgrade, or cancel without returning to app
|
|
```
|
|
|
|
## Testing with Stripe CLI (Local Webhooks)
|
|
|
|
To test webhooks locally without exposing your server:
|
|
|
|
```bash
|
|
# 1. Install Stripe CLI (if not already installed)
|
|
# macOS: brew install stripe/stripe-cli/stripe
|
|
# Linux/Windows: See https://stripe.com/docs/stripe-cli
|
|
|
|
# 2. Login to your Stripe account
|
|
stripe login
|
|
|
|
# 3. Start listening and forwarding to local server
|
|
stripe listen --forward-to localhost:3456/api/billing/webhook
|
|
|
|
# Output will show: Ready! Your webhook signing secret is: whsec_test_abc123...
|
|
# Update .env: STRIPE_WEBHOOK_SECRET=whsec_test_abc123...
|
|
|
|
# 4. In another terminal, trigger test events:
|
|
stripe trigger checkout.session.completed
|
|
|
|
# Webhook will be forwarded to localhost:3456/api/billing/webhook
|
|
```
|
|
|
|
## Test Cards
|
|
|
|
Stripe provides test cards for different scenarios:
|
|
|
|
| Card Number | Result | Expiry | CVC |
|
|
|---|---|---|---|
|
|
| 4242 4242 4242 4242 | Successful charge | Any future | Any 3 digits |
|
|
| 4000 0025 0000 3155 | Insufficient funds | Any future | Any 3 digits |
|
|
| 4000 0000 0000 0002 | Card declined | Any future | Any 3 digits |
|
|
| 4000 0000 0000 0010 | Address verification failed | Any future | Any 3 digits |
|
|
|
|
## Error Responses
|
|
|
|
### Missing Authorization Token
|
|
```bash
|
|
curl -X GET http://localhost:3456/api/billing/subscription
|
|
|
|
# Response:
|
|
{"error": "No token provided"}
|
|
```
|
|
|
|
### Invalid Plan
|
|
```bash
|
|
curl -X POST http://localhost:3456/api/billing/checkout \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-d '{"plan": "invalid_plan", ...}'
|
|
|
|
# Response:
|
|
{"error": "Invalid plan: invalid_plan"}
|
|
```
|
|
|
|
### No Active Subscription
|
|
```bash
|
|
curl -X POST http://localhost:3456/api/billing/portal \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-d '{"returnUrl": "..."}'
|
|
|
|
# Response (if user not subscribed):
|
|
{"error": "No subscription found for user"}
|
|
```
|
|
|
|
## Debugging
|
|
|
|
Check server logs for detailed webhook processing:
|
|
|
|
```bash
|
|
# Terminal running the server shows:
|
|
# Processing webhook event: checkout.session.completed
|
|
# Subscription created for user 5 on plan starter
|
|
```
|
|
|
|
Database query to check subscription status:
|
|
|
|
```bash
|
|
# Connect to PostgreSQL
|
|
psql -U tenderpilot -d tenderpilot -h localhost
|
|
|
|
# Check subscriptions
|
|
SELECT * FROM subscriptions WHERE user_id = 5;
|
|
|
|
# Check user tier
|
|
SELECT id, email, tier FROM users WHERE id = 5;
|
|
```
|