SageOx Firmware OTA API
The Firmware OTA API from SageOx — 1 operation(s) for firmware ota.
The Firmware OTA API from SageOx — 1 operation(s) for firmware ota.
openapi: 3.1.0
info:
title: SageOx Admin Firmware OTA API
version: 1.0.0
description: "# API Reference\n\n## Overview\n\nSageOx is a platform that captures team knowledge from discussions, decisions, and work context into a Ledger (per-repo historical record) and Team Context (team-wide shared knowledge). The SageOx API provides programmatic access to manage repositories, recordings, notifications, and team data with high performance and reliability.\n\n## Base URLs\n\n| Environment | URL |\n|---|---|\n| Local Development | `http://localhost:3000` |\n| Test | `https://test.sageox.ai` |\n| Production | `https://sageox.ai` |\n\n## Authentication\n\nAll requests to the SageOx API require authentication via JWT tokens issued by the Better Auth service.\n\nInclude your token in the `Authorization` header:\n```\nAuthorization: Bearer <token>\n```\n\n**Initial Auth Flow (CLI)**\n- CLI initiates device flow to obtain initial credentials\n- Exchange device code for JWT token\n- Store token securely for subsequent API requests\n- Refresh token when expired\n\n**Authenticated Endpoints**\n- Pass JWT in `Authorization: Bearer <token>` header\n- Tokens contain user identity and team membership\n- Invalid or expired tokens return 401 Unauthorized\n\n## Response Format\n\n### Success\nStandard response format with optional pagination metadata:\n```json\n{\n \"success\": true,\n \"data\": { ... }\n}\n```\n\n### Error\nAll errors follow a consistent format:\n```json\n{\n \"success\": false,\n \"error\": \"Human-readable error description\"\n}\n```\n\n**Status Codes**\n- `400` — Bad Request: Invalid parameters or malformed request\n- `401` — Unauthorized: Missing or invalid authentication token\n- `403` — Forbidden: Insufficient permissions for this resource\n- `404` — Not Found: Resource does not exist\n- `409` — Conflict: Request conflicts with current state (e.g., duplicate)\n- `429` — Rate Limited: Too many requests; retry with backoff\n- `500` — Internal Error: Server error; contact support if persistent\n\n## Pagination\n\nList endpoints support cursor-based pagination via query parameters:\n\n**Query Parameters**\n- `limit` — Number of results per page (default: 20, max: 100)\n- `offset` — Number of results to skip (default: 0)\n\n**Response Metadata**\nList responses include pagination info:\n```json\n{\n \"success\": true,\n \"data\": [ ... ],\n \"meta\": {\n \"total\": 150,\n \"limit\": 20,\n \"offset\": 0,\n \"hasMore\": true\n }\n}\n```\n\n## ID Formats\n\nResources use standardized ID formats for easy identification:\n\n| Resource | Format | Length | Example |\n|---|---|---|---|\n| Team | `team_<cuid2>` | 10 chars | `team_abc1234567` |\n| User | `usr_<cuid2>` | 10 chars | `usr_xyz9876543` |\n| Repository | `repo_<uuidv7>` | 36 chars | `repo_01934f5a-8b9c-7def-abcd-0123456789ab` |\n| Recording | `rec_<uuidv7>` | 36 chars | `rec_01934f5a-8b9c-7def-abcd-0123456789ab` |\n| Image | `img_<uuidv7>` | 36 chars | `img_01934f5a-8b9c-7def-abcd-0123456789ab` |\n| Notification | `notif_<cuid2>` | 14 chars | `notif_ab1cd2ef3g` |\n\nUse these IDs for all API operations. IDs are unique across environments.\n\n## Rate Limiting\n\nThe API applies rate limiting to protect against abuse and ensure fair access:\n\n**Authenticated Endpoints**\n- Limited per user: depends on subscription tier\n- Quota resets hourly\n\n**Unauthenticated Endpoints**\n- Limited per IP address\n- More restrictive than authenticated limits\n\nWhen rate limited, the API returns `429 Too Many Requests`. Retry requests with exponential backoff:\n```\nwait = min(2^attempt * 100ms, 10s)\n```\n\n## Timestamps\n\nAll timestamps in API responses use RFC 3339 / ISO 8601 format in UTC:\n```\n2025-12-03T10:30:00Z\n```\n\nUse this format when providing timestamps in requests as well. The API rejects timestamps in other formats.\n\n## SDKs & Tools\n\n- **OpenAPI Spec** — Full schema available at `/api/openapi.json`\n- **Postman Collection** — Import from `/api/postman.json` for interactive exploration\n- **CLI** — Use `ox` CLI for command-line access\n"
contact:
name: SageOx Team
license:
name: MIT
servers:
- url: http://localhost:3000
description: Devcontainer
- url: https://test.sageox.ai
description: Test
- url: https://sageox.ai
description: Production
security:
- bearerAuth: []
tags:
- name: Firmware OTA
paths:
/api/v1/firmware/latest:
get:
operationId: getFirmwareManifest
summary: Check for firmware updates
description: 'Device-facing endpoint that returns the latest available firmware manifest
for the specified device type. The device polls this endpoint periodically
and compares the returned version against its current firmware.
This endpoint is **unauthenticated** (devices have no JWT) but rate-limited
by X-Device-ID header. Returns 204 No Content when no update is available.
The response `url` field contains a pre-signed S3 URL (15-minute TTL) for
direct firmware binary download. The device fetches this URL directly with
no auth headers.
'
tags:
- Firmware OTA
security: []
parameters:
- name: device
in: query
required: true
description: Hardware type identifier (e.g., `scribe`, `clip`).
schema:
type: string
example: scribe
- name: version
in: query
required: true
description: Currently running firmware version (semver format).
schema:
type: string
example: 1.0.0
- name: channel
in: query
required: false
description: Release channel to check. Defaults to `stable`.
schema:
type: string
default: stable
enum:
- stable
- early-access
- beta
- nightly
- name: X-Device-ID
in: header
required: false
description: Hardware-derived stable identifier. For scribe, this is the 12-character lowercase hex of the ESP32 base MAC address.
schema:
type: string
example: aabbccddeeff
- name: X-Device-Type
in: header
description: Hardware type, same as the `device` query parameter. Informational.
schema:
type: string
example: scribe
- name: User-Agent
in: header
description: Device firmware user agent string.
schema:
type: string
example: sageox-scribe/1.0.0 (esp32-s3; xtensa-lx7)
responses:
'200':
description: A newer firmware version is available.
content:
application/json:
schema:
type: object
required:
- version
- url
description: 'Firmware manifest returned to devices. Field sizes are constrained by
the ESP32 firmware parser buffer sizes.
'
properties:
version:
type: string
description: Semver version string. Must be newer than the device's current version.
maxLength: 31
example: 1.2.0
url:
type: string
description: Pre-signed S3 URL for direct firmware binary download (15-minute TTL, no auth required).
maxLength: 511
sha256:
type: string
description: SHA-256 hash of the firmware binary, lowercase hex.
pattern: ^[0-9a-f]{64}$
example: a3f1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1
signature:
type: string
description: Base64-encoded RSA-2048 signature over the SHA-256 digest. Required for production builds.
maxLength: 511
size:
type: integer
format: int64
description: Firmware binary size in bytes. Used for progress bar and hash verification.
changelog:
type: string
description: Human-readable changelog. Devices with small screens truncate on their end.
maxLength: 2048
release_date:
type: string
format: date
description: ISO-8601 release date.
min_version:
type: string
description: Security floor. Devices below this version get a forced update (no skip option).
maxLength: 31
'204':
description: No update available. Device is up to date or not targeted.
'400':
description: Missing required query parameters.
'429':
description: Rate limit exceeded. Back off and retry.
'500':
description: Server error.
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'JWT token obtained from the auth service (/api/auth/token).
Token is validated using JWKS from the auth service.
Required claims: sub (user_id), email, name, tier.
'