Downlink Commands

Endpoint

  • POST /api/v1/open/downlink/commands — Accept a cloud-to-device command and return the platform acceptance result.

Interaction Model

001 implements synchronous acceptance and asynchronous dispatch event creation:
  1. The integrator submits a command.
  2. The platform verifies the signature, nonce, tenant binding, Casbin authorization, request body, and idempotency key.
  3. The platform writes the command fact and dispatch event in one PostgreSQL transaction.
  4. The platform returns command_id, ACCEPTED, accepted_at, and request_id.
  5. After the transaction commits, the API service attempts to publish the NATS event tenant.<tenant_id>.command.accepted.
202 Accepted only means the platform accepted and persisted the command. It does not mean the device executed it successfully.

Command Events and Status Model

Open Platform models subscribable events separately from command statuses. The currently subscribable command events are:
event_typeMeaning
command.acceptedThe platform authenticated, authorized, idempotency-checked, and persisted the command.
command.completedThe adapter or device returned a terminal result. The event payload carries the concrete execution result.
command.problemThe adapter found a command-boundary diagnostic problem that cannot safely be correlated to a concrete command, such as a malformed accepted-command payload.
Do not model every status as a separate callback event. Whether a command succeeded, failed, timed out, was unsupported, or was rejected is represented in the command.completed payload and the command query response. command.problem is not a normal command failure; UTMOS keeps the raw diagnostic payload as a device recent event. The command query response exposes the aggregate status as public_status:
public_status
ACCEPTED
DISPATCHING
DELIVERED
RUNNING
SUCCEEDED
FAILED
TIMED_OUT
UNSUPPORTED
CANCELLED
UNKNOWN
Adapter-side handling is represented by adapter_status:
adapter_status
RECEIVED
REJECTED_BY_ADAPTER
SENT_TO_DEVICE
TRANSPORT_FAILED
RESPONSE_TIMEOUT
Device-side execution is represented by device_execution_status:
device_execution_status
ACCEPTED_BY_DEVICE
SUCCEEDED
TEMPORARILY_REJECTED
DENIED
UNSUPPORTED
TIMED_OUT
PX4 adapters currently report these raw terminal values in command.completed.status:
status
rejected_by_agent
sent_to_px4
ack_accepted
ack_temporarily_rejected
ack_denied
ack_unsupported
ack_timeout
transport_failed
parameter_value_confirmed
parameter_error
parameter_response_mismatch
parameter_timeout
The platform maps those adapter-native statuses into adapter_status, device_execution_status, and public_status in the query response. PX4 adapter query responses also include these fields in adapter_result:
FieldDescription
human_summaryHuman-readable terminal summary generated by the agent. Chinese is preferred when available.
hardware_responseTyped PX4 hardware response payload. MAV_CMD flows use COMMAND_ACK; parameter read/set flows use PARAM_VALUE or PARAM_ERROR; mission, FTP, and timesync flows use MISSION_ACK, FILE_TRANSFER_PROTOCOL, and TIMESYNC typed payloads when integrated.
source_evidenceLocal PX4/MAVLink source evidence used by the agent for the response mapping.

Authentication

  • Open Platform signature authentication is required: X-Api-Id, X-Api-Timestamp, X-Api-Nonce, and X-Api-Signature.
  • X-Request-Id is optional. The platform generates one when omitted and returns it in responses and logs.
  • Creating a command requires tenant-domain authorization: open:command:create.
  • See Authentication & Signing for a signature example.

Request Body

FieldTypeRequiredDescription
vendorstringyesVendor identifier, e.g. dji
device_idstringyesTarget device ID
command_typestringyesCommand type from the platform-supported catalog
payloadobjectyesCommand payload
idempotency_keystringyesIdempotency key
timeout_secondsintegernoTimeout in seconds; defaults to 30, valid range 1~300
payload is not an arbitrary object. It must match the command protocol for the selected vendor and command_type. For the full DJI payload field catalog, JSON examples, and code samples, see Command Payloads.
{
  "vendor": "dji",
  "device_id": "drone-001",
  "command_type": "camera_mode_switch",
  "payload": {
    "payload_index": "52-0-0",
    "camera_mode": 0
  },
  "idempotency_key": "req-20260422-0001",
  "timeout_seconds": 30
}

Responses

  • 202 Accepted — New command accepted.
  • 200 OK — Idempotent replay hit; returns the existing command.
  • 4xx/5xx — Standard error envelope.
Success example:
{
  "command_id": "100e2826-e45b-50cb-b41f-e8df6d20bea2",
  "status": "ACCEPTED",
  "accepted_at": "2026-04-22T12:00:00Z",
  "request_id": "req-20260422-0001"
}

Idempotency

  • The idempotency scope is the authenticated tenant_id + client_id + idempotency_key.
  • Replaying the same business request in that scope returns the same command_id.
  • Reusing the same idempotency key with different business semantics returns IDEMPOTENCY_CONFLICT.
  • The semantic hash includes vendor, device_id, command_type, canonicalized payload, and normalized timeout_seconds.
  • The semantic hash does not include X-Request-Id, X-Api-Nonce, X-Api-Timestamp, or signature headers.

Persistence and Publication Semantics

The acceptance response means the platform wrote the command fact and dispatch event in one PostgreSQL transaction. After commit, the API service attempts to publish:
tenant.<tenant_id>.command.accepted
If the transaction commits and the immediate NATS publish fails, the command remains ACCEPTED. The platform keeps the dispatch event and failure reason for the recovery flow.

Common Errors

codeHTTPDescription
UNAUTHORIZED401Missing or invalid authenticated identity
SIGNATURE_INVALID401Signature mismatch
TIMESTAMP_EXPIRED401Request timestamp is outside the allowed skew window
NONCE_REPLAYED401Nonce was reused within the replay window
FORBIDDEN403Client is not authorized in the tenant domain
TENANT_NOT_FOUND404No active tenant could be resolved
TENANT_BINDING_INVALID403Client-to-tenant binding is invalid
INVALID_REQUEST_BODY400JSON request body is malformed
VALIDATION_FAILED400Required field missing, invalid type, or out-of-range value
UNSUPPORTED_VENDOR400Vendor is not registered in the platform command catalog
UNSUPPORTED_COMMAND400Command type is unsupported or outside trusted capability scope
IDEMPOTENCY_CONFLICT409Same idempotency key was used for different command semantics
INTERNAL_ERROR500Platform internal error

Example

The samples below show the request structure. Production clients must calculate X-Api-Signature as documented in Authentication & Signing.
BODY='{"vendor":"dji","device_id":"drone-001","command_type":"camera_mode_switch","payload":{"payload_index":"52-0-0","camera_mode":0},"idempotency_key":"req-20260422-0001","timeout_seconds":30}'
TIMESTAMP="$(date +%s)"
NONCE="$(uuidgen)"
SIGNATURE="YOUR_CALCULATED_SIGNATURE"

curl -X POST "https://dev.utmos.dev/api/v1/open/downlink/commands" \
  -H "Content-Type: application/json" \
  -H "X-Api-Id: YOUR_API_ID" \
  -H "X-Api-Timestamp: $TIMESTAMP" \
  -H "X-Api-Nonce: $NONCE" \
  -H "X-Api-Signature: $SIGNATURE" \
  -d "$BODY"