Skip to content

06 — Error Reference

All errors follow this structure:

{
  "error":   "ERROR_CODE",
  "message": "Human-readable description.",
  "status":  400
}

Some errors include additional fields (noted below).


Authentication & App Credentials

Code Status Cause Action
TOKEN_INVALID 401 Wrong app_id, wrong app_secret, or malformed JWT in Authorization header Verify credentials; ensure JWT is passed correctly
TOKEN_EXPIRED 401 Access token is older than 30 minutes Call /auth/refresh to get a new access token
TOKEN_REVOKED 401 Refresh token was explicitly revoked (user logged out) Ask user to log in again
TOKEN_REUSE 401 An already-rotated refresh token was replayed Session family revoked — user must log in again
USER_DELETED 403 User account has been soft-deleted Do not allow session; show appropriate message

OTP Errors

Code Status Cause Action
OTP_RATE_LIMITED 429 Too many OTPs sent to this contact in 10 minutes Check Retry-After header; wait before allowing another send
OTP_INVALID 400 OTP is incorrect Let user try again (up to 3 attempts total)
OTP_LOCKED 429 3 incorrect attempts — OTP is permanently locked Ask user to request a new OTP via send-otp
OTP_EXPIRED 400 OTP is older than 5 minutes Ask user to request a new OTP
OTP_ALREADY_USED 400 OTP has already been successfully verified Redirect user to log in again
OTP_WRONG_APP 403 otp_request_id was issued for a different app_id Ensure app_id matches across send and verify calls
OTP_WRONG_PURPOSE 403 purpose in verify doesn't match the one used in send Send and verify must use the same purpose
OTP_NOT_FOUND 404 otp_request_id does not exist Verify the ID is correct and not cleaned up

Extra fields on OTP_RATE_LIMITED and OTP_LOCKED:

{
  "error":       "OTP_RATE_LIMITED",
  "message":     "Too many OTP requests. Please wait before trying again.",
  "status":      429,
  "retry_after": 342
}
retry_after is in seconds. Use this to show a countdown in your UI.


License Key Errors

Code Status Cause Action
LICENSE_NOT_FOUND 404 Key doesn't exist or belongs to a different app Ask user to double-check the key they entered
LICENSE_INVALID 400 Key format is malformed (wrong length, bad characters) Validate format before sending; guide user to re-enter
LICENSE_REVOKED 403 Key was revoked by an admin Show "License revoked" message; contact support
LICENSE_EXPIRED 403 Key is past its expires_at date Show expiry message; offer renewal
LICENSE_NOT_ACTIVATED 403 Verify called but this device was never activated Send user through the activation flow first
LICENSE_MAX_ACTIVATIONS 403 All activation slots are used by other devices Show "Activation limit reached"; offer deactivation of another device
LICENSE_ALREADY_REVOKED 409 Revoke called on a key that was already revoked Idempotent — treat as success
LICENSE_ALREADY_ACTIVE 409 Activation slot already exists for this device Treat as success (idempotent re-activation)

Role Management Errors

Code Status Cause Action
TENANT_NOT_FOUND 404 Tenant doesn't exist or belongs to a different app Verify tenant_id is correct for your app_id
USER_NOT_FOUND 404 user_id does not exist in this app Verify user has completed OTP login first
ROLE_NOT_FOUND 404 No role exists for this user/tenant combination (on revoke) Safe to ignore — idempotent

Validation Errors

Code Status Cause
VALIDATION_ERROR 400 Missing required field or wrong format
{
  "error":   "VALIDATION_ERROR",
  "message": "Invalid request body.",
  "status":  400,
  "details": [
    { "field": "phone", "message": "Phone must be in E.164 format (e.g. +919876543210)" },
    { "field": "app_id", "message": "Must be a valid UUID" }
  ]
}

details lists every field that failed validation so you can surface them in your UI.


Infrastructure Errors

Code Status Cause Action
APP_NOT_FOUND 404 app_id UUID is not registered Check you are using the correct app_id
INTERNAL_ERROR 500 Unexpected server-side error Retry with exponential backoff; contact service owner if persistent

HTTP Status Code Summary

Status Meaning
200 Success
201 Created (new resource)
400 Bad request (validation error, wrong OTP, etc.)
401 Authentication required or credentials are wrong
403 Authenticated but not authorized
404 Resource not found
409 Conflict (duplicate, already exists)
429 Rate limited — check Retry-After header
500 Server error — retry with backoff
503 Service unavailable — retry with backoff

async function callAuthService(url: string, body: object) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });

  const data = await res.json();

  if (!res.ok) {
    switch (data.error) {
      case 'TOKEN_EXPIRED':
        // Trigger refresh flow
        await refreshAccessToken();
        // Retry original request
        break;
      case 'OTP_RATE_LIMITED':
        // Show countdown using data.retry_after
        showRetryCountdown(data.retry_after);
        break;
      case 'TOKEN_REUSE':
      case 'TOKEN_REVOKED':
      case 'USER_DELETED':
        // Force re-login
        clearSession();
        redirectToLogin();
        break;
      default:
        // Surface message to user
        showError(data.message);
    }
    return null;
  }

  return data;
}