payments-api
Payments API
- Base Path:
/api/v1/payments - Auth: Endpoints marked "Auth: required" expect a valid
Authorization: Bearer <token>.
Covers frontend integration for Razorpay (India, INR) and Stripe (non‑INR/non‑India) using Plan data.
Auth Notes
- Required: for all create actions (topups, subscriptions, Stripe onboarding).
- Guests: not allowed (
401) for create actions. - Dev override: non‑prod only, you can pass
x-user-id: <mongoUserId>instead of Bearer (not for production apps).
Razorpay: Get Public Key
- Method: GET
- Path:
/api/v1/payments/razorpay/key - Auth: not required
- Response 200:
\{ "keyId": "rzp_test_xxx" \} - Errors: 404 not configured; 500 on failure
Use the keyId with Razorpay Checkout.
Razorpay: Create Topup Order
- Method: POST
- Path:
/api/v1/payments/razorpay/topup - Auth: required
- Body:
\{ "planId": "<TOPUP plan _id>" \} - Behavior:
- Plan must be
planType: "TOPUP",country: "IN",currencyCode: "INR". - Creates a Razorpay Order (one‑time). points are credited by backend on webhook.
- Plan must be
- Response 201:
{
"order": {
"id": "order_Nf123...",
"amount": 9900,
"currency": "INR",
"status": "created",
"receipt": "topup_<userId>_169...",
"notes": { "userId": "u_123", "planId": "p_123", "points": 200 },
"created_at": 1693550000
},
"keyId": "rzp_test_xxx"
}
- Errors: 400 invalid plan; 401 auth; 404 not found; 500 failure
Client flow:
- Fetch key:
GET /api/v1/payments/razorpay/key - Create order:
POST /api/v1/payments/razorpay/topup \{ planId \} - Initialize Checkout (simplified): const rzp = new Razorpay({ key: keyId, order_id: order.id, amount: order.amount, currency: order.currency, name: 'Zishes', description: 'points Topup', notes: { planId }, handler: () => { /* optional: poll wallet */ } }); rzp.open();
Backend auto‑credits wallet on webhook (below).
Razorpay: Webhook (Topup Credit + Subscription Lifecycle)
- Method: POST
- Path:
/api/v1/payments/razorpay/webhook - Auth: Razorpay signature header
X-Razorpay-Signature(no Bearer) - Body: Raw JSON from Razorpay.
- Behavior:
- Verifies HMAC with
RAZORPAY_WEBHOOK_SECRETusing the raw body. - All point crediting: Only
invoice.paidevent credits points to wallet and sends notifications (for both topups and subscriptions). - Subscriptions: manages user's subscription status on lifecycle events:
subscription.activated: SetsactiveRazorpaySubscriptionandactiveRazorpayPlanafter first successful payment (no notification, no points)subscription.charged: Logged but no action taken (points credited viainvoice.paidinstead)invoice.paid: Credits points to wallet, creates ledger entry with country (India), sends "Plan purchase successful" notification showing points creditedsubscription.resumed: Reactivates subscription and clears cancellation flagssubscription.pending: Subscription created but not paid yet - no fields set (awaiting payment)subscription.cancelled: Clears all subscription fields (activeRazorpaySubscription,activeRazorpayPlan,razorpaySubscriptionCancelledAt,razorpaySubscriptionEndsAt), sends cancellation notificationsubscription.completed/subscription.halted/subscription.paused: Clears all subscription fields as subscription truly ended
- Verifies HMAC with
- Response 200:
\{ "ok": true \} - Errors: 401 invalid signature; 500 failures
Required env:
RAZORPAY_WEBHOOK_SECRET
Razorpay: Create Subscription
- Method: POST
- Path:
/api/v1/payments/razorpay/subscribe - Auth: required
- Body:
\{ "planId": "<SUBSCRIPTION plan _id>", "totalCount?": 12, "customerNotify?": false \} - Behavior:
- Plan must be
planType: "SUBSCRIPTION",country: "IN",currencyCode: "INR". - Ensures Razorpay Plan exists (creates if missing), then creates Subscription.
- Subscription is NOT immediately set on user - it will be set via webhook after successful payment (subscription.activated event).
- Plan must be
- Response 201:
\{ "subscription": \{ "id": "sub_Nfabc...", ... \} \} - Errors: 400 invalid input/type/country; 401 auth; 404 not found; 500 failure
Client flow:
- Fetch key:
GET /api/v1/payments/razorpay/key - Create subscription:
POST /api/v1/payments/razorpay/subscribe \{ planId \} - Initialize Checkout: const rzp = new Razorpay({ key: keyId, subscription_id: subscription.id, name: 'Zishes', description: 'Plan Subscription', notes: { planId } }); rzp.open();
Notes:
- India only (IN, INR). For other countries/currencies, use Stripe.
- Subscription lifecycle webhooks are handled (see Webhook section). The user document fields are updated as follows:
activeRazorpaySubscriptionandactiveRazorpayPlan: Set after first payment (subscription.activated)razorpaySubscriptionCancelledAt: Set when user cancels subscriptionrazorpaySubscriptionEndsAt: Set to the period end date when cancelled- All subscription fields cleared when subscription truly ends (completed/halted/paused events)
Razorpay: Get Active Subscription (India only)
- Method: GET
- Path:
/api/v1/payments/razorpay/subscription - Auth: required
- Behavior:
- Only for users whose address.country is
IN. - Looks up the user's
activeRazorpaySubscriptionand fetches latest details from Razorpay. - Includes stored plan details if available.
- Includes cancellation information if user has requested cancellation.
- Only for users whose address.country is
- Response 200:
{
"subscription": { "id": "sub_...", "status": "active", ... },
"plan": { "_id": "...", "points": 200, ... }, // optional
"cancelledAt": "2025-10-06T10:30:00.000Z", // ISO timestamp if cancellation requested, null otherwise
"endsAt": "2025-11-06T10:30:00.000Z" // ISO timestamp when subscription ends if cancelled, null otherwise
}
- Errors: 401 unauthorized; 404 when no active subscription; 500 failures
Razorpay: Unsubscribe (Cancel at Cycle End, India only)
- Method: POST
- Path:
/api/v1/payments/razorpay/unsubscribe - Auth: required
- Body: none
- Behavior:
- Only for users whose address.country is
IN. - Cancels the current subscription at the end of the ongoing billing cycle so the user is not billed next cycle.
- Records cancellation request (sets
razorpaySubscriptionCancelledAtandrazorpaySubscriptionEndsAt) and keeps subscription IDs until period ends. - User retains access to subscription benefits until the end date.
- Sends cancellation notification to user.
- Note: When Razorpay webhook
subscription.cancelledarrives, all fields (including IDs and cancellation timestamps) will be cleared.
- Only for users whose address.country is
- Response 200:
{
"ok": true,
"subscription": { "id": "sub_...", "status": "cancelled" | "pending", ... },
"cancelledAt": "2025-10-06T10:30:00.000Z", // ISO timestamp when cancellation was requested
"endsAt": "2025-11-06T10:30:00.000Z" // ISO timestamp when subscription period ends
}
- Errors:
- 400 non‑India
- 400 already cancelled:
{
"error": "Subscription already cancelled",
"message": "Your subscription is already scheduled to be cancelled on November 6, 2025",
"cancelledAt": "2025-10-06T10:30:00.000Z",
"endsAt": "2025-11-06T10:30:00.000Z"
}
- 401 unauthorized
- 404 when no active subscription
- 500 failures
Stripe: Get Publishable Key
- Method: GET
- Path:
/api/v1/payments/stripe/key - Auth: not required
- Response 200:
\{ "key": "pk_test_xxx" \} - Errors: 404 not configured; 500 failure
Use this key to initialize Stripe.js on the client.
Stripe subscriptions are not supported. Use Razorpay subscriptions (India, INR). Subscriptions outside India are currently disabled.
Stripe: Create Topup PaymentIntent (React Native)
- Method: POST
- Path:
/api/v1/payments/stripe/topup - Auth: required
- Body:
\{ "planId": "<TOPUP plan _id>" \} - Behavior:
- Plan must be
planType: "TOPUP"and NOT India/INR. For India INR, use Razorpay. - Creates a Stripe PaymentIntent with metadata
\{ userId, planId, points \}.
- Plan must be
- Response 201:
\{ "paymentIntentId": "pi_...", "clientSecret": "pi_..._secret_..." \}
Client flow (React Native):
- Get publishable key:
GET /api/v1/payments/stripe/key - Create intent:
POST /api/v1/payments/stripe/topup \{ planId \} - Collect card + confirm:
confirmPayment(clientSecret, \{ paymentMethodType: 'Card' \})
Wallet is credited via the Stripe webhook on payment_intent.succeeded.
The old URL-based Stripe Checkout flow and any Stripe subscription endpoints are removed.
Stripe: Webhook (Topups crediting)
- Method: POST
- Path:
/api/v1/payments/stripe/webhook - Auth: Stripe signature header
Stripe-Signature - Body: Raw JSON from Stripe
- Behavior:
payment_intent.succeeded: credits wallet for topups using metadata\{ userId, planId, points \}.
- Response 200:
\{ "ok": true \} - Env:
STRIPE_WEBHOOK_SECRET
Stripe: Account Onboarding (Sellers, non‑India)
- Method: POST
- Path:
/api/v1/payments/stripe/account/session - Auth: required
- Response 200:
\{ "client_secret": "as_123_secret_..." \} - Behavior:
- Creates or reuses a Stripe Express account for the authenticated user (non‑India only), returning an Account Session client secret for embedded onboarding.
- Errors: 400 not applicable (India); 401 auth; 404 user; 500 failure
Environment
- Razorpay:
RAZORPAY_KEY_ID,RAZORPAY_KEY_SECRET,RAZORPAY_WEBHOOK_SECRET - Stripe:
STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY
Related
- Plans catalog:
GET /api/v1/plans(seedocs/plans-api.md) - Wallet balance: see
docs/wallet-api.mdfor fetching current points after topup