V1 API Custom Tests
Source: OpenAPI 3.1.0 spec +
src/api/v1/custom_test/routes.py,src/api/v1/custom_test/schemas.pyTag: Custom Tests - Create, submit, discard, and list custom tests.
All endpoints require Bearer Token authentication. Response data is encrypted when feature flag is enabled.
POST /custom-tests
Summary: Create Custom Test
Create a new custom test with configurable MCQ selection, duration, mode, and explanation settings.
Parameters
| Name | In | Required | Description |
|---|---|---|---|
course_id | query | required | Course ID |
x-dev-time | header | optional | Epoch ms (13 digits) |
Request Body
{
"mcq_selection_filters": {
"selection_type": 1,
"taxonomy_ids__in": [
"60d5ec49f1b2c72e4c8b0001",
"60d5ec49f1b2c72e4c8b0002"
],
"year__in": [2022, 2023],
"tag_ids__in": null,
"question_type_distribution": {
"1": 60,
"2": 30,
"3": 10
}
},
"number_of_mcqs": 20,
"duration_in_mins": 30,
"test_mode": 2,
"explanation_mode": 3,
"explanation_detail_level": 2,
"mcq_algorithm": 3
}
| Field | Type | Default | Constraints | Description |
|---|---|---|---|---|
mcq_selection_filters | object | null | optional | MCQ selection criteria (see below) |
number_of_mcqs | integer | 10 | >0, <=120 | Number of MCQs to select |
duration_in_mins | integer | 10 | >0, <=300 | Test duration in minutes |
test_mode | integer | 1 | enum | STUDY=1, EXAM=2, MULTI_PLAYER=3 |
explanation_mode | integer | 1 | enum | ALL=1, WRONG_ONLY=2, SHOW_AT_END=3 |
explanation_detail_level | integer | 1 | enum | SHORT=1, FULL=2 |
mcq_algorithm | integer | 3 | enum | V1=1 (question type dist), V2=2 (taxonomy-first), V3=3 (simple filters) |
mcq_selection_filters fields:
| Field | Type | Description |
|---|---|---|
selection_type | integer | ADAPTIVE=1, MANUAL=2. Default: ADAPTIVE |
taxonomy_ids__in | array | Filter by taxonomy IDs |
year__in | array | Filter by years (e.g. [2022, 2023]) |
tag_ids__in | array | Filter by tag IDs |
question_type_distribution | object | Map of QuestionTypeEnum to percentage. Keys: "1" (PYQ), "2" (DQ), "3" (EQ). Must sum to 100 |
Conditions:
- For V3 algorithm (
mcq_algorithm=3):number_of_mcqsmust be betweenCUSTOM_TEST_V3_MIN_MCQSandCUSTOM_TEST_V3_MAX_MCQS; error1003 question_type_distributionvalues must sum to exactly 100; error1003if not- If no MCQs match the filters: error
6906(CUSTOM_TEST_NO_MCQS_FOUND_DURING_CREATION) - Rate limits: error
6909(free limit exceeded) or6910(daily limit exceeded)
Success Response
{
"status": "success",
"is_data_encrypted": 0,
"data": {
"id": "60d5ec49f1b2c72e4c8b9999",
"short_uid": "CT00456",
"mcqs": [
{
"id": "60d5ec49f1b2c72e4c8b4567",
"display_uid": "UPSC-POL-001",
"short_uid": "M00123",
"question": "Which article of the Indian Constitution deals with the Right to Equality?",
"question_format": 1,
"q_pview": "Which article of the Indian Constitution deals with the Right to Equality?",
"option_1": "Article 12",
"selected_option_1_count": 245,
"option_2": "Article 14",
"selected_option_2_count": 1893,
"option_3": "Article 19",
"selected_option_3_count": 312,
"option_4": "Article 21",
"selected_option_4_count": 189,
"correct_option": "option_2",
"question_type": 1,
"year": 2023,
"root_taxonomy_id": "60d5ec49f1b2c72e4c8b0001",
"taxonomy_ids": ["60d5ec49f1b2c72e4c8b0001", "60d5ec49f1b2c72e4c8b0002", "60d5ec49f1b2c72e4c8b0003"],
"sole": "U2FsdGVkX1+abc123...",
"sort_order": 15,
"tag_ids": ["60d5ec49f1b2c72e4c8baaaa"],
"docket_id": "60d5ec49f1b2c72e4c8bdddd",
"is_deleted": false,
"ordered_block_ids": ["B001"],
"block_details": [
{
"id": "60d5ec49f1b2c72e4c8beeee",
"short_uid": "B001",
"docket_id": "60d5ec49f1b2c72e4c8bdddd",
"title": "Right to Equality - Key Provisions",
"conte": "U2FsdGVkX1+xyz789...",
"is_deleted": false
}
]
}
],
"duration_in_mins": 30,
"course_id": 1,
"creation_params": {
"number_of_mcqs": 20,
"duration_in_mins": 30,
"test_mode": 2,
"explanation_mode": 3,
"explanation_detail_level": 2,
"mcq_algorithm": 3,
"mcq_selection_filters": { ... }
},
"sort_order": 5,
"l1_taxonomy_ids": ["60d5ec49f1b2c72e4c8b0001"],
"status": 2,
"message": null,
"l1_tax_ids": ["60d5ec49f1b2c72e4c8b0001"]
},
"error": null,
"app_actions": null
}
| Field | Description |
|---|---|
status | Always LIVE=2 on creation |
message | Present when fewer MCQs than requested, e.g. “You requested 20 but we only found 15 unattempted MCQs” |
mcqs | Full MCQ details in v1 MCQ detail shape (same as /mcqs/sync) |
l1_taxonomy_ids / l1_tax_ids | Unique L1 root taxonomy IDs across all MCQs in this test |
Error Responses
| Error Code | Condition |
|---|---|
| 1003 | Invalid parameters (V3 MCQ count out of range, distribution doesn’t sum to 100) |
| 6906 | No MCQs found matching the selection filters |
| 6909 | Free custom test limit exceeded |
| 6910 | Daily custom test limit exceeded |
POST /custom-tests/{custom_test_id}/discard
Summary: Discard Custom Test
Discard a custom test without submission. Marks the test as finished (status changes from LIVE to DISCARDED).
Parameters
| Name | In | Required | Description |
|---|---|---|---|
custom_test_id | path | required | Custom test ID (ObjectId) |
course_id | query | required | Course ID |
x-dev-time | header | optional | Epoch ms (13 digits) |
Success Response
{
"status": "success",
"is_data_encrypted": 0,
"data": null,
"error": null,
"app_actions": null
}
Note: data is null (JSON null). Never encrypted even when feature flag is on.
Error Responses
| Error Code | Condition |
|---|---|
| 6900 | Custom test not found |
| 1010 | Test already submitted or discarded (OPERATION_NOT_ALLOWED) |
POST /custom-tests/{custom_test_id}/submission
Summary: Submit Custom Test
Submit answers for a custom test. Calculates results with scoring, analytics, and percentile distribution.
Parameters
| Name | In | Required | Description |
|---|---|---|---|
custom_test_id | path | required | Custom test ID (ObjectId) |
course_id | query | required | Course ID |
x-dev-time | header | optional | Epoch ms (13 digits) |
Request Body
{
"answers": {
"60d5ec49f1b2c72e4c8b4567": "option_2",
"60d5ec49f1b2c72e4c8b4568": "option_1",
"60d5ec49f1b2c72e4c8b4569": -1,
"60d5ec49f1b2c72e4c8b456a": "option_3"
},
"started_at": 1714400000000,
"ended_at": 1714401800000,
"silly_mistake_mcq_ids": ["60d5ec49f1b2c72e4c8b4568"],
"streak": 3,
"guessed_mcq_ids": ["60d5ec49f1b2c72e4c8b456a"],
"marked_for_review_mcq_ids": ["60d5ec49f1b2c72e4c8b4569"]
}
| Field | Required | Description |
|---|---|---|
answers | required | Dict: key = mcq_id (ObjectId), value = "option_1" through "option_4", or -1 for skipped |
started_at | optional | Test start time in epoch ms |
ended_at | optional | Test end time in epoch ms |
silly_mistake_mcq_ids | optional | MCQ IDs the user marked as silly mistakes |
streak | optional | Consecutive correct answers streak |
guessed_mcq_ids | optional | MCQ IDs the user indicated were guesses (defaults to empty list) |
marked_for_review_mcq_ids | optional | MCQ IDs marked for later review (defaults to empty list) |
Conditions:
- Answer values must be valid options (
"option_1"through"option_4") or-1; error1003if invalid - List values in answers are auto-flattened to first item (validator handles array-wrapped values)
Scoring
- +2 per correct answer
- -0.66 per wrong answer
- 0 for unattempted/skipped (
-1) - Total marks can be negative
Success Response
{
"status": "success",
"is_data_encrypted": 0,
"data": {
"id": "60d5ec49f1b2c72e4c8bffff",
"total_correct_count": 12,
"marks": "21.36",
"total_mcq_count": 20,
"streak": 3,
"duration_in_seconds": 1800,
"percentile_distribution": {
"0-10": 5,
"10-20": 12,
"20-30": 25,
"30-40": 35,
"40-50": 18,
"50-60": 3,
"60-70": 1,
"70-80": 1,
"80-90": 0,
"90-100": 0
},
"taxonomy_wise_scores": [
{
"taxonomy_id": "60d5ec49f1b2c72e4c8b0001",
"taxonomy_name": "Polity",
"total_count": 8,
"correct_count": 5,
"marks": "7.02"
},
{
"taxonomy_id": "60d5ec49f1b2c72e4c8b0002",
"taxonomy_name": "History",
"total_count": 12,
"correct_count": 7,
"marks": "14.34"
}
],
"custom_test_sort_order": 5
},
"error": null,
"app_actions": null
}
| Field | Description |
|---|---|
marks | Decimal string. Scoring: +2 correct, -0.66 wrong, 0 skipped. Can be negative |
duration_in_seconds | Calculated from ended_at - started_at. 0 if timestamps not provided |
percentile_distribution | Distribution of user scores across percentile ranges |
taxonomy_wise_scores | Per-subject breakdown of performance |
Error Responses
| Error Code | Condition |
|---|---|
| 6900 | Custom test not found |
| 1010 | Test already submitted or discarded (OPERATION_NOT_ALLOWED) |
| 1003 | Invalid answer values |
GET /custom-tests/sync
Summary: Sync Custom Tests by Updated At
Retrieve a paginated list of custom tests (lightweight, without MCQ details).
Parameters
| Name | In | Required | Constraints | Description |
|---|---|---|---|---|
course_id | query | required | CourseEnum | Course ID |
limit | query | optional | 1-120, default 10 | Number of results |
next_cursor | query | optional | base64 string | Forward pagination cursor |
prev_cursor | query | optional | base64 string | Backward pagination cursor |
x-dev-time | header | optional | 13 digits | Epoch ms |
Success Response
{
"status": "success",
"is_data_encrypted": 0,
"data": [
{
"id": "60d5ec49f1b2c72e4c8b9999",
"short_uid": "CT00456",
"number_of_mcqs": 20,
"updated_at": 1714401800000,
"status": 3,
"created_at": 1714400000000,
"is_deleted": false,
"sort_order": 5,
"marks": "21.36",
"l1_tax_ids": ["60d5ec49f1b2c72e4c8b0001", "60d5ec49f1b2c72e4c8b0002"]
},
{
"id": "60d5ec49f1b2c72e4c8b8888",
"short_uid": "CT00455",
"number_of_mcqs": 10,
"updated_at": 1714300000000,
"status": 1,
"created_at": 1714300000000,
"is_deleted": false,
"sort_order": 4,
"marks": null,
"l1_tax_ids": ["60d5ec49f1b2c72e4c8b0001"]
}
],
"error": null,
"app_actions": null,
"pagination": {
"next_cursor": "eyJ1cGRhdGVkX2F0IjoxNzE0MzAwMDAwMDAwLCJpZCI6IjYwZDVlYzQ5ZjFiMmM3MmU0YzhiODg4OCJ9",
"prev_cursor": null,
"limit": 10,
"has_more": false
}
}
| Field | Description |
|---|---|
status | DISCARDED=1, LIVE=2, SUBMITTED=3 |
marks | Decimal string when submitted; null when not yet submitted. Can be negative |
GET /custom-tests/{custom_test_id}
Summary: Get Custom Test By Id Or Short Id
Retrieve the full custom test including all MCQ details. When submitted, each MCQ includes the user’s selected option.
Parameters
| Name | In | Required | Description |
|---|---|---|---|
custom_test_id | path | required | Custom test ObjectId or short_uid string |
course_id | query | required | Course ID |
x-dev-time | header | optional | Epoch ms (13 digits) |
Success Response (submitted test)
{
"status": "success",
"is_data_encrypted": 0,
"data": {
"id": "60d5ec49f1b2c72e4c8b9999",
"short_uid": "CT00456",
"mcqs": [
{
"id": "60d5ec49f1b2c72e4c8b4567",
"short_uid": "M00123",
"question": "Which article of the Indian Constitution deals with the Right to Equality?",
"question_format": 1,
"option_1": "Article 12",
"option_2": "Article 14",
"option_3": "Article 19",
"option_4": "Article 21",
"correct_option": "option_2",
"question_type": 1,
"year": 2023,
"taxonomy_ids": ["60d5ec49f1b2c72e4c8b0001", "60d5ec49f1b2c72e4c8b0002", "60d5ec49f1b2c72e4c8b0003"],
"selected_option": "option_2",
"block_details": [ ... ]
}
],
"duration_in_mins": 30,
"result": {
"id": "60d5ec49f1b2c72e4c8bffff",
"total_correct_count": 12,
"marks": "21.36",
"total_mcq_count": 20,
"streak": 3,
"duration_in_seconds": 1800,
"percentile_distribution": { ... },
"taxonomy_wise_scores": [ ... ],
"custom_test_sort_order": 5
},
"submission": {
"id": "60d5ec49f1b2c72e4c8beeee",
"answers": {
"60d5ec49f1b2c72e4c8b4567": "option_2",
"60d5ec49f1b2c72e4c8b4568": "option_1",
"60d5ec49f1b2c72e4c8b4569": "-1"
},
"started_at": 1714400000000,
"ended_at": 1714401800000,
"silly_mistake_mcq_ids": ["60d5ec49f1b2c72e4c8b4568"],
"streak": 3,
"guessed_mcq_ids": [],
"marked_for_review_mcq_ids": ["60d5ec49f1b2c72e4c8b4569"]
},
"creation_params": {
"number_of_mcqs": 20,
"duration_in_mins": 30,
"test_mode": 2,
"mcq_algorithm": 3
},
"sort_order": 5,
"l1_taxonomy_ids": ["60d5ec49f1b2c72e4c8b0001", "60d5ec49f1b2c72e4c8b0002"],
"status": 3,
"l1_tax_ids": ["60d5ec49f1b2c72e4c8b0001", "60d5ec49f1b2c72e4c8b0002"]
},
"error": null,
"app_actions": null
}
Notes:
mcqs[].selected_optionis present only when test is submitted. Contains"option_1"through"option_4","-1"for skipped, ornullresultisnullwhen test is LIVE or DISCARDEDsubmissionisnullwhen test is LIVE or DISCARDEDsubmission.answersvalues are always strings (even-1becomes"-1")
Error Responses
| Error Code | Condition |
|---|---|
| 6900 | Custom test not found |