Firmware Manifest API Contract

Stable interface between the ESP32 firmware OTA client and the Vercel-hosted web app.

Download .md

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, in vYYYYMMDDHHMM format.

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 like v202510020734.

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 binarySize in 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

FieldTypeNotes
versionstringvYYYYMMDDHHMM. Lexically sortable — string compare works.
releaseDatestringISO 8601 with timezone. Apollo Beach local time recommended.
binaryUrlstringAbsolute HTTPS URL. Must be on same origin or CORS-allowed.
binarySizeintegerBytes. Used for download progress display on device.
binarySha256string64-char lowercase hex. MUST verify before flashing.
minSourceVersionstring | nullIf non-null, devices on older versions should chain through this version first.
mandatorybooleanIf true, device UI should not present a Skip button. Reserve for security-critical fixes.
titlestringMax 60 chars. Shown on device update prompt.
summarystringMax 200 chars. Shown on device update prompt below title.
releaseNotesUrlstringFull release notes web page. Shown as QR code on device.
deprecatedbooleanIf 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 drops

Versioning 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.
  • minSourceVersion is for rare cases where intermediate state migration is required (e.g., NVS schema change). Use sparingly.
  • deprecated: true is 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:

  1. HTTPS only. Vercel forces it. Firmware must validate cert (no setInsecure() in production).
  2. SHA-256 verification. Manifest carries the hash; firmware verifies bytes before flashing. Defeats both bad-binary and basic MITM.
  3. ESP32 dual-partition rollback. If a flashed binary fails to boot, ESP32 reverts to previous partition automatically. Built-in to Update.h.
  4. mandatory: false by 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.bin

End 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.