Owner Abhishek P

V1 Client API Reference Overview

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

EnvironmentURL
Staginghttps://core-staging.cybosapiens.com/v1
Ngrok (local/interview)https://joint-mongrel-really.ngrok-free.app/api/v1
Localhttp://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):

  1. Device Time Validation - x-dev-time header validated within +/-120s tolerance
  2. Pre-Auth Header Validation - checks Authorization, device identifiers
  3. JWT Authentication - verifies Bearer token, checks platform/application against allowed lists, validates session is active and not revoked
  4. 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):

  1. VERSION_BLOCK - immediate failure response, blocks request
  2. MAINTENANCE - informs user, allows request
  3. 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."
  }
}

Cursor-Based Pagination (Sync Endpoints)

All sync endpoints use timestamp+id cursors for stable, forward-only pagination:

  1. Client sends next_cursor from previous response (omit for first page)
  2. Backend decodes cursor to extract updated_at + id
  3. Query: WHERE updated_at > cursor.updated_at OR (updated_at == cursor.updated_at AND id > cursor.id)
  4. Fetches limit + 1 items; if more than limit, has_more=true
  5. 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)

CodeNameDescription
1000UNKNOWNUnexpected error
1001INVALID_REQUESTMalformed request
1002MISSING_PARAMETERSRequired field missing
1003INVALID_PARAMETERSInvalid field value or mutually exclusive params
1004DUPLICATE_ENTRYResource already exists
1005RESOURCE_NOT_FOUNDRequested resource not found
1006VALIDATION_FAILEDSchema/model validation failed
1008OPERATION_FAILEDOperation could not complete
1010OPERATION_NOT_ALLOWEDAction not permitted in current state

Authentication (2000-2008)

CodeNameDescription
2000AUTH_FAILEDAuthentication failed
2001INVALID_CREDENTIALSWrong credentials
2002INVALID_OTPOTP is incorrect
2003OTP_EXPIREDOTP has expired
2005SSO_ERRORApple/Google SSO failure
2008OTI_EXPIREDOTP request identifier expired

Authorization (2500-2504)

CodeNameDescription
2500UNAUTHORIZEDNot authenticated
2501FORBIDDENAuthenticated but not allowed

Session & Token (3000-3005)

CodeNameDescription
3000TOKEN_EXPIRED / TOKEN_INVALID / TOKEN_REVOKEDJWT token issue
3003REFRESH_TOKEN_EXPIREDRefresh token expired
3004ENCRYPTION_FAILEDResponse encryption error
3005DECRYPTION_FAILEDRequest decryption error

User Account (3500-3709)

CodeNameDescription
3500USER_NOT_FOUNDUser does not exist
3506USER_UPDATE_FAILEDUser update operation failed
3708USER_PHONE_NUMBER_ALREADY_REGISTEREDPhone already linked to another account

Custom Tests (6900-6910)

CodeNameDescription
6900CUSTOM_TEST_NOT_FOUNDTest does not exist
6906CUSTOM_TEST_NO_MCQS_FOUND_DURING_CREATIONNo MCQs match the filters
6909CUSTOM_TEST_FREE_LIMIT_EXCEEDEDFree test limit reached
6910CUSTOM_TEST_DAILY_LIMIT_EXCEEDEDDaily test limit reached

Orders & Payments (9200-9223)

CodeNameDescription
9200ORDER_NOT_FOUNDOrder does not exist
9204COUPON_INVALID_FOR_PLANCoupon not valid for this plan
9205COUPON_USAGE_LIMIT_EXCEEDEDCoupon max usage reached
9206PLAN_NOT_FOUNDPlan does not exist
9207PLAN_INACTIVEPlan is not active
9219COUPON_USER_LIMIT_EXCEEDEDPer-user coupon limit reached
9221PLAN_TRIAL_ALREADY_ACTIVE_FOR_COURSEUser already has active trial
9222TRIAL_SUBSCRIPTION_NOT_ELIGIBLEUser not eligible for trial
9223COUPON_PLATFORM_NOT_ALLOWEDCoupon restricted to different platform

Endpoint Summary (37 total)

Infrastructure

MethodPathSummary
GET/healthHealth Check
POST/webhooks/razorpayRazorpay Webhook

Auth

MethodPathSummary
POST/auth/send-email-otpRequest OTP
POST/auth/verify-email-otpVerify OTP
POST/auth/login/apple/mobileApple Mobile Login
POST/auth/login/google/mobileGoogle Login Mobile
POST/auth/refreshRefresh Token
POST/auth/logoutLogout
GET/auth/meGet Current User

