# Route Spec

## Route ID
`auth-otp-verify`

## Endpoint
`POST /api/v1/auth/otp/verify`

## Human Description
Verifies the 4-digit OTP entered by the user and creates an authenticated session if valid.

## Authentication
- Required: `no`
- Auth type: `none`
- Required roles/scopes: `none`

## Request
### Headers
- `Content-Type: application/json`

### Body
```json
{
  "challengeId": "otp_ch_8f2a1b",
  "otpCode": "4812",
  "device": {
    "deviceId": "dev_abcd1234",
    "platform": "android"
  }
}
```

### Validation Rules
- `challengeId`: required.
- `otpCode`: required, exactly 4 digits.
- `device.deviceId`: required.
- `device.platform`: required, enum `ios|android`.

## Responses
### Success: `200 OK`
When returned:
- OTP is correct and not expired.

Body:
```json
{
  "success": true,
  "message": "OTP verified",
  "data": {
    "accessToken": "jwt_access_token",
    "refreshToken": "jwt_refresh_token",
    "accessTokenExpiresAt": "2026-02-18T13:34:56Z",
    "isNewUser": true,
    "onboarding": {
      "nextStep": "profile_basics"
    }
  }
}
```

### Error: `400 Bad Request`
When returned:
- OTP invalid, expired, or challenge already consumed.

Body:
```json
{
  "success": false,
  "error": {
    "code": "OTP_INVALID_OR_EXPIRED",
    "message": "The code is invalid or has expired.",
    "details": {
      "remainingAttempts": 2
    }
  }
}
```

### Error: `429 Too Many Requests`
When returned:
- Too many verification attempts were made in the current rate-limit window.

Body:
```json
{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Please try again later.",
    "details": {}
  }
}
```

## Data & Caching Dependencies
- **Spanner Tables:** `users, user_identities (Read/Write)`
- **Redis Cache:** `otp_challenges (Read/Delete), refresh_tokens (Write)`
- **GCS Storage:** `None`
- **Edge Cache (CDN):** `No`

## Side Effects
- Marks OTP challenge as used.
- Creates login session and refresh token pair.

## Idempotency and Retries
- Idempotent: `no`
- Retry guidance: retry only with a new OTP if consumed/expired.
