DDI-1 Contract — Device Direct Interface v1

Status: Frozen — DDI v1.0

DDI-1 is the wire contract between a device’s firmware and the FlexGalaxy device platform. A conforming device speaks two protocols to a single endpoint host and implements the surfaces below. The contract is frozen: wire-breaking changes go to a new major version (DDI v2), and additive changes within v1 (new optional fields, new error codes, new event types) never break a conforming device — see §10 Versioning. Firmware MUST ignore unknown fields.

Note

This is the device-integrator view of the frozen DDI v1.0 wire contract — the surface a firmware developer builds against. It is a stable external presentation; the versioning policy in §10 governs how it changes. Report an ambiguity or a suspected discrepancy through your integration contact.

Tip

Building firmware? Start with the device SDKs — the polyglot device SDKs (Python, Android/Java, C, C++) implement this contract for you. This page is the underlying wire reference the SDKs conform to.

1. Endpoints + transport

A device speaks two protocols to one endpoint host. Per-deployment values (host, CA chain) are configured at bootstrap-burn time — firmware MUST NOT hardcode hostnames.

Protocol

Port

TLS

Used for

HTTPS (HTTP/2 + HTTP/3)

443

mTLS with device cert

Provisioning, credential rotation, command ack, OTA polling, diagnostic-bundle upload, time-sync

MQTT 5.0

8883

mTLS with device cert

Telemetry, attribute report + ack, command receive, durable events, heartbeat, log streaming

v1 firmware MUST implement HTTPS + MQTT. Telemetry, attribute reports, events, command-receive, and shared-attribute push exist only on the MQTT surface (§4); there is no HTTPS carrier for them in v1. A constrained device MAY implement HTTPS only, forgoing the §4 capabilities (see the capability matrix).

Endpoint hostname convention: things.<deployment-domain> (e.g. things.flexgalaxy.ai for the managed environment). Configured at burn time.

2. Identity + authentication

2.1 Before first identify

A factory-burned device starts with:

  • Attestation token — issued by the factory at burn time; an opaque bearer (~256 bits) presented as Authorization: Bearer <attestation> on the first POST /ddi/v1/identify. Per-device or per-batch, at the factory’s discretion.

  • TLS trust anchors — the platform root CA + deployment intermediate CA, baked into firmware at burn time; used to verify the endpoint’s TLS chain. Rotated via firmware update.

  • No device certificate — issued during provisioning (§3.2).

2.2 After provisioning

  • Device X.509 certificate + private key (private key generated on-device; CSR sent, signed cert returned). Subject CN = <serial>, O = <registrar-name>, OU = <model>, SAN URI = urn:fgai:thing:<thing_id>. Valid 1 year.

  • CA chain — presented alongside the device cert in mTLS handshakes.

mTLS with the device cert is the sole auth mechanism for all post-provisioning traffic (HTTPS + MQTT). The attestation token is invalidated once provisioning succeeds.

2.3 Certificate rotation

Devices SHOULD rotate ≥ 30 days before expiry via POST /ddi/v1/rotate (§3.3). A device MUST NOT rotate within 1 hour of its last successful rotation (loop guard).

