Skip to main content
Version: Next

Device Protocol Reference

This page is the reference for every message a device exchanges with DeviceChain over MQTT and HTTP: the event a device sends, the command it receives, and the response it sends back. It lists every field, what happens to a message the platform refuses, and where that refusal can be seen. For a walkthrough, start with Connecting a Device.

LwM2M and Sparkplug B devices do not use these messages. Their protocol gateways translate to the platform's internal form; see LwM2M and Sparkplug B.

Machine-readable schemas​

Every message on this page is published as a JSON Schema (draft 2020-12). Use them to validate firmware output, or to generate types:

SchemaDescribes
device-event.schema.jsonThe event envelope a device sends
measurement-payload.schema.jsonThe payload of a Measurement event
location-payload.schema.jsonThe payload of a Location event
alert-payload.schema.jsonThe payload of an Alert event
new-relationship-payload.schema.jsonThe payload of a NewRelationship event
command-delivery.schema.jsonThe command the platform delivers to a device
command-response.schema.jsonThe response a device sends back

The same list, with absolute URLs, is under deviceProtocol in /schema/index.json. The envelope schema references the payload schemas by relative URL, so a validator that loads it by URL resolves them on its own.

The schemas are committed beside the code that decodes these messages, and a test fails the platform's build if a field is renamed, added or removed on one side and not the other.

What a schema cannot say:

  • Unknown members are ignored, not refused. A misspelled optional field (altID for altId is fine, alt_id is not) is silently dropped rather than rejected. A validator run with additionalProperties: false added locally catches that in testing.
  • "Required" describes what a device must send, not everything the platform refuses at the door. Most required fields are enforced, as the rejection tables list. These are not:
    • device may be missing when a credential authenticates the event; the event is attributed to the credential's device.
    • A NewRelationship event with no payload, or with relationshipType, targetType or target missing, is accepted (HTTP 202): each missing value reads as an empty string, and the event then fails at resolution and is dead-lettered with reason ApiCallFailed.
    • In a command response, a missing success reads as false and settles the command as FAILED; a missing commandToken matches no command and is dead-lettered with reason exhausted after its retries.

Versioning​

The messages carry no version field. The contract is the one described by the schemas published with the documentation for your release, and a schema changes in the same release as the code that decodes its message. Before v1.0.0, a release may change the contract, so compare the schemas between the release you run and the one you are moving to.

Topics and endpoints​

The platform broker is the MQTT server built into NATS. An MQTT topic and the NATS subject the platform reads are the same name, with / written as .:

MessageDirectionMQTT topicNATS subject
Eventdevice → platform{instanceId}/{tenant}/devices/{deviceToken}/events{instanceId}.{tenant}.devices.{deviceToken}.events
Command deliveryplatform → device{instanceId}/{tenant}/device-commands/{deviceToken}{instanceId}.{tenant}.device-commands.{deviceToken}
Command responsedevice → platform{instanceId}/{tenant}/command-responses/{deviceToken}{instanceId}.{tenant}.command-responses.{deviceToken}
Configuration and update messagesplatform → device{instanceId}/{tenant}/device-desired/{deviceToken}{instanceId}.{tenant}.device-desired.{deviceToken}
Configuration and update reportsdevice → platform{instanceId}/{tenant}/device-reports/{deviceToken}{instanceId}.{tenant}.device-reports.{deviceToken}
  • A device is authorized to publish and subscribe only on the topics carrying its own device token. It cannot read another device's commands or publish as another device.
  • Devices connect over MQTT, not raw NATS. A device credential authorizes an MQTT connection only: a plain NATS client presenting one is refused at connect. The subject column is what an operator sees in NATS tooling, not a second way in. Connection settings (the client id, the {tenant}:{credentialId} username, TLS) are in Connection settings.
  • Publish events at QoS 0, or at QoS 1 with an altId. QoS 2 publishes are refused by default and the broker closes the connection; see Quality of service.

