Error Envelope

All error responses use this consistent shape:
{
  "error": "ERROR_CODE",
  "message": "Human-readable description",
  "status_code": 401
}
FieldTypeDescription
errorstringMachine-readable error code — use this in app logic (switch on this field)
messagestringHuman-readable description — safe to show developers; may be too technical for end users
status_codeintegerMirrors the HTTP status code
Always use the error field in your switch statement, not the message field. Messages may be updated for clarity; error codes are stable API contracts.

400 — Bad Request

Client sent a malformed or invalid request body.
CodeCauseResolution
VALIDATION_ERRORRequest body failed validation (missing required field, wrong type)Check the message field — it names the specific field and constraint
INVALID_DATE_FORMATdob is not in YYYY-MM-DD format or is a future dateSend date as "1990-05-15"
NUMBER_OF_DAYS_OUT_OF_RANGEnumber_of_days is < 0 or > 365Use 0 for all history or 1–365 for a specific window
INVALID_PHONEPhone number format not recognisedSend 10-digit Indian mobile number (with or without +91 prefix)
IMMUTABLE_FIELDAttempted to update phone or aadhaar via the profile endpointUse the dedicated KYC endpoints for identity field changes
MISSING_REQUIRED_FIELDA required field is absent from the request bodyAdd the missing field
Example:
{
  "error": "VALIDATION_ERROR",
  "message": "email: must be a valid email address",
  "status_code": 400
}

401 — Unauthorized

Authentication failed or token is invalid.
CodeCauseResolution
INVALID_OTPOTP is wrong, already used, or the session doesn’t existLet user retry; trigger a new OTP if attempts are exhausted
OTP_EXPIREDOTP session is older than 10 minutesTrigger a new OTP
TOO_MANY_OTP_ATTEMPTS5 failed OTP attempts on one sessionTrigger a new OTP (previous session is permanently invalidated)
INVALID_TOKENJWT is missing, malformed, expired, or revokedAttempt token refresh via /auth/token/refresh; redirect to login if refresh fails
MISSING_TOKENAuthorization header is absent on a protected endpointAdd Authorization: Bearer <access_token> header
Example:
{
  "error": "INVALID_TOKEN",
  "message": "Access token has expired",
  "status_code": 401
}

403 — Forbidden

Authenticated but not authorized to access this resource.
CodeCauseResolution
FORBIDDENUser does not have permission for this action (e.g. accessing another user’s data)Verify the resource belongs to the authenticated user

404 — Not Found

Resource does not exist or is not accessible to the authenticated user.
CodeCauseResolution
RESOURCE_NOT_FOUNDThe ID in the path does not exist, or belongs to a different userVerify the ID; the API never reveals whether a resource exists for privacy reasons
INVALID_REQUEST_URLThe path does not match any registered routeCheck the path and HTTP method against the API reference
SUMMARY_NOT_READYThe Nama Agent has not yet generated a clinical summary for this sessionPoll again after a few seconds

405 — Method Not Allowed

Wrong HTTP verb used.
CodeCauseResolution
INVALID_HTTP_METHODHTTP method not supported for this path (e.g. GET on a POST-only endpoint)Check the API reference for the correct HTTP method

409 — Conflict

Request conflicts with the current state of the resource.
CodeCauseResolution
RESOURCE_CONFLICTUnique constraint violation (e.g. duplicate record)The resource already exists — fetch it instead of creating
SESSION_ALREADY_LOCKEDAttempted to submit a doctor session that is already lockedThe session is complete; no further actions are possible

422 — Unprocessable Entity

Request was well-formed but semantically invalid.
CodeCauseResolution
VALIDATION_ERRORSemantic validation failed (e.g. future DOB, invalid occupation enum value)Fix the field value per the message

429 — Too Many Requests

Rate limit hit.
CodeCauseResolution
TOO_MANY_OTP_ATTEMPTSOTP brute-force protection triggered (5 failures)Trigger a new OTP; implement exponential back-off
RATE_LIMIT_EXCEEDEDGeneral API rate limit exceededWait for the rate limit window to reset; check Retry-After header
Response includes retry guidance:
{
  "error": "RATE_LIMIT_EXCEEDED",
  "message": "Too many requests. Please try again in 47 seconds.",
  "status_code": 429,
  "retry_after": 47
}

