Owner Abhishek P

V1 API Payments

Source: OpenAPI 3.1.0 spec + src/api/v1/order/routes.py, src/api/v1/plan/routes.py, src/api/v1/coupon/routes.py, src/api/v1/payment/routes.py Tag: Payments - Plans, orders, coupons, payment verification.

All endpoints require Bearer Token authentication. Response data is encrypted when feature flag is enabled.


GET /plans

Summary: List Plans

List all plans for the course matching filters, sorted by updated_at. No pagination. Trial plan visibility is server-controlled based on user subscription history and trial eligibility.

Parameters

NameInRequiredConstraintsDescription
course_idqueryrequiredCourseEnumCourse ID
plan_group_idqueryoptional1-10, default 1Plan group ID (PlanGroupEnum)
statusqueryoptional0 or 1, default 1INACTIVE=0, ACTIVE=1
typequeryoptional1-4PUBLIC=1, PRIVATE=2, GIFT=3, TRIAL=4
x-dev-timeheaderoptional13 digitsEpoch ms

Success Response

{
  "status": "success",
  "is_data_encrypted": 0,
  "data": [
    {
      "id": "60d5ec49f1b2c72e4c8b1234",
      "name": "Annual Premium",
      "description": "[{\"type\":\"paragraph\",\"children\":[{\"text\":\"Full access to all subjects\"}]}]",
      "plan_group_id": 1,
      "sort_order": 1,
      "type": 1,
      "subscription_components": [
        {
          "content_type": "taxonomy",
          "content_id": "all"
        }
      ],
      "duration_in_days": 365,
      "currency": "INR",
      "precision": 2,
      "max_price": 99900,
      "min_price": 99900,
      "max_price_display": "999.00 INR",
      "min_price_display": "999.00 INR",
      "status": 1,
      "badge": "POPULAR",
      "platform": [1, 2, 3],
      "country": "INDIA"
    },
    {
      "id": "60d5ec49f1b2c72e4c8b5678",
      "name": "7-Day Free Trial",
      "description": null,
      "plan_group_id": 1,
      "sort_order": 0,
      "type": 4,
      "subscription_components": [
        {
          "content_type": "taxonomy",
          "content_id": "all"
        }
      ],
      "duration_in_days": 7,
      "currency": "INR",
      "precision": 2,
      "max_price": 0,
      "min_price": 0,
      "max_price_display": "0.00 INR",
      "min_price_display": "0.00 INR",
      "status": 1,
      "badge": null,
      "platform": [1, 2],
      "country": "INDIA"
    }
  ],
  "error": null,
  "app_actions": null
}
FieldDescription
descriptionPlateJS JSON string (rich text editor format) or null
typePUBLIC=1, PRIVATE=2, GIFT=3, TRIAL=4
subscription_componentsWhat the plan grants access to. content_type: “taxonomy”, “custom_test”. content_id: specific ID or “all”
max_price / min_priceIn smallest currency unit (paisa for INR). 99900 = 999.00 INR
precisionDecimal places for display (e.g. 2 for INR/USD)
badge"POPULAR", "RECOMMENDED", or null
platformSupported platforms. IOS=1, ANDROID=2, WEB=3

Notes:

  • TRIAL plans (type=4) are only returned if the user is eligible (no active/previous trial for this course)
  • Plans with min_price=0 and type=4 are free trials

POST /orders

Summary: Create Order

Create a new order with plan and optional coupon code. Returns checkout-shaped data.

Parameters

NameInRequiredDescription
course_idqueryrequiredCourse ID
x-dev-timeheaderoptionalEpoch ms (13 digits)
x-platformheaderoptionalClient platform identifier
x-device-idheaderoptionalDevice ID

Request Body

{
  "plan_id": "60d5ec49f1b2c72e4c8b1234",
  "coupon_code": "SAVE20",
  "state": "karnataka"
}
FieldRequiredConstraintsDescription
plan_idrequiredObjectIdPlan to purchase
coupon_codeoptionalmax 50 charsCoupon code to apply
stateoptionalmax 100 chars, auto-lowercasedState where order was created

