Firmware Manifest API Contract
Stable interface between the ESP32 firmware OTA client and the Vercel-hosted web app.
Purpose
Lock this contract down early so both the firmware and web sides can be built in parallel.
Base URL
https://datawalker.app (replace with actual production domain)
Format
JSON over HTTPS. UTF-8. No authentication required for read endpoints.
Endpoints
GET /api/firmware/latest
Returns the manifest for the latest non-deprecated firmware release.
Response 200 (application/json):
{
"version": "v202605221130",
"releaseDate": "2026-05-22T11:30:00-04:00",
"binaryUrl": "https://datawalker.app/firmware/data-walker-v202605221130.bin",
"binarySize": 1287456,
"binarySha256": "3a7f9c8e2b1d4a5f6c0e9d8b7a6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c",
"minSourceVersion": "v202510020734",
"mandatory": false,
"title": "Daily Calibration Feature",
"summary": "Adds boot-time daily calibration walk with bidirectional 3 m measurement.",
"releaseNotesUrl": "https://datawalker.app/releases/v202605221130",
"deprecated": false
}Headers:
Content-Type: application/json
Cache-Control: public, max-age=300
Access-Control-Allow-Origin: *GET /api/firmware/check?current=vYYYYMMDDHHMM
Lightweight "is there an update?" call. Preferred over /latest when the device just wants to know if it needs to do anything.
Query parameters:
current(required) — the version string currently running on the device, invYYYYMMDDHHMMformat.
Response 200, update available:
{
"hasUpdate": true,
"manifest": { ...same shape as /latest response... }
}Response 200, no update available:
{
"hasUpdate": false
}Response 400 (malformed current):
{
"error": "invalid_version",
"message": "Expected format vYYYYMMDDHHMM"
}GET /api/firmware/[version]
Returns the manifest for a specific historical version. Useful for rollback or pinning.
Path parameter:
version— version string likev202510020734.
Response 200:
Same shape as /latest.
Response 404:
{
"error": "not_found",
"message": "No firmware manifest for version v202401010000"
}GET /firmware/data-walker-vYYYYMMDDHHMM.bin
Direct binary download. Served as static asset from Vercel's CDN.
Response 200:
- Content-Type:
application/octet-stream - Content-Length: bytes (matches
binarySizein manifest) - ETag and Last-Modified headers for caching
Important
The firmware client MUST verify binarySha256 from the manifest against the SHA-256 of the downloaded bytes before flashing. Mismatch = abort and keep current firmware.
Field Reference
| Field | Type | Notes |
|---|---|---|
| version | string | vYYYYMMDDHHMM. Lexically sortable — string compare works. |
| releaseDate | string | ISO 8601 with timezone. Apollo Beach local time recommended. |
| binaryUrl | string | Absolute HTTPS URL. Must be on same origin or CORS-allowed. |
| binarySize | integer | Bytes. Used for download progress display on device. |
| binarySha256 | string | 64-char lowercase hex. MUST verify before flashing. |
| minSourceVersion | string | null | If non-null, devices on older versions should chain through this version first. |
| mandatory | boolean | If true, device UI should not present a Skip button. Reserve for security-critical fixes. |
| title | string | Max 60 chars. Shown on device update prompt. |
| summary | string | Max 200 chars. Shown on device update prompt below title. |
| releaseNotesUrl | string | Full release notes web page. Shown as QR code on device. |
| deprecated | boolean | If true, devices running this version should see a persistent warning. |
Firmware-side Flow (Reference)
On boot, after WiFi connection succeeds:
GET /api/firmware/check?current={VERSION_NUMBER}
if response.hasUpdate == false:
proceed to normal operation
if response.hasUpdate == true:
show update prompt screen:
- Title and summary from manifest
- "Update Now" / "Later" / "Never" buttons
- QR code for releaseNotesUrl
- If mandatory: no Skip option
on "Update Now":
- Connect HTTPClient to binaryUrl (verify HTTPS cert)
- Stream to Update.h, computing SHA-256 incrementally
- On EOF: verify SHA-256 matches manifest.binarySha256
- If match: Update.end() and ESP.restart()
- If mismatch: Update.abort(), display error, keep current firmware
on "Later":
- Skip for this session, prompt again next boot
on "Never":
- Store dismissed version to NVS
- Don't prompt again for this specific version
- Still prompt when an even newer version dropsVersioning Rules
- Lexical comparison works as long as you stick to
vYYYYMMDDHHMM. No need for semver. - Never reuse a version string. Even for hotfixes — bump the minute.
minSourceVersionis for rare cases where intermediate state migration is required (e.g., NVS schema change). Use sparingly.deprecated: trueis a soft signal. It does not force a downgrade or block boot; it warns the user. Reserve for versions with known clinical-data-affecting bugs.
Security Considerations
Current threat model is low-to-moderate. Devices are in clinical settings, behind clinic WiFi. The realistic threats are accidental — bad binary, MITM via misconfigured router, downgrade attack.
Built-in protections in this design:
- HTTPS only. Vercel forces it. Firmware must validate cert (no
setInsecure()in production). - SHA-256 verification. Manifest carries the hash; firmware verifies bytes before flashing. Defeats both bad-binary and basic MITM.
- ESP32 dual-partition rollback. If a flashed binary fails to boot, ESP32 reverts to previous partition automatically. Built-in to
Update.h. mandatory: falseby default. Clinician has the option to defer. Important for clinics with maintenance windows.
Not in this design (defer to Phase D+):
- Firmware signing (requires secure boot configuration on ESP32 — non-trivial)
- Per-device authentication (devices currently anonymous)
- Encrypted firmware payload (HTTPS in transit is sufficient for current threat model)
Future FDA Consideration
If the device ever moves from wellness-monitoring to FDA-regulated medical device territory, firmware signing becomes mandatory and this contract needs to add a binarySignature field with ECDSA over the binary.
Example: Seed firmware-manifest.json
This file lives in the Next.js project root and is read by the API routes. Replace the SHA-256 hashes with actual values after building each binary.
{
"manifests": [
{
"version": "v202605221130",
"releaseDate": "2026-05-22T11:30:00-04:00",
"binaryUrl": "https://datawalker.app/firmware/data-walker-v202605221130.bin",
"binarySize": 1287456,
"binarySha256": "PLACEHOLDER_HASH_REPLACE_AFTER_BUILD...",
"minSourceVersion": "v202510020734",
"mandatory": false,
"title": "Daily Calibration Feature",
"summary": "Adds boot-time daily calibration walk.",
"releaseNotesUrl": "https://datawalker.app/releases/v202605221130",
"deprecated": false
}
]
}To generate SHA-256 of a built binary:
sha256sum data-walker-v202605221130.bin
# or on macOS:
shasum -a 256 data-walker-v202605221130.binEnd of contract. Treat this as the source of truth for both sides of the OTA system. If something needs to change, change this doc first, then bump both implementations.