500 — Internal Server Error

Unexpected backend error.
CodeCauseResolution
INTERNAL_SERVER_ERRORUnexpected backend error — likely a bug or infrastructure issueRetry with exponential back-off; report if persistent with the X-Request-ID from your request
500 errors are logged with full context on the server side (including stack traces). They never expose internal details in the response body. If you encounter a persistent 500, share the X-Request-ID header value with the Aarokya team for log correlation.

When to Retry vs When to Show the User

Use this table to decide how to handle each error in your app:
ErrorRetry automatically?Show user-facing error?Action
INVALID_TOKENYes — attempt token refresh firstNo (unless refresh fails)Refresh tokens silently; show login only if refresh fails
OTP_EXPIREDNoYes”OTP has expired. Tap to get a new one.”
INVALID_OTPNo (let user re-enter)Yes”Incorrect OTP. X attempts remaining.”
TOO_MANY_OTP_ATTEMPTSNoYes”Too many attempts. Please request a new OTP.”
RATE_LIMIT_EXCEEDEDYes — after retry_after secondsNo (unless persists)Wait and retry silently
VALIDATION_ERRORNoDeveloper message onlyFix the request; show generic “something went wrong” to user
RESOURCE_NOT_FOUNDNoDepends on contextShow “not found” screen or navigate back
INTERNAL_SERVER_ERRORYes — up to 3 times with back-offAfter max retries”Something went wrong. Please try again.”
MISSING_TOKENNo — code bugNoFix the client code; always attach the token

Debugging Guide

Step 1: Check the error code

The error field is your primary signal. Match it against the tables above before looking at message.

Step 2: Use X-Request-ID for log correlation

Every request you send should include an X-Request-ID header with a UUID you generate:
curl https://api.aarokya.in/user/profile \
  -H 'Authorization: Bearer <token>' \
  -H 'X-Request-ID: 550e8400-e29b-41d4-a716-446655440000'
The server echoes this in the response header and logs it. When you contact support, provide the X-Request-ID — it pinpoints the exact request in server logs.

Step 3: Check the status_code

If you get an unexpected response shape (e.g. HTML instead of JSON), the status_code in the error envelope still tells you what happened. The API always returns JSON for expected error conditions — HTML usually means a load balancer or proxy rejected the request before it reached the backend.

Step 4: Common misconfigurations

SymptomLikely Cause
401 on every requestExpired access token — implement refresh logic
400 on OTP verifyPhone number format — try 9876543210 without +91
404 on wallet endpointsWallet not yet provisioned — check wallet_status first
500 on loginDatabase connection issue — check DB is running locally

Client-Side Error Handling

interface AarokyaError {
  error: string;
  message: string;
  status_code: number;
  retry_after?: number;
}

async function apiFetch(url: string, options: RequestInit): Promise<any> {
  const res = await fetch(url, options);

  if (!res.ok) {
    const err: AarokyaError = await res.json();

    switch (err.error) {
      case "INVALID_TOKEN":
      case "MISSING_TOKEN":
        // Attempt token refresh, then retry
        const refreshed = await attemptTokenRefresh();
        if (refreshed) return apiFetch(url, options);
        navigateTo("/login");
        return;

      case "OTP_EXPIRED":
        navigateTo("/auth/otp", { forceNew: true });
        return;

      case "TOO_MANY_OTP_ATTEMPTS":
        navigateTo("/auth/otp", { forceNew: true, showWarning: true });
        return;

      case "RATE_LIMIT_EXCEEDED":
        const retryAfter = err.retry_after ?? 30;
        await sleep(retryAfter * 1000);
        return apiFetch(url, options);

      case "INTERNAL_SERVER_ERROR":
        // Retry up to 3 times with exponential back-off
        for (let attempt = 1; attempt <= 3; attempt++) {
          await sleep(Math.pow(2, attempt) * 1000);
          try {
            return await apiFetch(url, options);
          } catch {}
        }
        throw new Error("Service temporarily unavailable");

      case "RESOURCE_NOT_FOUND":
        navigateTo("/not-found");
        return;

      default:
        throw new Error(err.message);
    }
  }

  return res.json();
}