# Towerops Mobile API Documentation Version: 1.0 Base URL: `https://your-towerops-instance.com/api/v1/mobile` ## Table of Contents - [Authentication](#authentication) - [QR Code Login Flow](#qr-code-login-flow) - [Session Management](#session-management) - [Data Endpoints](#data-endpoints) - [Organizations](#organizations) - [Sites](#sites) - [Equipment](#equipment) - [Alerts](#alerts) - [Error Handling](#error-handling) - [Rate Limiting](#rate-limiting) ## Authentication The Towerops Mobile API uses bearer token authentication. All authenticated requests must include an `Authorization` header: ``` Authorization: Bearer ``` ### QR Code Login Flow The recommended authentication method is QR code login, which allows users to authenticate without entering credentials in the mobile app. #### Step 1: User Generates QR Code User logs into the web interface at `/mobile/qr-login` and generates a QR code containing a temporary token. #### Step 2: Verify QR Token **Endpoint:** `POST /api/v1/mobile/auth/qr/verify` Verifies that a scanned QR token is valid before proceeding with login. **Request Body:** ```json { "token": "base64-encoded-token-from-qr-code" } ``` **Response (200 OK):** ```json { "valid": true, "user_email": "user@example.com" } ``` **Response (401 Unauthorized):** ```json { "valid": false, "error": "Invalid or expired token" } ``` #### Step 3: Complete QR Login **Endpoint:** `POST /api/v1/mobile/auth/qr/complete` Completes the QR login by creating a long-lived mobile session (90 days). **Request Body:** ```json { "token": "base64-encoded-token-from-qr-code", "device_name": "iPhone 15 Pro", "device_os": "iOS 17.2", "app_version": "1.0.0" } ``` **Response (200 OK):** ```json { "session_token": "long-lived-bearer-token", "expires_at": "2026-04-15T19:44:25Z", "user": { "id": "user-uuid", "email": "user@example.com" } } ``` **Response (401 Unauthorized):** ```json { "error": "Invalid or expired token" } ``` **Response (422 Unprocessable Entity):** ```json { "error": "Failed to create session", "details": { "device_name": ["can't be blank"] } } ``` **Notes:** - QR tokens expire after 5 minutes - QR tokens can only be used once - Session tokens expire after 90 days - Store the `session_token` securely in the device keychain/keystore ### Session Management #### Get Current Session **Endpoint:** `GET /api/v1/mobile/auth/session` **Headers:** ``` Authorization: Bearer ``` **Response (200 OK):** ```json { "id": "session-uuid", "device_name": "iPhone 15 Pro", "device_os": "iOS 17.2", "app_version": "1.0.0", "last_used_at": "2026-01-15T19:44:25Z", "expires_at": "2026-04-15T19:44:25Z" } ``` #### Revoke Session (Logout) **Endpoint:** `DELETE /api/v1/mobile/auth/session` **Headers:** ``` Authorization: Bearer ``` **Response (200 OK):** ```json { "success": true } ``` ## Data Endpoints All data endpoints require authentication via the `Authorization: Bearer ` header. ### Organizations #### List Organizations **Endpoint:** `GET /api/v1/mobile/organizations` Returns all organizations the authenticated user has access to. **Response (200 OK):** ```json { "organizations": [ { "id": "org-uuid", "name": "Acme Corp", "sites_count": 3, "equipment_count": 15, "active_alerts_count": 2 } ] } ``` ### Sites #### List Sites **Endpoint:** `GET /api/v1/mobile/organizations/:organization_id/sites` Returns all sites for an organization. **Response (200 OK):** ```json { "sites": [ { "id": "site-uuid", "name": "Main Office", "location": "New York, NY", "equipment_count": 5, "equipment_down_count": 1 } ] } ``` **Response (403 Forbidden):** ```json { "error": "Access denied to this organization" } ``` ### Equipment #### List Equipment **Endpoint:** `GET /api/v1/mobile/organizations/:organization_id/equipment` Returns all equipment for an organization with current status. **Query Parameters:** - `site_id` (optional): Filter by specific site UUID - `status` (optional): Filter by status (`up`, `down`, `unknown`) **Example:** ``` GET /api/v1/mobile/organizations/org-123/equipment?status=down&site_id=site-456 ``` **Response (200 OK):** ```json { "equipment": [ { "id": "equipment-uuid", "name": "Core Router", "ip_address": "192.168.1.1", "site_name": "Main Office", "status": "up", "uptime": "15d 4h", "last_seen_at": "2026-01-15T19:44:25Z" } ] } ``` #### Get Equipment Details **Endpoint:** `GET /api/v1/mobile/equipment/:id` Returns detailed equipment information including interfaces and sensors. **Response (200 OK):** ```json { "id": "equipment-uuid", "name": "Core Router", "ip_address": "192.168.1.1", "status": "up", "uptime": "15d 4h", "site": { "id": "site-uuid", "name": "Main Office" }, "interfaces": [ { "id": "interface-uuid", "name": "GigabitEthernet0/0", "alias": "WAN", "status": "up", "admin_status": "up", "speed": 1000000000, "mac_address": "00:1A:2B:3C:4D:5E" } ], "sensors": [ { "id": "sensor-uuid", "name": "CPU Usage", "type": "cpu", "unit": "%", "current_value": 45.5, "status": "ok" } ] } ``` **Response (404 Not Found):** ```json { "error": "Equipment not found" } ``` **Response (403 Forbidden):** ```json { "error": "Access denied to this equipment" } ``` ### Alerts #### List Alerts **Endpoint:** `GET /api/v1/mobile/organizations/:organization_id/alerts` Returns alerts for an organization. **Query Parameters:** - `severity` (optional): Filter by severity (`critical`, `warning`, `info`) - `status` (optional): Filter by status (`active`, `acknowledged`, `resolved`) - `limit` (optional): Number of alerts to return (default 50, max 200) **Example:** ``` GET /api/v1/mobile/organizations/org-123/alerts?severity=critical&status=active&limit=100 ``` **Response (200 OK):** ```json { "alerts": [ { "id": "alert-uuid", "severity": "critical", "status": "active", "message": "Equipment Down: Core Router", "equipment_name": "Core Router", "equipment_id": "equipment-uuid", "occurred_at": "2026-01-15T19:44:25Z" } ] } ``` **Alert Severity Levels:** - `critical`: Requires immediate attention (equipment down, critical sensor thresholds) - `warning`: Requires attention (sensor warnings, interface changes) - `info`: Informational (equipment up, sensor recovery) **Alert Status Values:** - `active`: Alert is active and unacknowledged - `acknowledged`: Alert has been acknowledged but not resolved - `resolved`: Alert has been resolved ## Error Handling All API endpoints follow consistent error response formats. ### HTTP Status Codes - `200 OK`: Request succeeded - `400 Bad Request`: Invalid request parameters - `401 Unauthorized`: Missing or invalid authentication token - `403 Forbidden`: Authenticated but not authorized for this resource - `404 Not Found`: Resource not found - `422 Unprocessable Entity`: Validation errors - `500 Internal Server Error`: Server error ### Error Response Format ```json { "error": "Human-readable error message" } ``` For validation errors (422): ```json { "error": "Failed to create session", "details": { "field_name": ["error message"] } } ``` ## Rate Limiting The API does not currently implement rate limiting, but it may be added in future versions. Implement exponential backoff in your client for failed requests. ## Best Practices ### Token Storage - Store session tokens securely in the device keychain (iOS) or keystore (Android) - Never log or expose tokens in plaintext - Clear tokens on logout ### Session Management - Check session expiration before making requests - Implement automatic token refresh or re-authentication when sessions expire - Handle 401 responses by prompting for re-authentication ### Network Requests - Implement timeout handling (30 seconds recommended) - Use exponential backoff for retries - Cache responses appropriately (organizations list, equipment details) - Update last_used_at is automatic - no need to ping ### Error Handling - Display user-friendly error messages - Log errors for debugging - Handle network connectivity issues gracefully - Implement offline mode where appropriate ## Example Client Implementation ### Swift (iOS) ```swift import Foundation class ToweropsAPI { private let baseURL = "https://your-instance.com/api/v1/mobile" private var sessionToken: String? // Authenticate with QR token func completeQRLogin(token: String, deviceInfo: DeviceInfo) async throws -> SessionToken { let url = URL(string: "\(baseURL)/auth/qr/complete")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body = [ "token": token, "device_name": deviceInfo.name, "device_os": deviceInfo.os, "app_version": deviceInfo.appVersion ] request.httpBody = try JSONEncoder().encode(body) let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw APIError.authenticationFailed } let result = try JSONDecoder().decode(SessionToken.self, from: data) self.sessionToken = result.session_token return result } // Fetch organizations func fetchOrganizations() async throws -> [Organization] { guard let token = sessionToken else { throw APIError.notAuthenticated } let url = URL(string: "\(baseURL)/organizations")! var request = URLRequest(url: url) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw APIError.requestFailed } let result = try JSONDecoder().decode(OrganizationsResponse.self, from: data) return result.organizations } } ``` ### Kotlin (Android) ```kotlin import okhttp3.* import kotlinx.serialization.json.Json class ToweropsAPI(private val baseURL: String = "https://your-instance.com/api/v1/mobile") { private val client = OkHttpClient() private var sessionToken: String? = null // Authenticate with QR token suspend fun completeQRLogin(token: String, deviceInfo: DeviceInfo): SessionToken { val json = """ { "token": "$token", "device_name": "${deviceInfo.name}", "device_os": "${deviceInfo.os}", "app_version": "${deviceInfo.appVersion}" } """.trimIndent() val request = Request.Builder() .url("$baseURL/auth/qr/complete") .post(RequestBody.create(MediaType.parse("application/json"), json)) .build() val response = client.newCall(request).execute() if (!response.isSuccessful) { throw APIException("Authentication failed") } val result = Json.decodeFromString(response.body()!!.string()) sessionToken = result.session_token return result } // Fetch organizations suspend fun fetchOrganizations(): List { val token = sessionToken ?: throw APIException("Not authenticated") val request = Request.Builder() .url("$baseURL/organizations") .header("Authorization", "Bearer $token") .build() val response = client.newCall(request).execute() if (!response.isSuccessful) { throw APIException("Request failed") } val result = Json.decodeFromString(response.body()!!.string()) return result.organizations } } ``` ## Support For API support, bug reports, or feature requests: - GitHub Issues: https://github.com/yourusername/towerops - Documentation: https://docs.towerops.net ## Changelog ### Version 1.0 (2026-01-15) - Initial release - QR code authentication - Basic data endpoints (organizations, sites, equipment, alerts) - Session management