3. HTTPS surface — /ddi/v1/*

Request bodies are application/json; success responses are application/json, errors are application/problem+json (RFC 9457). Every endpoint accepts an Idempotency-Key header (RFC 9529) — 24-hour dedup window.

3.1 POST /ddi/v1/identify — first-boot identification

Auth: attestation token. Idempotent: yes (24h, keyed by model + sn + key).

Request:

{
  "model": "FlexSwift MAX",
  "sn": "SR202606180087",
  "registrar_hint": "syrius-robotics",
  "attestation_method": "factory-burn",
  "firmware_version": "1.4.2",
  "hardware_revision": "B"
}

Success (200):

{
  "thing_id": "uuid",
  "enrollment_token": "opaque-32-byte-base64",
  "provision_endpoint": "/ddi/v1/provision",
  "server_time": "2026-06-22T14:32:11.500Z",
  "metadata": { "registrar_id": "uuid", "factory_service_id": "uuid" }
}

Errors: DDI-1-403-NOT-RECOGNIZED (serial not a registered factory pattern), DDI-1-403-IDENTITY-BLACKLISTED (terminal — stop retrying, halt provisioning), DDI-1-422-UNKNOWN-MODEL.

3.2 POST /ddi/v1/provision — provisioning

Auth: attestation token + enrollment_token from §3.1. Idempotent: yes.

Production pattern (required for production firmware): the device generates a keypair on-device (TPM / secure enclave), sends a CSR, and receives a signed cert

  • CA chain. The private key never leaves the device.

Request (CSR pattern):

{
  "thing_id": "uuid",
  "enrollment_token": "from /identify response",
  "csr_pem": "-----BEGIN CERTIFICATE REQUEST-----\n...",
  "device_attributes": {
    "mac_addresses": ["02:1a:2b:3c:4d:5e"],
    "tpm_attestation": "optional-base64",
    "uptime_seconds": 17
  }
}

Success (200):

{
  "cert_pem": "-----BEGIN CERTIFICATE-----\n...",
  "ca_chain_pem": "-----BEGIN CERTIFICATE-----\n...",
  "expires_at": "2027-06-22T14:32:11Z",
  "serial_number": "0x1a2b...",
  "thing_id": "uuid",
  "registrar_id": "uuid",
  "shared_config": {
    "mqtt_endpoint": "mqtts://things.flexgalaxy.ai:8883",
    "polling_interval_seconds": 30,
    "telemetry_sample_rate_hz": 1
  }
}

After success the device switches to mTLS with the new cert and discards the attestation token.

3.3 POST /ddi/v1/rotate — credential rotation

Auth: current device mTLS. Idempotent: yes; rate-limited to 1/hour per device.

Body { "csr_pem": "..." }. Response has the same shape as /provision but omits shared_config. The old cert stays valid for a 24-hour overlap window.

3.4 GET /ddi/v1/time — time-sync fallback

Auth: mTLS (post-provision) or attestation (pre-provision). Returns { "server_time": "...", "server_time_epoch_ms": 1782489131500 }. Devices SHOULD prefer standard SNTP; every other HTTPS response also carries server_time in the body or the standard Date: header, so a dedicated /time call is rarely needed.

3.5 GET /ddi/v1/ota/* — OTA polling

Auth: mTLS. The endpoint speaks the Eclipse hawkBit DDI wire format unchanged — this contract does not redefine it. Firmware SHOULD use the Eclipse Hara reference client.

3.6 POST /ddi/v1/bundles — diagnostic-bundle upload (chunked)

Auth: mTLS. Resumable, chunked upload:

  1. POST /ddi/v1/bundles → create session {kind, content_type, estimated_size, captured_from, captured_to}{upload_id, chunk_size, presigned_post}.

  2. PUT /ddi/v1/bundles/{upload_id}/chunks/{n} → sequential chunks.

  3. POST /ddi/v1/bundles/{upload_id}/complete{sha256, actual_size}{bundle_id, state: "available"}.

  4. POST /ddi/v1/bundles/{upload_id}/abort → cancel.

Chunk at ≤ 4 MiB; resume from the last successful chunk on failure.

3.7 POST /ddi/v1/commands/{command_id}/ack — command acknowledgment

Auth: mTLS. Idempotent: yes. Commands arrive via MQTT (§4); the device acks over HTTPS:

{
  "state": "received | executing | succeeded | failed",
  "result_payload": {},
  "error": null,
  "completed_at": "2026-06-22T14:35:00Z"
}

3.8 POST /ddi/v1/sentry-ingest/* — error-reporting ingest

Auth: mTLS. A Sentry-compatible ingest prefix — point the standard Sentry SDK at it. Auth translation is handled platform-side; from the device it just works.

4. MQTT 5.0 surface — device/<thing_id>/*

MQTT over TLS on port 8883 with the device cert as the client cert. CONNECT username = thing_id, password empty (the cert is the credential). MQTT 5 user properties carry correlation IDs.

Note

Payload encoding. Every MQTT payload is the binary protobuf serialization of the corresponding fgai.device.v1 message. The JSON shown below is a logical-schema illustration of the fields, not the wire format — the wire is protobuf. (HTTPS bodies in §3 remain JSON.)

QoS: 0 for telemetry + heartbeat; 1 for attribute reports, events, and command receive/ack. QoS 2 is never used — QoS 1 with receiver-side dedup is the canonical pattern.

Topic

Direction

QoS

Purpose

device/<thing_id>/telemetry

device → cloud

0

Time-series values + optional geo block

device/<thing_id>/attribute/client

device → cloud

1

CLIENT-bucket attributes (device-only-writable)

device/<thing_id>/attribute/shared/push

cloud → device

1

SHARED-bucket updates (subscribe on connect)

device/<thing_id>/attribute/shared/ack

device → cloud

1

Ack a SHARED update after applying it

device/<thing_id>/commands

cloud → device

1

Command dispatch (subscribe on connect); ack via §3.7

device/<thing_id>/events

device → cloud

1

Durable, structured, high-value events

device/<thing_id>/heartbeat

device → cloud

0

Presence (recommend 30s; missed > 3 → OFFLINE)

Telemetry (values free-form, SI units preferred; optional canonical geo block):

{
  "ts": "2026-06-22T14:32:11.500Z",
  "values": { "battery_pct": 87.3, "temp_c": 28.5, "rpm_left": 421 },
  "geo": { "lat": 37.7749, "lng": -122.4194, "alt_m": 12.3, "speed_mps": 1.8, "source": "gnss" }
}

CLIENT attribute report — full snapshot on every new connection, deltas on change:

{ "attributes": { "firmware_version": "1.4.2", "boot_count": 142 }, "reported_at": "2026-06-22T14:32:11Z", "wire_message_id": "uuid" }

SHARED push (subscribe) → apply locally → ack:

{ "key": "telemetry_sample_rate_hz", "value_json": 2, "version": 18, "set_at": "2026-06-22T14:32:00Z", "wire_message_id": "uuid" }

Command (subscribe) — execute, then ack via §3.7:

{ "command_id": "uuid", "command": "upload_logs_bundle", "payload": {}, "deadline": "2026-06-22T14:45:00Z", "idempotency_key": "uuid" }

Event:

{ "event_id": "uuid", "event_type": "alarm.collision_avoided", "severity": "warning", "ts": "2026-06-22T14:32:11Z", "payload": {}, "wire_message_id": "uuid" }

5. Lifecycle

Device state

Wire interactions

Rolled off

Boot bootstrap firmware; POST /ddi/v1/identify with the attestation token

Provisioned

Received cert from /provision; open MQTT on 8883; subscribe to shared-push + commands; publish the initial CLIENT attribute snapshot

Activated

Normal operation — telemetry / events / heartbeat flow per §4

Deactivated

Cert revoked; the next mTLS attempt fails with TLS alert certificate_revoked. Halt cloud comms after N consecutive failures + retry with backoff (a cert may be re-issued via §3.3 if re-activated)

Annihilated

DDI-1-403-IDENTITY-BLACKLISTEDterminal; stop retrying

6. Idempotency + retries

Every state-mutating call accepts Idempotency-Key (HTTPS) or wire_message_id (MQTT). Dedup window = 24h. Recommended retry policy:

  • Network errors / HTTP 5xx: exponential backoff, full jitter, base 1s, max 60s, max 8 attempts.

  • HTTP 4xx (except 408/429): do NOT retry.

  • HTTP 429: respect Retry-After.

  • DDI-1-403-IDENTITY-BLACKLISTED: do NOT retry (terminal).

  • MQTT disconnect: reconnect with backoff; resubscribe to shared-push + commands; republish the CLIENT snapshot.

7. Error codes

application/problem+json responses carry error_code of the form DDI-1-<HTTP_STATUS>-<KIND>. The v1 enumeration (additions are non-breaking):

Code

HTTP

Meaning

DDI-1-400-MALFORMED-BODY

400

Body not valid JSON or required fields missing

DDI-1-401-NO-CREDENTIAL

401

mTLS cert or attestation token missing

DDI-1-401-INVALID-CERT

401

mTLS cert signature invalid or chain broken

DDI-1-403-NOT-RECOGNIZED

403

Identity not a registered factory pattern

DDI-1-403-IDENTITY-BLACKLISTED

403

Identity annihilated — terminal

DDI-1-403-CERT-REVOKED

403

Device cert revoked (deactivated)

DDI-1-409-IDEMPOTENCY-CONFLICT

409

Same Idempotency-Key, different body

DDI-1-422-UNKNOWN-MODEL

422

Model not in the registered catalog

DDI-1-422-MALFORMED-CSR

422

CSR cannot be parsed or its signature is invalid

DDI-1-429-RATE-LIMITED

429

Per-device rate limit exceeded; respect Retry-After

DDI-1-503-BACKEND-UNAVAILABLE

503

Backend temporarily unavailable; retry with backoff

8. Time + clock

  • All ts / reported_at / set_at / applied_at fields are ISO 8601 UTC, millisecond precision.

  • Every HTTPS response includes a standard Date: header.

  • Devices with an RTC SHOULD sync via SNTP; DDI-1 time endpoints are a fallback.

  • Devices without SNTP MUST consume server_time or Date: on each response to hold a clock baseline.

9. Conformance

Firmware certification requires a green conformance run: 39 scenarios across 14 categories (identify, provision, rotate, MQTT connect, telemetry, attributes, commands, events, heartbeat, bundles, OTA, time, and each enumerated error code). Conformance is per-profile — a device asserts its profile (below) and only the relevant scenarios run.

10. Versioning

DDI-1 is DDI v1, frozen. Wire-breaking changes go to DDI v2 (new path prefix /ddi/v2/*, new MQTT topic prefix); v1 and v2 coexist during a deprecation window (target 12 months). Non-breaking changes within v1 — new optional response fields, new error codes, new event types — MAY land without a version bump and are recorded in the revision history. Devices MUST ignore unknown JSON fields. Deprecated fields are marked Deprecated for 6+ months before removal.

11. Capability matrix — device profiles

Capability

Full (AMR / robot)

Embedded (sensor / actuator)

Constrained (battery / LoRa)

HTTPS provisioning (§3.1–§3.3)

required

required

required

MQTT telemetry (§4)

required

required

required if telemetry reported

Attributes (§4)

required

required

client-only, requires MQTT

Commands (§4 + §3.7)

required

optional

requires MQTT to receive; ack via HTTPS

Events (§4)

required

required

requires MQTT if used

Heartbeat (§4)

required

required

required

Logs (§4)

required

optional

none (use error-reporting ingest §3.8)

Bundles (§3.6)

required

optional

none

OTA hawkBit DDI (§3.5)

required

required

required

Error-reporting ingest (§3.8)

required

required

required (low-volume)

Time / SNTP (§8)

SNTP preferred

SNTP preferred

Date: header