Skip to main content

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.
  • 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_SECRET using the raw body.
    • All point crediting: Only invoice.paid event credits points to wallet and sends notifications (for both topups and subscriptions).
    • Subscriptions: manages user's subscription status on lifecycle events:
      • subscription.activated: Sets activeRazorpaySubscription and activeRazorpayPlan after first successful payment (no notification, no points)
      • subscription.charged: Logged but no action taken (points credited via invoice.paid instead)
      • invoice.paid: Credits points to wallet, creates ledger entry with country (India), sends "Plan purchase successful" notification showing points credited
      • subscription.resumed: Reactivates subscription and clears cancellation flags
      • subscription.pending: Subscription created but not paid yet - no fields set (awaiting payment)
      • subscription.cancelled: Clears all subscription fields (activeRazorpaySubscription, activeRazorpayPlan, razorpaySubscriptionCancelledAt, razorpaySubscriptionEndsAt), sends cancellation notification
      • subscription.completed / subscription.halted / subscription.paused: Clears all subscription fields as subscription truly ended
  • 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).
  • 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:
    • activeRazorpaySubscription and activeRazorpayPlan: Set after first payment (subscription.activated)
    • razorpaySubscriptionCancelledAt: Set when user cancels subscription
    • razorpaySubscriptionEndsAt: 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 activeRazorpaySubscription and fetches latest details from Razorpay.
    • Includes stored plan details if available.
    • Includes cancellation information if user has requested cancellation.
  • 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 razorpaySubscriptionCancelledAt and razorpaySubscriptionEndsAt) 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.cancelled arrives, all fields (including IDs and cancellation timestamps) will be cleared.
  • 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 \}.
  • 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 (see docs/plans-api.md)
  • Wallet balance: see docs/wallet-api.md for fetching current points after topup