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) 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, region discovery, 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

The HTTPS origin requests and verifies a device certificate when one is presented, but permits a certificate-free TLS connection for first boot. Only identify, provision, and the public time fallback are usable without a device certificate. Rotation, discovery, OTA, and every other post-provision operation require a verified, non-revoked device certificate at the route boundary; an invalid or revoked presented certificate fails TLS rather than being treated as anonymous.

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.

Residency refusal — DDI-1-409-REGION-MISMATCH. An account selects a home region, and its devices provision on that region’s deployment. A /provision that would issue a certificate to this device on a deployment its account did not select is refused, terminally, and the refusal names the region the device must use instead:

{
  "type": "about:blank",
  "title": "Region mismatch",
  "status": 409,
  "error_code": "DDI-1-409-REGION-MISMATCH",
  "home_region": "ap-northeast-2",
  "plane_region": "ap-northeast-1",
  "mqtt_endpoint": "ssl://things.ap-northeast-2.flexgalaxy.ai:8883",
  "detail": "This account's selected home region is ap-northeast-2, so this device must provision on that region's deployment.",
  "instance": "/ddi/v1/provision"
}

home_region names the region this device belongs to — the account’s selected region for a first provisioning, or the region already recorded on its registration when a re-provision would move it — and it is the region the device must use. plane_region is the deployment that refused; mqtt_endpoint is the MQTT endpoint for home_region — the same value §3.9 answers for that region, so firmware may take it here or from discovery. It is always the platform’s region map answering for the region, never the refusing deployment’s own endpoint. The refusal is terminal (no Retry-After): it states where the account belongs, and no retry changes that.

An account that has selected no region uses the default region (ap-northeast-1), so a first provisioning for such a device on another deployment is this same refusal with home_region: "ap-northeast-1". And if the platform cannot determine the account’s selection at all, the answer is DDI-1-503-BACKEND-UNAVAILABLE — retryable — never a default region and never this refusal; “we could not ask” is not “no region was selected”.

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.

3.9 POST /ddi/v1/discover — device→region discovery

Auth: device mTLS. thing_id is taken from the verified client certificate (SAN URI urn:fgai:thing:<thing_id>) and never from the request — no request field is read, so a device cannot name itself, or its region, on this call. Idempotent: yes. Read-only: no cloud-side state is created or changed.

A device’s home region belongs to the device, and is answered from the device→region registration the platform holds for it — never from geography, and never from anything the device supplies. A device that has never moved and one whose record has been migrated ask the same question and each receives its own answer.

Request: {} — an empty JSON object. There is no field this operation reads.

Success (200):

{
  "thing_id": "uuid",
  "region": "ap-northeast-1",
  "mqtt_endpoint": "mqtts://things.flexgalaxy.ai:8883"
}

mqtt_endpoint here is the endpoint of the region the registration names — a different value from §3.2’s shared_config.mqtt_endpoint, which is the endpoint of the deployment the device provisioned against. The two agree for a device that has never moved and diverge for one that has; that divergence is the case this operation exists for.

Refusals, each with its own §7 code:

Code

HTTP

When

Retry (§6)

DDI-1-401-NO-CREDENTIAL

401

No verified client certificate

terminal

DDI-1-403-NOT-RECOGNIZED

403

The certificate’s identity cannot be resolved to a thing_id

terminal

DDI-1-404-NOT-REGISTERED

404

The registration service answered: no registration exists for this thing_id

terminal

DDI-1-403-REGION-ABSENT

403

The registration exists and names no region (an enrollment that did not complete). No default region is applied

terminal

DDI-1-503-REGION-NOT-SERVED

503

The record names a region this deployment has no MQTT endpoint for. The deployment never falls back to its own endpoint

retry with backoff; Retry-After present

DDI-1-503-ENDPOINT-MAP-UNUSABLE

503

This deployment’s region→endpoint map is unset or malformed, so it can answer for no device

retry with backoff; Retry-After present

DDI-1-503-BACKEND-UNAVAILABLE

503

The registration service did not answer, or could not be reached. “We could not ask” is never reported as “there is no such device”

retry with backoff; Retry-After present

A named region that is missing from the endpoint map is a REFUSAL, not a redirect. The tempting repair — answering with the endpoint of the deployment that received the request — would connect the device to the wrong region. Only the platform’s region→endpoint map may produce an endpoint on this call, and no refusal above carries one.

Reachability. This operation is served on the same endpoint host as every other DDI-1 call (§1), and a dedicated, globally stable name for it is in place: discovery.flexgalaxy.ai. It resolves to the same device-facing entry as things.flexgalaxy.ai — identical targets — so firmware may keep using the host configured at burn time, and the name does not change across platform rollouts. The device-facing certificate that terminates TLS for the name carries discovery.flexgalaxy.ai as a DNS name: a TLS client that verifies the name against the certificate authority that signs the device-facing certificate receives a successful verification (Verification: OK), while a name the certificate does not carry fails with a hostname mismatch. The client half of the handshake — a device presenting its own certificate and receiving an answer on this name — is not yet proven, and is the one open item here.

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.

