Source: Generated from the OpenAPI 3.1.0 spec and Keystone backend source code.
For the live interactive spec, run the Keystone backend and visit /docs.
API Title: Keystone API - Client APIs (v1)
Version: 1.0.0
OpenAPI: 3.1.0
Servers
| Environment | URL |
|---|
| Staging | https://core-staging.cybosapiens.com/v1 |
| Ngrok (local/interview) | https://joint-mongrel-really.ngrok-free.app/api/v1 |
| Local | http://localhost:8080/v1 |
Authentication
Most endpoints require a Bearer token (access_token) in the Authorization header.
Public endpoints (no token required): GET /health, POST /auth/send-email-otp, POST /auth/verify-email-otp, POST /auth/login/apple/mobile, POST /auth/login/google/mobile, POST /webhooks/razorpay.
Protected route dependency chain (executed in strict order):
- Device Time Validation -
x-dev-time header validated within +/-120s tolerance
- Pre-Auth Header Validation - checks Authorization, device identifiers
- JWT Authentication - verifies Bearer token, checks platform/application against allowed lists, validates session is active and not revoked
- Endpoint Logic - the actual route handler
HTTP Status: Always 200
All responses return HTTP 200, regardless of success or failure. The actual status is in the JSON body’s status field ("success" or "failure"). Validation errors, business logic errors, auth errors - all return 200 with structured error details.
Response Wrapper
Every response uses BaseResponse<T>:
{
"status": "success",
"is_data_encrypted": 0,
"data": {
"id": "507f1f77bcf86cd799439011",
"name": "Example Resource"
},
"error": null,
"app_actions": null
}
Success Response
{
"status": "success",
"is_data_encrypted": 0,
"data": { ... },
"error": null,
"app_actions": null
}
Error Response
{
"status": "failure",
"is_data_encrypted": 0,
"data": null,
"error": {
"code": 1003,
"message": "Use either year or taxonomy_id, not both."
},
"app_actions": null
}
Paginated Response
Paginated endpoints add a pagination field:
{
"status": "success",
"is_data_encrypted": 0,
"data": [ ... ],
"error": null,
"app_actions": null,
"pagination": {
"next_cursor": "eyJ1cGRhdGVkX2F0IjoxNzE0NDAwMDAwMDAwLCJpZCI6IjUwN2YxZjc3YmNmODZjZDc5OTQzOTAxMSJ9",
"prev_cursor": null,
"limit": 10,
"has_more": true
}
}
Response Encryption
When encryption is enabled (FEATURE_FLAG_V1_CLIENT_WHOLE_PAYLOAD_ENCRYPTION_ENABLED=true):
Protected-route responses encrypt the data field with CryptoV2.1 (secret=device_id, salt=user_id from Bearer token sub claim).
{
"status": "success",
"is_data_encrypted": 1,
"data": "6C76576S4h8Sk/2J+lD9oW0KqH7sZxEwT5mN...",
"error": null,
"app_actions": null
}
Special cases:
- Auth/token endpoints (login, OTP, refresh) - always plaintext regardless of flag
data: null responses (e.g. discard test) - always is_data_encrypted: 0
is_data_encrypted values:
0 = NONE (plaintext)
1 = V2_1 (response encryption)
2 = V2_2 (request encryption)
Request Encryption
When the whole-payload flag is true, clients send { "encrypted_data": "..." } (CryptoV2.2, secret=device_id, salt=x_dev_time). Query params stay plaintext. Exclusions: Google/Apple mobile login, health, webhooks.
AppActions Middleware
The middleware validates app headers and may inject app_actions into responses:
Required headers (mobile clients):
App-Course-Name (e.g. “stemma-upsc”)
App-Platform (e.g. “android”, “ios”)
App-Build-Version (integer)
App-OS-Version (float)
Action types (priority order):
VERSION_BLOCK - immediate failure response, blocks request
MAINTENANCE - informs user, allows request
FORCE_UPDATE - recommends update, allows request
Example VERSION_BLOCK response:
{
"status": "failure",
"is_data_encrypted": 0,
"data": null,
"error": {
"message": "Access blocked due to application version restrictions.",
"code": 200
},
"app_actions": {
"action_code": "VERSION_BLOCK",
"min_version": 120,
"message": "Please update your app to continue."
}
}
All sync endpoints use timestamp+id cursors for stable, forward-only pagination:
- Client sends
next_cursor from previous response (omit for first page)
- Backend decodes cursor to extract
updated_at + id
- Query:
WHERE updated_at > cursor.updated_at OR (updated_at == cursor.updated_at AND id > cursor.id)
- Fetches
limit + 1 items; if more than limit, has_more=true
- Response includes
next_cursor encoded as base64(json({updated_at, id}))
Error Codes Reference
All errors return HTTP 200 with status: "failure".
Generic/Common (1000-1011)
| Code | Name | Description |
|---|
| 1000 | UNKNOWN | Unexpected error |
| 1001 | INVALID_REQUEST | Malformed request |
| 1002 | MISSING_PARAMETERS | Required field missing |
| 1003 | INVALID_PARAMETERS | Invalid field value or mutually exclusive params |
| 1004 | DUPLICATE_ENTRY | Resource already exists |
| 1005 | RESOURCE_NOT_FOUND | Requested resource not found |
| 1006 | VALIDATION_FAILED | Schema/model validation failed |
| 1008 | OPERATION_FAILED | Operation could not complete |
| 1010 | OPERATION_NOT_ALLOWED | Action not permitted in current state |
Authentication (2000-2008)
| Code | Name | Description |
|---|
| 2000 | AUTH_FAILED | Authentication failed |
| 2001 | INVALID_CREDENTIALS | Wrong credentials |
| 2002 | INVALID_OTP | OTP is incorrect |
| 2003 | OTP_EXPIRED | OTP has expired |
| 2005 | SSO_ERROR | Apple/Google SSO failure |
| 2008 | OTI_EXPIRED | OTP request identifier expired |
Authorization (2500-2504)
| Code | Name | Description |
|---|
| 2500 | UNAUTHORIZED | Not authenticated |
| 2501 | FORBIDDEN | Authenticated but not allowed |
Session & Token (3000-3005)
| Code | Name | Description |
|---|
| 3000 | TOKEN_EXPIRED / TOKEN_INVALID / TOKEN_REVOKED | JWT token issue |
| 3003 | REFRESH_TOKEN_EXPIRED | Refresh token expired |
| 3004 | ENCRYPTION_FAILED | Response encryption error |
| 3005 | DECRYPTION_FAILED | Request decryption error |
User Account (3500-3709)
| Code | Name | Description |
|---|
| 3500 | USER_NOT_FOUND | User does not exist |
| 3506 | USER_UPDATE_FAILED | User update operation failed |
| 3708 | USER_PHONE_NUMBER_ALREADY_REGISTERED | Phone already linked to another account |
Custom Tests (6900-6910)
| Code | Name | Description |
|---|
| 6900 | CUSTOM_TEST_NOT_FOUND | Test does not exist |
| 6906 | CUSTOM_TEST_NO_MCQS_FOUND_DURING_CREATION | No MCQs match the filters |
| 6909 | CUSTOM_TEST_FREE_LIMIT_EXCEEDED | Free test limit reached |
| 6910 | CUSTOM_TEST_DAILY_LIMIT_EXCEEDED | Daily test limit reached |
Orders & Payments (9200-9223)
| Code | Name | Description |
|---|
| 9200 | ORDER_NOT_FOUND | Order does not exist |
| 9204 | COUPON_INVALID_FOR_PLAN | Coupon not valid for this plan |
| 9205 | COUPON_USAGE_LIMIT_EXCEEDED | Coupon max usage reached |
| 9206 | PLAN_NOT_FOUND | Plan does not exist |
| 9207 | PLAN_INACTIVE | Plan is not active |
| 9219 | COUPON_USER_LIMIT_EXCEEDED | Per-user coupon limit reached |
| 9221 | PLAN_TRIAL_ALREADY_ACTIVE_FOR_COURSE | User already has active trial |
| 9222 | TRIAL_SUBSCRIPTION_NOT_ELIGIBLE | User not eligible for trial |
| 9223 | COUPON_PLATFORM_NOT_ALLOWED | Coupon restricted to different platform |
Endpoint Summary (37 total)
Infrastructure
| Method | Path | Summary |
|---|
| GET | /health | Health Check |
| POST | /webhooks/razorpay | Razorpay Webhook |
Auth
| Method | Path | Summary |
|---|
| POST | /auth/send-email-otp | Request OTP |
| POST | /auth/verify-email-otp | Verify OTP |
| POST | /auth/login/apple/mobile | Apple Mobile Login |
| POST | /auth/login/google/mobile | Google Login Mobile |
| POST | /auth/refresh | Refresh Token |
| POST | /auth/logout | Logout |
| GET | /auth/me | Get Current User |
User
| Method | Path | Summary |
|---|
| GET | /users/mcq-daily/sync | Sync User MCQ Daily Stats |
| DELETE | /users | Delete User |
| PATCH | /users/info | Update User Info |
| POST | /users/profile-picture | Upload Profile Picture |
| POST | /users/send-phone-otp | Initiate Phone Number Validation |
| POST | /users/verify-phone-otp | Verify Phone Number Validation |
| POST | /users/report | Create User Report |
MCQ
| Method | Path | Summary |
|---|
| GET | /mcqs/sync | List PYQ MCQs by Updated At (sync) |
| GET | /mcqs/batch | Get MCQs by IDs |
MCQ Actions
| Method | Path | Summary |
|---|
| POST | /mcqs_attrs/reactions | Like/Dislike MCQ |
| GET | /mcqs_attrs/sync | List MCQ Reactions |
| POST | /mcqs_attrs/bookmark | Bookmark MCQ |
| POST | /mcqs_attrs/attempt | Attempt MCQ |
Bookmark Collections
| Method | Path | Summary |
|---|
| POST | /bookmark-collections | Create Bookmark Collection |
| GET | /bookmark-collections/sync | Sync Bookmark Collections |
| PATCH | /bookmark-collections/{collection_id} | Update Bookmark Collection |
| DELETE | /bookmark-collections/{collection_id} | Delete Bookmark Collection |
Custom Tests
| Method | Path | Summary |
|---|
| POST | /custom-tests | Create Custom Test |
| POST | /custom-tests/{custom_test_id}/discard | Discard Custom Test |
| POST | /custom-tests/{custom_test_id}/submission | Submit Custom Test |
| GET | /custom-tests/sync | Sync Custom Tests by Updated At |
| GET | /custom-tests/{custom_test_id} | Get Custom Test By Id Or Short Id |
Payments
| Method | Path | Summary |
|---|
| GET | /plans | List Plans |
| POST | /orders | Create Order |
| POST | /orders/verify-payment | Verify Razorpay Payment (primary) |
| POST | /orders/{order_id}/initiate-payment | Initiate Payment |
| GET | /payments | List Billing History |
| POST | /coupons/validate | Validate Coupon Code |
Sync
| Method | Path | Summary |
|---|
| GET | /mcqs/sync | List PYQ MCQs by Updated At (sync) |
| GET | /tags/sync | Sync Tags by Updated At |
| GET | /dockets/sync | Sync Dockets by Updated At (Lightweight) |
| GET | /taxonomy/sync | Sync Taxonomies by Updated At |
| GET | /years/sync | Sync Years by Updated At |
| GET | /custom-tests/sync | Sync Custom Tests by Updated At |
| GET | /bookmark-collections/sync | Sync Bookmark Collections |
| GET | /mcqs_attrs/sync | List MCQ Reactions |
Common Enums
| Enum | Values |
|---|
LoginPlatformEnum | web, ios, ios_t, an, an_t |
LoginApplicationEnum | wri, prx, arivu |
MobileLoginPlatformEnum | ios, ios_t, an, an_t |
CourseEnum | 80085 (TEST), 1 (UPSC), 2 (STATE_PSC), 3 (SSC_CGL), 10 (NEET), 11 (AIIMS), 12 (JIPMER), 13, 20-23, 30-32, 40-42 |
QuestionTypeEnum | PYQ=1, DQ=2, EQ=3 |
QuestionContentFormatEnum | SIMPLE_MCQ=1, ADVANCED_MCQ=2 |
GenderEnum | NON_BINARY=0, MALE=1, FEMALE=2, OTHER=3 |
PlatformEnum | IOS=1, ANDROID=2, WEB=3 |
PublishingStatusEnum | DRAFT=1, PUBLISHED=2, UNPUBLISHED=3, ARCHIVE=4 |
PlanTypeEnum | PUBLIC=1, PRIVATE=2, GIFT=3, TRIAL=4 |
PlanStatusEnum | INACTIVE=0, ACTIVE=1 |
BookmarkStatusEnum | BOOKMARKED=1, NOT_BOOKMARKED=2 |
MCQReactionTypeEnum | LIKE=1, DISLIKE=2, NONE=3 |
CustomTestModeEnum | STUDY=1, EXAM=2, MULTI_PLAYER=3 |
CustomTestStatusEnum | DISCARDED=1, LIVE=2, SUBMITTED=3 |
CustomTestMCQExplanationModeEnum | ALL=1, WRONG_ONLY=2, SHOW_AT_END=3 |
CustomTestMCQExplanationDetailLevelEnum | SHORT=1, FULL=2 |
McqAlgorithmEnum | V1=1, V2=2, V3=3 |
CustomTestMCQSelectionTypeEnum | ADAPTIVE=1, MANUAL=2 |
DataEncryptionEnum | NONE=0, V2_1=1, V2_2=2 |
ResponseStatus | success, failure |