A device with no credential yet reaches the platform over HTTPS to register itself, at POST /api/device-management/provisioning/{tenant}/register. That is a separate endpoint from event ingest, served by device-management.

Over HTTP, a device can send events only. POST the same event body to /{instanceId}/{tenant}/events on the event-sources service (port 8081). There is no HTTP downlink: an HTTP-only device cannot receive commands, configuration or update assignments.

The gateway namespace​

A gateway (the edge agent is one) is a device that publishes for other devices. It does so in its own namespace:

MessageDirectionMQTT topic
Event for a device behind the gatewaygateway → platform{instanceId}/{tenant}/gateways/{gatewayToken}/devices/{deviceToken}/events
Notice that an event was refusedplatform → gateway{instanceId}/{tenant}/gateways/{gatewayToken}/notices
  • A gateway is authorized to publish only under its own token, with any child token after devices/, and to subscribe only to its own notices. It cannot publish in another gateway's or another tenant's namespace, or on a child's own topic. The grant is the same size however many devices the gateway fronts, and every device holds it, so assigning a first device to a gateway needs no reconnect.
  • The platform accepts an event only if the device named in the topic exists, is assigned to that gateway, and the gateway is enabled. Otherwise it refuses the event with the reason NotGatewayChild (below) and sends the gateway a notice. The broker acknowledges the publish either way, because MQTT 3.1.1 cannot refuse one.
  • Identity is the topic plus the assignment. The body carries none: an event on this namespace that carries credentialType, credentialId or credentialSecret is refused, and so is a body device that names another device. The event is attributed to the child, and the resolved event carries the gateway's token as well.
  • A notice is a JSON object, {"altId": "...", "device": "...", "reason": "not_gateway_child"}. It carries no payload and is not stored, so a gateway that was offline when it was sent misses it. The record of a refusal is the failed-events stream.
  • The grant also covers responses and reports under the namespace and subscribing to commands and desired, but nothing reads or sends them yet. Publish events only.

Device event​

One JSON object per MQTT message or HTTP request. One message is one event.

FieldTypeRequiredMeaningExample
devicestringyes¹The token of the device sending the event. On MQTT it must equal the {deviceToken} in the topic. When a credential authenticates the event, it must name that credential's device."sensor-001"
eventTypestringyesMeasurement, Location, Alert or NewRelationship, case-sensitive. Selects the payload shape."Measurement"
payloadobjectyesThe event's content. Its shape depends on eventType; see Payloads.{"entries":[…]}
occurredTimestring, RFC 3339noWhen the event happened. Omitted, the event is dated when the platform received the message."2026-08-09T12:00:00.125Z"
altIdstringnoA device-chosen idempotency key: a redelivered event carrying the same altId and envelope occurredTime is skipped. Currently matched per tenant, not per device, which is a known limitation; see below."sensor-001-4417"
relationshipstringnoAccepted, carried through the pipeline, and not used. The platform records every one of the device's tracked relationships on the event, whatever this says. Do not rely on it.—
credentialTypestringHTTP: see below. MQTT: neverACCESS_TOKEN or MQTT_BASIC. With credentialId, authenticates the event."ACCESS_TOKEN"
credentialIdstringHTTP: see below. MQTT: neverFor ACCESS_TOKEN, the token itself. For MQTT_BASIC, the username, without the {tenant}: prefix the MQTT connection uses."5f98…98b2"
credentialSecretstringHTTP, MQTT_BASIC only. MQTT: neverThe MQTT_BASIC password.

¹ Send device on every event. Strictly, the platform tolerates its absence when a credential authenticates the event, and then attributes the event to the credential's device.

The credential, over MQTT. Do not send one. On the platform's MQTT broker the device is authenticated when it connects, and the topic names the device, so credentialType, credentialId and credentialSecret must be absent: an event that carries any of them is refused (see the rejection table). Treat the credential of a device that did so as exposed and rotate it. A broker you operate yourself and connect as an external MQTT source does not authenticate devices for the platform, so events from it still carry the credential, as over HTTP.

