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.pyTag: 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
| Name | In | Required | Constraints | Description |
|---|---|---|---|---|
course_id | query | required | CourseEnum | Course ID |
plan_group_id | query | optional | 1-10, default 1 | Plan group ID (PlanGroupEnum) |
status | query | optional | 0 or 1, default 1 | INACTIVE=0, ACTIVE=1 |
type | query | optional | 1-4 | PUBLIC=1, PRIVATE=2, GIFT=3, TRIAL=4 |
x-dev-time | header | optional | 13 digits | Epoch 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
}
| Field | Description |
|---|---|
description | PlateJS JSON string (rich text editor format) or null |
type | PUBLIC=1, PRIVATE=2, GIFT=3, TRIAL=4 |
subscription_components | What the plan grants access to. content_type: “taxonomy”, “custom_test”. content_id: specific ID or “all” |
max_price / min_price | In smallest currency unit (paisa for INR). 99900 = 999.00 INR |
precision | Decimal places for display (e.g. 2 for INR/USD) |
badge | "POPULAR", "RECOMMENDED", or null |
platform | Supported 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=0andtype=4are free trials
POST /orders
Summary: Create Order
Create a new order with plan and optional coupon code. Returns checkout-shaped data.
Parameters
| Name | In | Required | Description |
|---|---|---|---|
course_id | query | required | Course ID |
x-dev-time | header | optional | Epoch ms (13 digits) |
x-platform | header | optional | Client platform identifier |
x-device-id | header | optional | Device ID |
Request Body
{
"plan_id": "60d5ec49f1b2c72e4c8b1234",
"coupon_code": "SAVE20",
"state": "karnataka"
}
| Field | Required | Constraints | Description |
|---|---|---|---|
plan_id | required | ObjectId | Plan to purchase |
coupon_code | optional | max 50 chars | Coupon code to apply |
state | optional | max 100 chars, auto-lowercased | State where order was created |
Business Logic
- Validates
plan_idexists and is active - If
coupon_codeprovided: validates coupon for the plan (discount group, usage limits, validity period, platform restrictions) - Calculates:
total_amount = mrp - discount - Paid order (
total_amount > 0): Creates Razorpay order, returnspayment_mode="razorpay"withrazorpay_order_idandrazorpay_key_id - Trial order (
total_amount = 0, plan type = TRIAL): Returnspayment_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
}
| Field | Description |
|---|---|
payment_mode | "razorpay" for paid orders, "trial" for free trials |
amount | In smallest currency unit (paisa). 79900 = 799.00 INR |
razorpay_order_id | Pass to Razorpay Checkout.js SDK. null for trials |
razorpay_key_id | Razorpay public key for Checkout.js. null for trials |
Error Responses
| Error Code | Condition |
|---|---|
| 9206 | Plan not found |
| 9207 | Plan is inactive |
| 9204 | Coupon not valid for this plan |
| 9205 | Coupon total usage limit exceeded |
| 9219 | Coupon per-user limit exceeded |
| 9217 | Coupon is inactive |
| 9223 | Coupon restricted to different platform |
| 9221 | User already has active trial for this course |
| 9222 | User 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"
}
| Field | Required | Description |
|---|---|---|
order_id | required | Keystone 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
}
| Field | Description |
|---|---|
verification_status | COMPLETED=1, PENDING=2, FAILED=3 |
subscription_components | Active components after fulfillment |
expiry_date | Max expires_on across subscription components (epoch ms) |
started_at | Min started_on across subscription components (epoch ms) |
Error Responses
| Error Code | Condition |
|---|---|
| 9200 | Order not found |
| 9212 | Order user does not match authenticated user |
| 9213 | Payment amount mismatch |
| 9214 | Invalid 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
| Name | In | Required | Description |
|---|---|---|---|
order_id | path | required | Order ID (ObjectId) |
x-dev-time | header | optional | Epoch ms (13 digits) |
x-platform | header | optional | Client platform |
x-device-id | header | optional | Device ID |
Business Logic
- Validates order exists and belongs to authenticated user
- Validates order status is
CREATEDorPENDING - If PENDING with existing Razorpay order: returns stored IDs (no duplicate gateway order)
- If CREATED: creates new Razorpay order
- Free trial: returns
payment_mode="trial"
Success Response
Same InitiatePaymentResponse shape as POST /orders.
Error Responses
| Error Code | Condition |
|---|---|
| 9200 | Order not found |
| 1010 | Order 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
| Name | In | Required | Constraints | Description |
|---|---|---|---|---|
limit | query | optional | 1-100, default 20 | Max payment rows to return |
x-dev-time | header | optional | 13 digits | Epoch 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
| Name | In | Required | Description |
|---|---|---|---|
course_id | query | required | Course ID |
x-dev-time | header | optional | Epoch ms (13 digits) |
x-platform | header | optional | Client platform (needed when coupon has platform restrictions) |
x-device-id | header | optional | Device ID |
Request Body
{
"coupon_code": "SAVE20",
"plan_id": "60d5ec49f1b2c72e4c8b1234"
}
Both fields required. coupon_code is a string, plan_id is an ObjectId.
Validation Logic
- Checks coupon exists and is active
- Validates coupon is within its validity date range
- Checks coupon’s
discount_groupmatches the plan’sdiscount_group - Checks total usage has not exceeded
usage_limit - Checks per-user usage has not exceeded
user_limit - If coupon has
platformrestriction: validatesx-platformheader matches - 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
}
| Field | Description |
|---|---|
valid | true if coupon is valid for this plan, false otherwise |
discount_amount | In smallest currency unit (paisa). 19900 = 199.00 INR. null when invalid |
discount_type | FIXED_AMOUNT=1, PERCENTAGE=2. null when invalid |
precision | Decimal places from the plan. null when invalid |
discount_amount_display | Formatted string (e.g. “199.00 INR”). null when invalid |
discount_type_label | "FIXED_AMOUNT" or "PERCENTAGE". null when invalid |
message | Success or reason for invalidity |
Note: Invalid coupons return status: "success" with valid: false and a descriptive message. They do NOT return an error response.