Business Logic

  1. Validates plan_id exists and is active
  2. If coupon_code provided: validates coupon for the plan (discount group, usage limits, validity period, platform restrictions)
  3. Calculates: total_amount = mrp - discount
  4. Paid order (total_amount > 0): Creates Razorpay order, returns payment_mode="razorpay" with razorpay_order_id and razorpay_key_id
  5. Trial order (total_amount = 0, plan type = TRIAL): Returns payment_mode="trial" without Razorpay

Success Response (paid order)

{
  "status": "success",
  "is_data_encrypted": 0,
  "data": {
    "order_id": "60d5ec49f1b2c72e4c8b7890",
    "payment_mode": "razorpay",
    "razorpay_order_id": "order_LkjH8sdf9sKJh",
    "razorpay_key_id": "rzp_live_abc123def456",
    "amount": 79900,
    "currency": "INR"
  },
  "error": null,
  "app_actions": null
}

Success Response (trial order)

{
  "status": "success",
  "is_data_encrypted": 0,
  "data": {
    "order_id": "60d5ec49f1b2c72e4c8b7891",
    "payment_mode": "trial",
    "razorpay_order_id": null,
    "razorpay_key_id": null,
    "amount": 0,
    "currency": "INR"
  },
  "error": null,
  "app_actions": null
}
FieldDescription
payment_mode"razorpay" for paid orders, "trial" for free trials
amountIn smallest currency unit (paisa). 79900 = 799.00 INR
razorpay_order_idPass to Razorpay Checkout.js SDK. null for trials
razorpay_key_idRazorpay public key for Checkout.js. null for trials

Error Responses

Error CodeCondition
9206Plan not found
9207Plan is inactive
9204Coupon not valid for this plan
9205Coupon total usage limit exceeded
9219Coupon per-user limit exceeded
9217Coupon is inactive
9223Coupon restricted to different platform
9221User already has active trial for this course
9222User not eligible for trial subscription

POST /orders/verify-payment

Summary: Verify Razorpay Payment (primary)

Primary verification path. Handles both paid (Razorpay gateway resolution) and trial (internal fulfillment) orders.

Request Body

{
  "order_id": "60d5ec49f1b2c72e4c8b7890"
}
FieldRequiredDescription
order_idrequiredKeystone order document ID (ObjectId)

Business Logic

  • Paid orders: Server resolves payment status via Razorpay API, creates subscription on success
  • Trial orders (total_amount=0, plan type=TRIAL): Completes internal trial fulfillment, creates subscription

Deprecated: POST /v1/payments/razorpay-verify (use this endpoint instead)

Success Response

{
  "status": "success",
  "is_data_encrypted": 0,
  "data": {
    "order_id": "60d5ec49f1b2c72e4c8b7890",
    "payment_id": "pay_LkjH8sdf9sKJh",
    "status": "SUCCESS",
    "message": "Payment verified successfully",
    "verification_status": 1,
    "subscription_components": [
      {
        "content_type": "taxonomy",
        "content_id": "all"
      }
    ],
    "expiry_date": 1745936000000,
    "started_at": 1714400000000
  },
  "error": null,
  "app_actions": null
}
FieldDescription
verification_statusCOMPLETED=1, PENDING=2, FAILED=3
subscription_componentsActive components after fulfillment
expiry_dateMax expires_on across subscription components (epoch ms)
started_atMin started_on across subscription components (epoch ms)

Error Responses

Error CodeCondition
9200Order not found
9212Order user does not match authenticated user
9213Payment amount mismatch
9214Invalid payment status from gateway

POST /orders/{order_id}/initiate-payment

Summary: Initiate Payment

Returns checkout data for an existing order. If the order is already PENDING with a stored Razorpay order, returns those IDs without creating a duplicate.

Parameters

