Errors and recovery

Recovery must preserve device identity and avoid turning retries into duplicate commands, corrupt certificates, or partially installed firmware.

Provisioning errors

All bindings map RFC 9457 responses to the same taxonomy:

Error code

HTTP

Retry

Action

DDI-1-400-MALFORMED-BODY

400

no

fix request serialization

DDI-1-401-NO-CREDENTIAL

401

no

restore the correct bootstrap credential or client certificate

DDI-1-403-NOT-RECOGNIZED

403

no

wait for registrar binding or correct model/serial

DDI-1-403-IDENTITY-BLACKLISTED

403

never

quarantine the device and escalate; this is terminal

DDI-1-422-MALFORMED-CSR

422

no

regenerate a valid signed CSR

DDI-1-429-RATE-LIMITED

429

bounded

honor Retry-After, then back off with jitter

DDI-1-503-BACKEND-UNAVAILABLE

503

bounded

retry with exponential backoff and jitter

The SDK provisioning clients perform bounded retries for 429 and 503. A surfaced retryable error means their retry budget is exhausted; persist the attempt state and apply a slower outer schedule rather than starting a tight loop.

Python raises typed subclasses of ProvisioningError. Android raises typed ProvisioningException subclasses. C returns fgai_prov_error_t and provides last-error accessors. Branch on those types or codes, not localized error text.

Provisioning origin mismatch

A device records the provisioning origin it was enrolled against, and stays on that origin. An ordinary save can never change it: a save carrying a different origin is refused, nothing is written, and the recorded origin is left exactly as it was, so a mistyped region cannot move a device between planes.

An origin has two forms and they are one origin: the host (things.flexgalaxy.ai) and the short name the platform records on the device (tokyo). Recording one and passing the other is not a different origin, and is not refused.

Binding

Refusal signal

Python

OriginMismatchError

C

FGAI_PROV_ERR_ORIGIN_MISMATCH

Android

SdkException.OriginMismatchException

The error names the origin on record and the origin the save carried. Do not retry it, and do not re-issue the save with the recorded origin to make it succeed — an origin change is a re-provision, not a retry. A save naming the same origin is idempotent, and a device whose record predates the origin field records its origin as a first record: neither is a conflict, and neither needs a flag.

A not-authorized MQTT CONNACK is terminal. When the broker answers the device’s credentials with a not-authorized reason code, the SDK surfaces a typed error — Python raises RegionRefusedError — and stops retrying: the reconnect loop exits instead of backing off forever, because a credential issued for one region cannot be made acceptable to the other by repetition. It is the wire-side sibling of the refusal above, caught by the broker instead of before the save.

In Python the sibling is also caught before the wire: connect() raises OriginMismatchError when the configured host is on a different origin than the one the device recorded, and no connection is attempted — the wrong-region credential never reaches the broker at all. The broker-side signal above remains for a credential that is refused for a reason this side cannot see (the account was moved, the certificate was revoked).

Re-provisioning a device

Moving a device between regions is a deliberate act with its own call. It is the only path that may replace a recorded origin, and it does not ask for confirmation, so reach for it only when the device genuinely is moving — a save naming the same origin is already idempotent and needs no flag.

Binding

Explicit re-provision

Python

persist(..., reprovision=True)

C

fgai_provisioning_origin_replace

Android

TrustMintSDK.reset(), then enrol again

Confirm the account’s region before re-provisioning.

Certificate rotation

Generate a new key for rotation. Write the new key and certificate to temporary storage, verify that they match and chain to the trusted CA, then atomically switch the active identity. Keep the prior pair until the new pair completes an mTLS proof. If proof fails, restore the prior pair and retry according to the certificate’s remaining validity.

Do not expect rotation to return a new CA chain, MQTT endpoint, or expiry field. The implemented response contains only thing_id and certificate.

MQTT connection loss

Use a stable fgai-<thing_id> client ID, persistent session, one-hour default session expiry, and exponential reconnect with jitter.

After each reconnect:

  1. re-subscribe commands and SHARED pushes even if the broker reports a resumed session;

  2. restore all message callbacks before application traffic;

  3. republish the complete CLIENT attribute snapshot;

  4. resume telemetry with a monotonic application sequence; and

  5. deduplicate commands by command_id before executing side effects.

Python performs reconnect and re-subscription automatically. C reconnects internally and exposes an informational disconnect callback. Android reconnects automatically and replays commands, twin desired, and twin delta on every reconnect; SHARED pushes remain application-driven, so a device that uses SharedAttributeManager must re-subscribe them itself (step 1 above).

Dated note (2026-09-20): the C reconnect described in the sentence above became true on this date. Before the 2026-09-20 landing the C client neither armed a reconnect nor replayed its subscriptions, so an application built against an older C SDK must still drive the sequence above itself.

Inbound message failures

Reject malformed protobuf messages without applying them. A shared-attribute callback must apply the value durably before acknowledging it. Python currently acknowledges after the callback returns even if the callback raises, so callbacks should contain their own error handling and must not signal success by throwing. Android and C adapters should follow the same explicit effect-then-ack rule.

Command handlers should be short and idempotent. Persist the command ID and operation state before an irreversible side effect. If work is long-running, hand it to a durable local queue and respond according to the command contract; do not block an MQTT network callback.

OTA failures

Failure

Binding signal

Recovery

HTTP or transport

Python OtaHttpError/OtaError; Android ApiResponse; C transport/HTTP code

retry safe GET/HEAD with bounded backoff; send feedback when the action supplies a feedback link

unsafe HAL link

argument/value failure; C FGAI_OTA_ERR_UNSAFE_LINK

reject permanently and report a platform contract error

SHA-256 mismatch

Python OtaIntegrityError; Android OtaIntegrityException; C FGAI_OTA_ERR_INTEGRITY

delete the temporary file, report failure, and do not install

parse failure

document/JSON exception; C FGAI_OTA_ERR_PARSE

retain current firmware and report malformed assignment

local I/O

exception; C FGAI_OTA_ERR_IO

preserve the running slot, reclaim storage, then retry if safe

Downloads must land in a temporary file or inactive slot. Verify expected size when supplied and SHA-256 before an atomic rename or boot-slot switch. On boot failure, the product bootloader—not DDI—must roll back to the known-good slot.

Licensing failures

The device licensing HTTP routes are not published. Treat connection failure or 404 from those compatibility methods as “capability unavailable,” not as a license grant. The Android offline parser is not signature verification and must not be used to fail open. See Device licensing compatibility.

Observability without secret leakage

Log operation name, non-secret thing_id, error code, HTTP status, retry count, and correlation ID. Never log bootstrap or enrollment tokens, JWTs, private keys, PEM bodies, raw authorization headers, HawkBit upstream credentials, or command payloads that may contain secrets.