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 firstPOST /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:
POST /ddi/v1/bundles→ create session{kind, content_type, estimated_size, captured_from, captured_to}→{upload_id, chunk_size, presigned_post}.PUT /ddi/v1/bundles/{upload_id}/chunks/{n}→ sequential chunks.POST /ddi/v1/bundles/{upload_id}/complete→{sha256, actual_size}→{bundle_id, state: "available"}.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) |
|---|---|---|---|
|
401 |
No verified client certificate |
terminal |
|
403 |
The certificate’s identity cannot be resolved to a |
terminal |
|
404 |
The registration service answered: no registration exists for this |
terminal |
|
403 |
The registration exists and names no region (an enrollment that did not complete). No default region is applied |
terminal |
|
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; |
|
503 |
This deployment’s region→endpoint map is unset or malformed, so it can answer for no device |
retry with backoff; |
|
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; |
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 → cloud |
0 |
Time-series values + optional geo block |
|
device → cloud |
1 |
CLIENT-bucket attributes (device-only-writable) |
|
cloud → device |
1 |
SHARED-bucket updates (subscribe on connect) |
|
device → cloud |
1 |
Ack a SHARED update after applying it |
|
cloud → device |
1 |
Command dispatch (subscribe on connect); ack via §3.7 |
|
device → cloud |
1 |
Durable, structured, high-value events |
|
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; |
Provisioned |
Received cert from |
Activated |
Normal operation — telemetry / events / heartbeat flow per §4 |
Deactivated |
Cert revoked; the next mTLS attempt fails with TLS alert |
Annihilated |
|
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 |
|---|---|---|
|
400 |
Body not valid JSON or required fields missing |
|
401 |
mTLS cert or attestation token missing |
|
401 |
mTLS cert signature invalid or chain broken |
|
403 |
Identity not a registered factory pattern |
|
403 |
Identity annihilated — terminal |
|
403 |
Device cert revoked (deactivated) |
|
403 |
(§3.9) The device’s registration names no home region; terminal, and no default region is applied |
|
404 |
(§3.9) The registration service answered: no device→region registration for this |
|
409 |
Same |
|
409 |
(§3.2) The account’s selected home region is not the region serving this provisioning request; terminal, and it names the |
|
422 |
Model not in the registered catalog |
|
422 |
CSR cannot be parsed or its signature is invalid |
|
429 |
Per-device rate limit exceeded; respect |
|
503 |
Backend temporarily unavailable; retry with backoff |
|
503 |
(§3.9) The device’s region has no MQTT endpoint on this deployment; the deployment never substitutes its own endpoint |
|
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_atfields 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_timeorDate: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: |
2026-10-07 |
Discovery reachability. The dedicated, globally stable name for §3.9 — |
2026-10-09 |
New refusal: |
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 |
|