towerops/MOBILE_API.md

12 KiB

Towerops Mobile API Documentation

Version: 1.0 Base URL: https://your-towerops-instance.com/api/v1/mobile

Table of Contents

Authentication

The Towerops Mobile API uses bearer token authentication. All authenticated requests must include an Authorization header:

Authorization: Bearer <session_token>

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:

{
  "token": "base64-encoded-token-from-qr-code"
}

Response (200 OK):

{
  "valid": true,
  "user_email": "user@example.com"
}

Response (401 Unauthorized):

{
  "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:

{
  "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):

{
  "session_token": "long-lived-bearer-token",
  "expires_at": "2026-04-15T19:44:25Z",
  "user": {
    "id": "user-uuid",
    "email": "user@example.com"
  }
}

Response (401 Unauthorized):

{
  "error": "Invalid or expired token"
}

Response (422 Unprocessable Entity):

{
  "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 <session_token>

Response (200 OK):

{
  "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 <session_token>

Response (200 OK):

{
  "success": true
}

Data Endpoints

All data endpoints require authentication via the Authorization: Bearer <token> header.

Organizations

List Organizations

Endpoint: GET /api/v1/mobile/organizations

Returns all organizations the authenticated user has access to.

Response (200 OK):

{
  "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):

{
  "sites": [
    {
      "id": "site-uuid",
      "name": "Main Office",
      "location": "New York, NY",
      "equipment_count": 5,
      "equipment_down_count": 1
    }
  ]
}

Response (403 Forbidden):

{
  "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):

{
  "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):

{
  "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):

{
  "error": "Equipment not found"
}

Response (403 Forbidden):

{
  "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):

{
  "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

{
  "error": "Human-readable error message"
}

For validation errors (422):

{
  "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)

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)

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<SessionToken>(response.body()!!.string())
        sessionToken = result.session_token
        return result
    }

    // Fetch organizations
    suspend fun fetchOrganizations(): List<Organization> {
        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<OrganizationsResponse>(response.body()!!.string())
        return result.organizations
    }
}

Support

For API support, bug reports, or feature requests:

Changelog

Version 1.0 (2026-01-15)

  • Initial release
  • QR code authentication
  • Basic data endpoints (organizations, sites, equipment, alerts)
  • Session management