What that means for delivery, stated because it is easy to read as stronger than it is. A QoS 0 publish is fire-and-forget: the broker acknowledges nothing, so a telemetry sample is at-most-once — it can be lost, and your device is never told. That is the deliberate choice for high-volume readings that the next sample supersedes. It follows that a successful publish call does not mean the reading was stored; at QoS 0 it means only that the sample was handed to the network. If a particular reading must not be lost, carry it on a QoS 1 message (attribute reports) or use an application-level acknowledgement — do not assume a guarantee here that the protocol does not give. The service side counts every telemetry record that does not reach storage, so loss is measurable rather than silent.

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-BLACKLISTED — terminal; 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-403-REGION-ABSENT

403

(§3.9) The device’s registration names no home region; terminal, and no default region is applied

DDI-1-404-NOT-REGISTERED

404

(§3.9) The registration service answered: no device→region registration for this thing_id; terminal

DDI-1-409-IDEMPOTENCY-CONFLICT

409

Same Idempotency-Key, different body

DDI-1-409-REGION-MISMATCH

409

(§3.2) The account’s selected home region is not the region serving this provisioning request; terminal, and it names the home_region, plane_region and mqtt_endpoint the device must use

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

DDI-1-503-REGION-NOT-SERVED

503

(§3.9) The device’s region has no MQTT endpoint on this deployment; the deployment never substitutes its own endpoint

DDI-1-503-ENDPOINT-MAP-UNUSABLE

503

(§3.9) This deployment’s region→endpoint map is unset or malformed, so it can answer for no device

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: 46 scenarios across 15 categories (identify, provision, rotate, discover, MQTT connect, telemetry, attributes, commands, events, heartbeat, logs, 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 every change to this contract, normative or otherwise, is recorded in the revision history with a date and a summary (ADR-0043 F-4). A wire-breaking change requires a new major version — DDI v2 — ratified by a new architecture decision before it lands. Devices MUST ignore unknown JSON fields. Deprecated fields are marked Deprecated for 6+ months before removal.

Revision history

Date

Change

2026-06-22

Initial draft.

2026-07-01

Frozen as DDI v1.0. §9 conformance taxonomy locked (39 scenarios across 14 categories); §11 capability matrix completed for all 3 profiles; §7 error enumeration closed for v1. Change-control ratified by ADR-0043.

2026-07-01

Relocated to the canonical DDI-1 contract home; frozen §1–§11 wire body unchanged.

2026-07-20

Deployment-status correction. Records the live gateway, time, edge blacklist, MQTT, and OTA paths; distinguishes reserved capabilities; corrects the managed hostname, HTTP/2 support, rotation section link, documentation URL, payload wording, and unified SDK ownership. No wire-breaking change.

2026-08-03

TLS policy clarification. §2 identity + authentication now records that the boundary permits a certificate-free TLS connection for first boot only, and that an invalid or revoked presented certificate FAILS TLS rather than being treated as anonymous. No wire-breaking change.

2026-09-28

Delivery-guarantee clarification. §4 now states what the QoS figures mean for delivery: a QoS 0 publish is acknowledged by nothing, so telemetry and heartbeat are at-most-once and a successful publish call does not mean the reading was stored. The guarantee the protocol already had, recorded rather than changed — no QoS value changed and no wire format changed.

2026-10-07

Additive operation: POST /ddi/v1/discover (§3.9) + four error codes. A new certificate-bound, read-only operation answers a device’s home region and the MQTT endpoint for it, derived from the platform’s device→region registration and never from the request. Its refusals carry distinct codes, so “the platform could not be asked” can never arrive as “there is no such device”; a region this deployment cannot map is refused rather than answered with the deployment’s own endpoint. §9’s locked taxonomy moves with it: 39→44 scenarios, 14→15 categories, and the error enumeration from 11 to 15 codes. No existing field, code, or operation changes — non-breaking per §10, with no DDI version bump.

2026-10-07

Discovery reachability. The dedicated, globally stable name for §3.9 — discovery.flexgalaxy.ai — is live in DNS and resolves to the same device-facing entry as the managed device host (identical targets), and the device-facing certificate carries that name, so a TLS client that verifies it receives a successful verification while a name the certificate does not carry fails with a hostname mismatch. The client half of the handshake — a device presenting its own certificate and receiving an answer on this name — is not yet proven. No operation, field, code, or wire change.

2026-10-09

New refusal: DDI-1-409-REGION-MISMATCH (§3.2). A /provision that would issue a certificate to a device on a deployment its account did not select is now refused terminally, and the refusal carries the region the device must use as a field (home_region), the refusing deployment (plane_region), and that region’s MQTT endpoint (mqtt_endpoint) — the same value §3.9 answers for the region, so the endpoint always comes from the platform’s region map and never from the refusing deployment. A selection the platform cannot determine is not this refusal and not a default region — it is DDI-1-503-BACKEND-UNAVAILABLE. §9’s taxonomy moves with it: 44→46 scenarios and the error enumeration from 15 to 16 codes. No existing field, code, or operation changes — non-breaking per §10, with no DDI version bump.

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