The credential, over HTTP. The default device-authentication mode is required: an event with no credential is refused (see the rejection table). Omit the credential only on an instance configured as optional or disabled. A credential is read only when both credentialType and a non-empty credentialId are present. See Device credentials.

Timestamps. Every occurredTime, on the envelope or on an entry, is RFC 3339. The platform refuses one that is not, one that is 0001-01-01T00:00:00Z (reserved to mean "no time was reported"), and one more than 366 days before the platform received the message. A time far ahead of the platform's clock is stored at a ceiling rather than refused; see Clocks that run ahead.

altId and duplicates​

At-least-once delivery (MQTT QoS 1, an HTTP retry after a 503) can deliver one event twice. Without an altId, both copies are stored. With one, the second is skipped:

  • The match is on altId and the envelope occurredTime together. An entry's occurredTime does not count.
  • Send an occurredTime on the envelope. Without one, each copy is dated on arrival, the two times differ, and both are stored.
  • The match is currently made per tenant rather than per device; see the limitation below.
  • A second event with the same altId and occurredTime is skipped even if its content is different.
Known limitation

Duplicates are currently detected per tenant on (altId, occurredTime), not per device, so two devices that send the same altId for the same instant collide, and one of the events is skipped. This is a defect, and a fix is under way. Until it ships, make the value unique across the fleet, for example by prefixing the device token.

Payloads​

Measurement, Location and Alert payloads wrap their content in an entries array. One entry is one reading at one instant, and each entry may carry its own occurredTime; an entry without one takes the envelope's. A payload with no entries, or with its content placed directly under payload, is refused.

One event carries at most 256 readings, on both transports. A reading is one measurement key, or one location or alert entry. A message over the limit is refused whole, never trimmed.

Measurement​

FieldTypeRequiredMeaningExample
entriesarrayyesThe samples. At least one.
entries[].measurementsobject of string → stringyesMetric name to value. Every value is a JSON string: "21.5", not 21.5. A bare number fails the whole message. At least one metric.{"temperature":"21.5"}
entries[].occurredTimestring, RFC 3339noThis sample's instant.

Location​

Every location field is a JSON string, including the numeric ones. Each must parse as a finite number inside its range.

FieldTypeRequiredMeaningRange
entriesarrayyesThe fixes. At least one.
entries[].latitudestringyesWGS84 decimal degrees−90 to 90
entries[].longitudestringyesWGS84 decimal degrees−180 to 180
entries[].elevationstringnoMetres above the WGS84 ellipsoid, not above mean sea levelmagnitude ≤ 99999999
entries[].accuracystringnoHorizontal accuracy, metres0 to 99999999
entries[].speedstringnoMetres per second0 to 99999999
entries[].headingstringnoDegrees clockwise from true north0 up to but not including 360; 359.99995 and above is refused
entries[].occurredTimestring, RFC 3339noThis fix's instant

Alert​

FieldTypeRequiredMeaningExample
entriesarrayyesThe alerts. At least one.
entries[].typestringyesThe classifier notification policies, rules and console filters route on. Empty is refused."overheat"
entries[].levelintegernoSeverity, a bare JSON integer from 0 to 2147483647. A quoted level fails the whole message. Omitted, it is 0.5
entries[].messagestringnoHuman-readable text."coolant over limit"
entries[].sourcestringnoWhat on the device raised it."ecu"
entries[].occurredTimestring, RFC 3339noThis alert's instant.

NewRelationship​

Creates one relationship from the sending device to another entity in the same tenant. This payload has no entries array, and its three keys are read by their exact names, so their case matters. A missing key is not refused when the message is decoded: it reads as an empty string, and the event then fails at resolution like the cases below.

