Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Pragyat-Nikunj/Learning-Management-System-backend/llms.txt

Use this file to discover all available pages before exploring further.

The LMS backend delegates payment collection to Stripe Checkout. Your client never handles raw card data — it receives a Stripe-hosted session URL, redirects the user there, and waits for the server to confirm the outcome through a webhook. This page covers every step a client needs to implement that flow correctly.

Environment variables

Before making any payment calls, ensure these variables are set on the server:
VariablePurpose
STRIPE_SECRET_KEYAuthenticates all Stripe API requests
STRIPE_WEBHOOK_SECRETValidates the signature on every incoming webhook event
STRIPE_SUCCESS_URLWhere Stripe redirects after successful payment (defaults to http://localhost:5173/payment/success?session_id={CHECKOUT_SESSION_ID})
STRIPE_CANCEL_URLWhere Stripe redirects when the user cancels (defaults to http://localhost:5173/payment/cancel)
Never expose STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET to the browser. Both are server-only secrets.

Payment flow

1

Create a checkout session

Your client sends a POST request with the courseId the user wants to buy. The server looks up the course, creates a Stripe Checkout session priced in USD, records a CoursePurchase document with status pending, and returns the full session object.
curl -X POST https://your-api/api/v1/payments/create-checkout-session \
  -H "Content-Type: application/json" \
  -H "Cookie: token=<your-auth-token>" \
  -d '{"courseId": "64f1a2b3c4d5e6f7a8b9c0d1"}'
Response
{
  "success": true,
  "message": "Stripe checkout initiated successfully.",
  "data": {
    "session": {
      "id": "cs_test_a1B2c3D4e5F6...",
      "url": "https://checkout.stripe.com/pay/cs_test_a1B2c3D4e5F6...",
      "payment_status": "unpaid",
      "metadata": {
        "courseId": "64f1a2b3c4d5e6f7a8b9c0d1",
        "userId": "64e0f1a2b3c4d5e6f7a8b9c0"
      }
    },
    "coursePurchase": {
      "_id": "64f2a3b4c5d6e7f8a9b0c1d2",
      "status": "pending",
      "paymentMethod": "stripe",
      "currency": "USD",
      "amount": 49
    },
    "course": {
      "title": "Complete Node.js Bootcamp",
      "description": "...",
      "amount": 49
    }
  }
}
This endpoint requires authentication. The isAuthenticated middleware reads the user ID from the request, so the user must be logged in before initiating checkout.
2

Redirect the user to Stripe

Extract data.session.url from the response and redirect the browser there. Stripe hosts the payment form — no card UI is needed in your frontend.
const { data } = await response.json();
window.location.href = data.session.url;
3

Handle post-payment redirects

After payment, Stripe redirects the user back to your app:
  • Success: STRIPE_SUCCESS_URL — the session_id query parameter is appended automatically when you use {CHECKOUT_SESSION_ID} in the URL template.
  • Cancel: STRIPE_CANCEL_URL — the user abandoned checkout; the purchase remains pending in the database.
Check the purchase status at GET /api/v1/payments/courses/:courseId/purchase-status to confirm the final state before unlocking course content.
4

Stripe calls the webhook

After a charge settles, Stripe sends a POST request to POST /api/v1/payments/webhook. The server verifies the stripe-signature header and updates the CoursePurchase record accordingly.
EventResult
charge.succeededPurchase status set to completed
charge.failedPurchase status set to failed
The webhook responds with {"received": true} on success. Any other event type is logged and ignored.

Purchase status lifecycle

A CoursePurchase document moves through these states:
pending  →  completed   (charge.succeeded webhook received)
         →  failed      (charge.failed webhook received)
The refunded status exists in the model but is not set by the Stripe webhook handler — it is reserved for manual refund processing.

Webhook setup

Why express.raw is required

The webhook route is registered with express.raw({ type: 'application/json' }) instead of the standard JSON body parser:
router.post("/webhook", express.raw({ type: 'application/json' }), handleStripeWebhook);
Stripe’s signature verification (stripe.webhooks.constructEvent) requires the raw, unmodified request body as a Buffer. If any middleware parses the body into a JavaScript object first, the bytes change and the signature check will always fail with a 400 error.
Do not add express.json() or any other body-parsing middleware to the webhook route. The express.raw middleware on this route must receive the unmodified byte stream.

Test webhooks locally with Stripe CLI

Run the Stripe CLI listener to forward live Stripe events to your local server:
stripe listen --forward-to localhost:4000/api/v1/payments/webhook
The CLI prints a webhook signing secret — set it as STRIPE_WEBHOOK_SECRET in your .env for local testing. Stop the listener with Ctrl+C when done.
Install the Stripe CLI from stripe.com/docs/stripe-cli before running the command above.
Returns the purchase status for the authenticated user and a given course. Use this after the Stripe redirect to decide whether to unlock course content.Response
{
  "status": "success",
  "data": {
    "course": {
      "title": "Complete Node.js Bootcamp",
      "price": 49
    },
    "purchaseStatus": "completed"
  }
}
purchaseStatus will be "not purchased" if no record exists.
Returns all courses the authenticated user has successfully purchased (status completed), sorted newest first.Response shape
{
  "status": "success",
  "data": {
    "purchase": [
      {
        "course": { "title": "...", "price": 49 },
        "amount": 49,
        "currency": "USD",
        "paymentMethod": "stripe",
        "createdAt": "2024-09-01T10:00:00.000Z"
      }
    ]
  }
}

Build docs developers (and LLMs) love