NameInRequiredDescription
order_idpathrequiredOrder ID (ObjectId)
x-dev-timeheaderoptionalEpoch ms (13 digits)
x-platformheaderoptionalClient platform
x-device-idheaderoptionalDevice ID

Business Logic

  1. Validates order exists and belongs to authenticated user
  2. Validates order status is CREATED or PENDING
  3. If PENDING with existing Razorpay order: returns stored IDs (no duplicate gateway order)
  4. If CREATED: creates new Razorpay order
  5. Free trial: returns payment_mode="trial"

Success Response

Same InitiatePaymentResponse shape as POST /orders.

Error Responses

Error CodeCondition
9200Order not found
1010Order already paid/failed (OPERATION_NOT_ALLOWED)

GET /payments

Summary: List Billing History

Successful payments for the current user (newest first), with order plan snapshots.

Parameters

NameInRequiredConstraintsDescription
limitqueryoptional1-100, default 20Max payment rows to return
x-dev-timeheaderoptional13 digitsEpoch ms

Success Response

{
  "status": "success",
  "is_data_encrypted": 0,
  "data": [
    {
      "order_id": "60d5ec49f1b2c72e4c8b7890",
      "payment_id": "pay_LkjH8sdf9sKJh",
      "amount": 79900,
      "currency": "INR",
      "status": "PAID",
      "plan_name": "Annual Premium",
      "plan_duration_in_days": 365,
      "created_at": 1714400000000
    }
  ],
  "error": null,
  "app_actions": null
}

POST /coupons/validate

Summary: Validate Coupon Code

Validate a coupon code for a specific plan and get discount information. Returns valid: false with a message for invalid coupons (not an error response).

Parameters

NameInRequiredDescription
course_idqueryrequiredCourse ID
x-dev-timeheaderoptionalEpoch ms (13 digits)
x-platformheaderoptionalClient platform (needed when coupon has platform restrictions)
x-device-idheaderoptionalDevice ID

Request Body

{
  "coupon_code": "SAVE20",
  "plan_id": "60d5ec49f1b2c72e4c8b1234"
}

Both fields required. coupon_code is a string, plan_id is an ObjectId.

Validation Logic

  1. Checks coupon exists and is active
  2. Validates coupon is within its validity date range
  3. Checks coupon’s discount_group matches the plan’s discount_group
  4. Checks total usage has not exceeded usage_limit
  5. Checks per-user usage has not exceeded user_limit
  6. If coupon has platform restriction: validates x-platform header matches
  7. Calculates discount amount based on discount_type (FIXED_AMOUNT or PERCENTAGE)

Success Response (valid coupon)

{
  "status": "success",
  "is_data_encrypted": 0,
  "data": {
    "valid": true,
    "discount_amount": 19900,
    "discount_type": 1,
    "precision": 2,
    "currency": "INR",
    "discount_amount_display": "199.00 INR",
    "discount_type_label": "FIXED_AMOUNT",
    "message": "Coupon applied successfully"
  },
  "error": null,
  "app_actions": null
}

Success Response (invalid coupon)

{
  "status": "success",
  "is_data_encrypted": 0,
  "data": {
    "valid": false,
    "discount_amount": null,
    "discount_type": null,
    "precision": null,
    "currency": null,
    "discount_amount_display": null,
    "discount_type_label": null,
    "message": "Coupon code has expired"
  },
  "error": null,
  "app_actions": null
}
FieldDescription
validtrue if coupon is valid for this plan, false otherwise
discount_amountIn smallest currency unit (paisa). 19900 = 199.00 INR. null when invalid
discount_typeFIXED_AMOUNT=1, PERCENTAGE=2. null when invalid
precisionDecimal places from the plan. null when invalid
discount_amount_displayFormatted string (e.g. “199.00 INR”). null when invalid
discount_type_label"FIXED_AMOUNT" or "PERCENTAGE". null when invalid
messageSuccess or reason for invalidity

Note: Invalid coupons return status: "success" with valid: false and a descriptive message. They do NOT return an error response.