FieldTypeRequiredMeaningExample
relationshipTypestringyesThe token of the relationship type: one defined in the tenant, or one of the platform's reserved types, which are created on first use."member"
targetTypestringyesdevice, asset, area, customer or group."group"
targetstringyesThe token of the target entity."building-7-sensors"

A relationship the platform cannot create (an unknown target type, a target that does not exist, an unknown relationship type) is not created. Like any event that cannot be resolved, it is retried and then dead-lettered.

Command delivery​

What a device receives on its device-commands topic, once for each dispatch of a command. Delivery is live-only: a device that is not connected and subscribed when the command is published does not receive it, and the platform is not told either way. See Receiving commands.

FieldTypeAlways presentMeaningExample
tokenstringyesThe command's token. It identifies the command, not the device. Send it back as commandToken."6f1c0f8e-…"
deviceTokenstringyesThe device the command is addressed to: always the device whose topic it arrived on."sensor-001"
namestringyesThe command key. When the device's profile declares a command vocabulary, one of its published commands."reboot"
dispatchNoncestringyesNames this dispatch of the command. Opaque: do not parse it. Echo it in the response. If the same command arrives again, answer with the nonce from the latest delivery."0f6f4a2c-…"
payloadany JSON valuenoThe command's parameters, exactly as issued. Already validated against the command's parameter schema when the profile declares one. Absent when the command was issued without parameters.{"delaySeconds":5}

Command response​

What a device publishes on its command-responses topic to settle a command. The platform takes the responding device from the topic, never from the body.

FieldTypeRequiredMeaningExample
commandTokenstringyesThe token of the delivery being answered: the command's token, not the device's."6f1c0f8e-…"
dispatchNoncestringyesThe dispatchNonce of the delivery being answered."0f6f4a2c-…"
successbooleanyestrue settles the command as SUCCESSFUL, false as FAILED. Omitted, it reads as false.true
payloadany JSON valuenoThe command's result data, stored with the command and returned by the API. A string is stored as its text; an object, array, number or boolean is stored as that JSON value, exactly as sent, so a device can return structured data directly. Omitted or null, the command has no response payload."rebooting in 5s" or {"level":3}
errorstringnoWhy the command failed. Stored only when success is false; ignored when it is true."actuator jammed"
reachstringnoFor a gateway or protocol adapter that answers on a device's behalf; a device reporting its own outcome omits it. Read only when success is false. "UNANSWERED": the command was put on the wire and nothing came back, so it settles as TIMEOUT. "UNSENT": the adapter refused it before it reached the device, so it settles as FAILED and is recorded as never delivered. Omitted, a failure is the device's own answer. An unrecognised value is treated as "UNANSWERED"."UNANSWERED"
{"commandToken":"6f1c0f8e-6d1e-4a1a-9a3f-1f2b0d0a5c11","dispatchNonce":"0f6f4a2c-9b71-4d0e-8a5b-3c2d1e0f7a94","success":false,"error":"actuator jammed"}

Self-registration​

A device that has no credential yet can register itself, over HTTPS, with a fleet-wide provision key and secret from a provisioning profile. It makes up its own credential, sends it once, and the platform answers with the device's token and MQTT client id. The platform keeps only a digest of the credential, and no response contains a secret.

POST /api/device-management/provisioning/{tenant}/register
  • HTTPS only. The request carries the provision secret in its body. There is no MQTT registration with a secret. A device that holds a client certificate can be registered when it first connects over MQTT, by a certificate profile, which has no provision secret and does not use this endpoint.
  • It needs the instance's API ingress, which routes /api/<area> to the services and strips the prefix. With ingress.enabled: false the route has no public path. dcctl device register takes the same --server host as the other commands.
  • There is no Authorization header. The provision key and secret in the body are the authentication.

Request​

Content-Type: application/json, at most 8 KiB. A field the schema does not define is refused, so a misspelled field fails instead of registering the device without it.