User

MethodPathSummary
GET/users/mcq-daily/syncSync User MCQ Daily Stats
DELETE/usersDelete User
PATCH/users/infoUpdate User Info
POST/users/profile-pictureUpload Profile Picture
POST/users/send-phone-otpInitiate Phone Number Validation
POST/users/verify-phone-otpVerify Phone Number Validation
POST/users/reportCreate User Report

MCQ

MethodPathSummary
GET/mcqs/syncList PYQ MCQs by Updated At (sync)
GET/mcqs/batchGet MCQs by IDs

MCQ Actions

MethodPathSummary
POST/mcqs_attrs/reactionsLike/Dislike MCQ
GET/mcqs_attrs/syncList MCQ Reactions
POST/mcqs_attrs/bookmarkBookmark MCQ
POST/mcqs_attrs/attemptAttempt MCQ

Bookmark Collections

MethodPathSummary
POST/bookmark-collectionsCreate Bookmark Collection
GET/bookmark-collections/syncSync Bookmark Collections
PATCH/bookmark-collections/{collection_id}Update Bookmark Collection
DELETE/bookmark-collections/{collection_id}Delete Bookmark Collection

Custom Tests

MethodPathSummary
POST/custom-testsCreate Custom Test
POST/custom-tests/{custom_test_id}/discardDiscard Custom Test
POST/custom-tests/{custom_test_id}/submissionSubmit Custom Test
GET/custom-tests/syncSync Custom Tests by Updated At
GET/custom-tests/{custom_test_id}Get Custom Test By Id Or Short Id

Payments

MethodPathSummary
GET/plansList Plans
POST/ordersCreate Order
POST/orders/verify-paymentVerify Razorpay Payment (primary)
POST/orders/{order_id}/initiate-paymentInitiate Payment
GET/paymentsList Billing History
POST/coupons/validateValidate Coupon Code

Sync

MethodPathSummary
GET/mcqs/syncList PYQ MCQs by Updated At (sync)
GET/tags/syncSync Tags by Updated At
GET/dockets/syncSync Dockets by Updated At (Lightweight)
GET/taxonomy/syncSync Taxonomies by Updated At
GET/years/syncSync Years by Updated At
GET/custom-tests/syncSync Custom Tests by Updated At
GET/bookmark-collections/syncSync Bookmark Collections
GET/mcqs_attrs/syncList MCQ Reactions

Common Enums

EnumValues
LoginPlatformEnumweb, ios, ios_t, an, an_t
LoginApplicationEnumwri, prx, arivu
MobileLoginPlatformEnumios, ios_t, an, an_t
CourseEnum80085 (TEST), 1 (UPSC), 2 (STATE_PSC), 3 (SSC_CGL), 10 (NEET), 11 (AIIMS), 12 (JIPMER), 13, 20-23, 30-32, 40-42
QuestionTypeEnumPYQ=1, DQ=2, EQ=3
QuestionContentFormatEnumSIMPLE_MCQ=1, ADVANCED_MCQ=2
GenderEnumNON_BINARY=0, MALE=1, FEMALE=2, OTHER=3
PlatformEnumIOS=1, ANDROID=2, WEB=3
PublishingStatusEnumDRAFT=1, PUBLISHED=2, UNPUBLISHED=3, ARCHIVE=4
PlanTypeEnumPUBLIC=1, PRIVATE=2, GIFT=3, TRIAL=4
PlanStatusEnumINACTIVE=0, ACTIVE=1
BookmarkStatusEnumBOOKMARKED=1, NOT_BOOKMARKED=2
MCQReactionTypeEnumLIKE=1, DISLIKE=2, NONE=3
CustomTestModeEnumSTUDY=1, EXAM=2, MULTI_PLAYER=3
CustomTestStatusEnumDISCARDED=1, LIVE=2, SUBMITTED=3
CustomTestMCQExplanationModeEnumALL=1, WRONG_ONLY=2, SHOW_AT_END=3
CustomTestMCQExplanationDetailLevelEnumSHORT=1, FULL=2
McqAlgorithmEnumV1=1, V2=2, V3=3
CustomTestMCQSelectionTypeEnumADAPTIVE=1, MANUAL=2
DataEncryptionEnumNONE=0, V2_1=1, V2_2=2
ResponseStatussuccess, failure