{
"provisionKey": "acme-sensors-2026",
"provisionSecret": "<shown once when the profile was created or rotated>",
"externalId": "SN-000123",
"credentialSecret": "<the device's own credential, 22 to 256 base64url characters>",
"name": "optional",
"metadata": { "optional": "object" }
}
FieldRequiredRules
provisionKey, provisionSecretyes1 to 256 characters each. The secret is the one the server generated for the profile.
externalIdyesThe device's serial number or other id, 1 to 256 characters. It is how the platform finds the device.
credentialSecretyesThe device's own credential material, which becomes its access token. base64url (letters, digits, - and _, no padding), 22 to 256 characters. Generate 32 random bytes and encode them (43 characters).
namenoAt most 128 characters. Used only when this request creates the device.
metadatanoA JSON object. Used only when this request creates the device.

Generate the credential on the device and store it before you send the request. The platform cannot give it back. A request whose answer was lost is then safe to repeat with the same material: it is recognised as a replay and answered 200, instead of being refused as a second device.

Response​

201 when the request created the device, 200 when it adopted an existing one or recognised a replay. The body is the same in all three cases and holds nothing secret:

{
"outcome": "CREATED",
"tenant": "acme",
"deviceToken": "pv-SN-000123-06e08144e3e8",
"externalId": "SN-000123",
"credentialType": "ACCESS_TOKEN",
"mqtt": { "clientId": "<instanceId>:acme:pv-SN-000123-06e08144e3e8" }
}
outcomeStatusMeaning
CREATED201The device did not exist. The profile (strategy ALLOW_NEW) created it, of the profile's device type and earmarked for the profile, with the credential. The device token is derived from the external id and starts with pv-.
ADOPTED200The device already existed, was earmarked for this profile and held no credential. It was given this one.
REPLAYED200The device already holds this profile's credential and the request presented the same material. Nothing was written.

Connect with the client id from the response and the MQTT username {tenant}:{credentialSecret} with no password, as in Connection settings. The response does not repeat the credential.

What decides the answer​

Every refusal is decided before anything is written. A device the platform already knows is judged by these steps in order, and the first that matches is the answer:

StepConditionAnswer
1The device is disabled.403 DEVICE_DISABLED
2Its device type is not the profile's.409 DEVICE_TYPE_MISMATCH
3It is not earmarked for this profile.409 DEVICE_NOT_EARMARKED
4This profile's credential on it was disabled or has expired.409 ALREADY_PROVISIONED, with the message "the device's credential was revoked; an operator must reset its provisioning"
5It holds this profile's live credential and the material matches.200 REPLAYED
6It holds this profile's live credential and the material differs, or it holds any other credential.409 ALREADY_PROVISIONED
7It holds no credential and the profile has registered as many devices as its maxDevices.422 LIMIT_EXCEEDED
8Otherwise.200 ADOPTED

A device the platform does not know (no live device has this externalId) is created under the strategy ALLOW_NEW (422 LIMIT_EXCEEDED at the cap), and refused with 404 DEVICE_NOT_REGISTERED under CHECK_PRE_PROVISIONED. Neither strategy ever adopts a device that was not earmarked for the profile: an operator-made device is taken over only by a profile somebody pointed at it, which is what earmarking is.

Errors​

An error answers { "code": "...", "message": "..." }. The set of codes is closed. A 429, and a 503 for want of capacity, also carry Retry-After, in seconds. A message never contains a secret and never says which part of the authentication failed.

StatuscodeWhen
400INVALID_VALUEThe tenant in the path is not a valid token; the body is not one JSON object or carries a field that is not defined; a field is out of range (the message names the field, never its value).
401PROVISIONING_FAILEDThe provision key or secret is not valid. This is one answer for every cause: an unknown tenant, an unknown key, a wrong secret, a profile that is disabled or expired, a profile whose secret the server never generated (see rotate it) and a tenant that is being deleted.
403DEVICE_DISABLEDStep 1 above.
404DEVICE_NOT_REGISTEREDAn unknown device under CHECK_PRE_PROVISIONED.
408INVALID_VALUEThe body did not arrive within the read deadline (10 seconds by default). The connection is closed.
409DEVICE_NOT_EARMARKED, DEVICE_TYPE_MISMATCH, ALREADY_PROVISIONEDSteps 3, 2, and 4 or 6 above.
409DEVICE_TOKEN_COLLISIONThe device token derived from this external id is held by a different device. An operator must resolve it.
413INVALID_VALUEThe body is over 8 KiB.
422LIMIT_EXCEEDEDThe profile has registered as many devices as its maxDevices.
429RATE_LIMITEDA limit is used up: the source address's allowance or the tenant's registrations per second. Retry-After says how long to wait.
500PROVISIONING_MISCONFIGUREDThe profile cannot register anything: its device type was deleted, or its strategy is not one the platform knows. An operator must fix the profile.
500PROVISIONING_UNAVAILABLEAn internal failure. The registration was not completed; retry later.
501CREDENTIAL_TYPE_UNSUPPORTEDThe profile mints a credential type this build cannot. Terminal; it never falls back to another type.
503PROVISIONING_UNAVAILABLEThe replica is busy (with Retry-After), or the instance has no keys to check a provision secret with (without).

The limits​

The limits keep someone who has not authenticated from making the service spend more than the request is worth, and keep one factory run from drowning the rest of the tenant. In the order they apply:

  1. A body deadline. The body must arrive within 10 seconds (provisioning.readDeadline, at most 1 minute). A request still sending its body holds nothing but its own connection.
  2. A size cap and parse. 8 KiB, and the body is read and validated before the request counts against anything below.
  3. A bound on concurrent requests: 64 per replica (provisioning.maxInFlight), then 503 with Retry-After.
  4. A per-source allowance: a burst of 60 requests, then one a second, per client address and per replica (provisioning.sourceBurst, provisioning.sourceEvery). IPv6 clients are counted by /64. Nothing exempts a source that has succeeded before. The address is the real client's only when the services are told which proxies to believe: see Client addresses and trusted proxies.
  5. A per-tenant ceiling, charged only once the provision key and secret have been accepted: by default a burst of 500 and 20 registrations a second, resolved through the tenant's tier and overrides like the other governance ceilings. A missing or unreachable setting means the platform default, never unlimited.
  6. A cap per profile: maxDevices, set by the operator. Without one, the profile is uncapped.

There is no limit on guessing the provision secret, because it cannot be guessed: it is 256 random bits, compared in constant time, and an unknown key costs the same work as a wrong secret. A sustained flood of 401s raises an alert instead.

The retry contract​

Persist the credential before the first send, and present the same credential on every retry.

AnswerWhat to do
429, or 503 with Retry-AfterWait Retry-After plus a random extra of 0 to 100% of it, then retry.
A network error, a timeout, 408, 502, 504, 503 without Retry-After, or 500 with PROVISIONING_UNAVAILABLEBack off exponentially from 1 second to 5 minutes, with full jitter, then retry.
400, 413Stop. The request is wrong; this is a firmware defect.
401, 403, 404, 409, 422, 501, 500 with PROVISIONING_MISCONFIGUREDStop and report. Retrying cannot change the answer; an operator has to.

The jitter matters: a fleet that powers on together and retries in step arrives together again. dcctl device register implements this contract and is the reference for firmware authors.

Rejections and errors​

What happens to a message the platform does not accept depends on where it is refused, and on the transport. HTTP answers the request, so a device learns of a refusal at the door. MQTT acknowledges a publish (PUBACK) as soon as the broker has captured it, before it is decoded, so an MQTT device is never told: every refusal after the broker is visible only to an operator. With the default setting, a QoS 1 publish acknowledged while the capture stream changes leader can also be lost, both while the capture stream changes leader and, under sustained load, when ingest falls behind and the stream fills; see Quality of service.

At the door​

Checked by event-sources when the message is received and decoded.

ConditionHTTPMQTTWhere an operator sees it
Instance id in the path or topic is not this instance's404The broker refuses the publish: the device is not authorized for that topic—
Tenant in the path is not a valid token400The broker refuses the publish—
Tenant is over its ingest ceiling, by messages or by readings429 with Retry-After: 1Acknowledged, then droppedtotal_msg_rate_limited, total_msg_reading_limited
Body cannot be read, or is over 1 MiB400——
Body does not decode: not JSON, an unknown or platform-only eventType (StateChange, CommandInvocation, CommandResponse), a value of the wrong JSON type, no entries, an empty entry, a missing or out-of-range location field, an alert with no type or with a level over 2147483647400, with the reason in the bodyAcknowledged, then routed to the failed-decode streamtotal_msg_failed_decode
A timestamp that is not RFC 3339, is 0001-01-01T00:00:00Z, or is more than 366 days old400, naming the fieldRouted to failed-decodetotal_msg_failed_decode, total_msg_invalid_event_time
More than 256 readings400, naming the count and the limitRouted to failed-decodetotal_msg_failed_decode, total_msg_too_many_readings
The body's device is not the device in the topic—Routed to failed-decodetotal_msg_failed_decode
The body carries credentialType, credentialId or credentialSecret, on the platform's MQTT broker—Routed to failed-decode (redacted)total_msg_failed_decode, total_msg_credential_in_mqtt_body
The platform is applying backpressure503 with Retry-After: not stored, send it againNot refused: it waits in the capture stream; the oldest captured messages are discarded only if that stream fills
The event could not be handed to the pipeline503 without Retry-After: it may have been stored, so a resend stores it twice unless it carries an altId—
Accepted202PUBACK (QoS 1)

A 400 is terminal: the same request gets the same answer. A refusal at the door refuses the whole message, including every other entry in it.

After acceptance​

An accepted event (202, or captured by the broker) is then resolved by device-management, which authenticates the device and attaches the event to it. These refusals are the same on both transports, and no device is told of them. The event is retried, then recorded on the failed-events stream with one of these reasons:

ReasonCause
UnauthenticatedNo credential while device authentication is required; a credential that does not authenticate; or a body device that is not the credential's device.
DeviceNotFoundNo credential was used, and the device token is not registered in the tenant.
InvalidThe event is older than 366 days, or could not be interpreted for its type.
ApiCallFailedA lookup or write the resolution needed failed, including a NewRelationship the platform could not create.
NotGatewayChildA gateway published for a device that is not assigned to it, or that does not exist, or the gateway is disabled. Not retried: it is recorded on the first delivery.

A refused event is retried until its fifth delivery, so a device registered or a database restored in the meantime still gets its event stored. The exception is an event too old to store, which is recorded on its first delivery, since no retry can change its age.

Command responses​

A command response is never answered, on any transport. What happens to it:

ResponseOutcome
Matches a command the device owns, with the dispatchNonce of its current dispatchThe command is settled SUCCESSFUL or FAILED.
Answers a command that is already finishedIgnored; the command keeps its outcome.
Not decodable: not JSON, or a field of the wrong type, such as a quoted successNot settled; recorded on the dead-letters stream with reason unprocessable, naming the responding device. The command stays SENT until it times out, unless the device answers again correctly.
No dispatchNonceNot settled; recorded on the dead-letters stream with reason unprocessable.
A dispatchNonce from a dispatch the command has moved offNot settled; dead-lettered with reason unprocessable.
A commandToken that matches no command (most often the device's own token sent by mistake)Retried until the fifth delivery, then dead-lettered with reason exhausted.
A command that belongs to a different deviceRefused: logged and counted, not recorded against the command and not dead-lettered.

A command nobody answers stays SENT until its expiry turns it into TIMEOUT. See Why the nonce is required.