Skip to content

Implement commands ​

Before you start ​

Complete Telemetry. Keep the same files and credentials. If you announced SECURITY_TRANSPORT_MTLS, install the device's certificate, key, and CA trust. Leave commands disabled during implementation.

MC sends commands through the bus link to the address you announced on the payload LAN (Connect, step 1). The bus must be adopted in your organization and reach MC over the bus link. A cloud bench bus may not carry commands to your payload; ask Urban Sky which bench to use for this step.

1. Define the command ​

Save as command-manifest.json:

json
{
  "schemaVersion": 1,
  "commands": [
    {
      "name": "sensor.set_sample_period",
      "description": "Set the measurement interval in seconds. Read back the applied interval in telemetry.",
      "confirm": true,
      "params": [
        {
          "key": "seconds",
          "label": "Sample period",
          "type": "FIELD_TYPE_NUMBER",
          "unit": "s",
          "min": 1,
          "max": 60,
          "required": true
        }
      ]
    }
  ]
}

Generate this manifest and your validation from one command registry. Missing required means required; only false allows omission. Enforce integer-only constraints in the device when needed.

confirm: true adds a UI confirmation. It does not replace authentication or hardware interlocks.

2. Implement and test the command server ​

Serve POST /commands at the announced address and port. Accept JSON, including when telemetry uses protobuf. MC sends:

json
{
  "name": "sensor.set_sample_period",
  "requestId": "68ca9d81-e40b-4b45-9b29-5a1bba5474f5",
  "params": {
    "seconds": 5
  }
}

Your handler must:

  1. With mTLS, verify MC's certificate before hardware access.
  2. Parse a bounded JSON body; require a nonempty requestId and a known command.
  3. If targetSerial is present, route to that child or reject it. Never default an unknown target to the gateway.
  4. Validate required parameters, types, enums, ranges, unknown keys, and hardware interlocks.
  5. Atomically reserve the request ID. Concurrent duplicates with the same body execute once and return the recorded result. Reject the same ID with a different body.
  6. Execute or durably queue the operation. Record failures and any partial changes.
  7. Return the outcome and report actual state in telemetry.

Persist request records when deduplication must survive reboot.

Reply within 10 seconds. For longer operations, return HTTP 202 after accepting the work:

json
{
  "requestId": "68ca9d81-e40b-4b45-9b29-5a1bba5474f5",
  "state": "COMMAND_STATE_ACCEPTED"
}
StateMeaning
COMMAND_STATE_ACCEPTEDAccepted; not yet complete.
COMMAND_STATE_APPLIEDApplied successfully.
COMMAND_STATE_REJECTEDRefused before execution; include reason.
COMMAND_STATE_FAILEDExecution failed; include reason and partial-state details.

Always echo the request ID. Empty responses, mismatched IDs, and unspecified states are invalid.

A command that reads a value, for example a getter for device limits, can return it in result. result is any JSON value. MC passes it back to the operator:

json
{
  "requestId": "68ca9d81-e40b-4b45-9b29-5a1bba5474f5",
  "state": "COMMAND_STATE_APPLIED",
  "result": { "min_s": 1, "max_s": 60 }
}

For a valid request, HTTP 200 with a rejected/failed acknowledgment preserves the command outcome. Use HTTP errors for authentication or malformed requests; MC treats non-2xx responses as delivery failures.

Report completion through telemetry; there is no completion callback. A timeout can mean the reply was lost after execution—check device state before retrying.

Log each command ​

Write one log line for each stage of a command. Put MC's requestId in every line. MC and the bus log the same ID, so one ID follows a command from the operator's click to your device's answer. A command with no log line on your device did not reach your device.

Write one JSON object per line:

json
{
  "time": "2026-10-19T22:05:39.753Z",
  "level": "info",
  "message": "payload command answered",
  "event": "payload_command_answered",
  "request_id": "68ca9d81-e40b-4b45-9b29-5a1bba5474f5",
  "command_name": "sensor.set_sample_period",
  "param_names": "seconds",
  "outcome": "applied",
  "duration_ms": 12
}
EventWhen
payload_command_receivedAfter you parse the request, before validation.
payload_command_refusedYou refuse it before execution: auth, body, target, or interlock.
payload_command_sentYour driver sends it to the hardware.
payload_command_device_answeredThe hardware answers, fails, or times out.
payload_command_answeredYou send the acknowledgment to MC.
FieldValue
request_idMC's requestId, unchanged.
command_nameThe command name.
param_namesParameter names, comma-separated. Never the values.
outcomeaccepted, applied, rejected, failed, or unknown (it may have run).
error_codeYour error code, when the outcome is not accepted or applied.
errorThe reason you return, when there is one.
device_statusThe hardware's status code, when it has one.
duration_msMilliseconds from payload_command_received to this line.
replayedtrue when you answer a duplicate requestId from your record.

Use these exact field names. Do not log credentials, parameter values, addresses, or positions.

Use level info for accepted and applied, and warn for the other outcomes. A payload on a Husky bus sends these lines to the bus. See Send logs and metrics.

Check: unauthorized and invalid requests never reach hardware; concurrent duplicates execute once; driver failures return the correct outcome; one requestId finds every line of its command in your log.

3. Announce the new command ​

Add the manifest to announce.json and resend it:

bash
jq -e --slurpfile commands command-manifest.json \
  '.commandManifest = ($commands[0] // error("Empty command manifest"))' announce.json > announce-next.json &&
curl --fail-with-body --silent --show-error \
  -X POST "${MC_URL:?Set MC_URL}/payloads/announce" \
  -H "x-device-api-token: ${DEVICE_API_TOKEN:?Set DEVICE_API_TOKEN}" \
  -H 'Content-Type: application/json' \
  --data-binary @announce-next.json &&
mv announce-next.json announce.json

Expected: HTTP 200 and the same UUID. The response does not include manifests; inspect command_manifest in the registration read below. Adoption and command enablement remain unchanged.

4. Adopt and enable the device ​

Your Urban Sky account needs payload read and write permissions in the device's organization. Some commands require higher roles; see permissions.

With Balto:

bash
balto payloads list --bus uskybus-8901fc          # your registration: status, commands_enabled
balto payloads adopt ENV-001
balto payloads enable-commands ENV-001
balto payloads enable-commands ENV-001 --off      # turn them off again

<payload> is the payload's ID or serial. Each command prints JSON. The same actions are in the Balto app, on the bus's Payloads tab.

With the HTTP API, use an operator token:

Inspect the identity, address, manifests, and measurements:

bash
curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer ${OPERATOR_TOKEN:?Set OPERATOR_TOKEN}" \
  "${MC_URL:?Set MC_URL}/payloads/${PAYLOAD_ID:?Run Connect first}"

curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer ${OPERATOR_TOKEN:?Set OPERATOR_TOKEN}" \
  "${MC_URL:?Set MC_URL}/payloads/${PAYLOAD_ID:?Run Connect first}/telemetry"

The step 5 command must return 403 while the device is discovered. Adopt it:

bash
curl --fail-with-body --silent --show-error \
  -X POST "${MC_URL:?Set MC_URL}/payloads/${PAYLOAD_ID:?Run Connect first}/adopt" \
  -H "Authorization: Bearer ${OPERATOR_TOKEN:?Set OPERATOR_TOKEN}"

Repeat the command while commands_enabled is false; expect 403 again. Then enable it:

bash
curl --fail-with-body --silent --show-error \
  -X PUT "${MC_URL:?Set MC_URL}/payloads/${PAYLOAD_ID:?Run Connect first}" \
  -H "Authorization: Bearer ${OPERATOR_TOKEN:?Set OPERATOR_TOKEN}" \
  -H 'Content-Type: application/json' \
  --data '{"commandsEnabled":true}'

Check: registration reads status: "adopted" and commands_enabled: true. Write requests use commandsEnabled; read responses use commands_enabled.

5. Send one command and verify its effect ​

With Balto:

bash
balto payloads send ENV-001 sensor.set_sample_period seconds=5
balto payloads audit ENV-001 --limit 5

Numbers and true/false keep their type. Quote a value to send it as a string: code='"0012"'. --request-id ID sends under a request ID you choose, for a retry of the same command. balto payloads audit reads what MC sent, newest first; --request-id finds one command. balto trace ENV-001 --down --follow shows commands as MC sends them.

With the HTTP API:

bash
curl --fail-with-body --silent --show-error \
  -X POST "${MC_URL:?Set MC_URL}/payloads/${PAYLOAD_ID:?Run Connect first}/commands" \
  -H "Authorization: Bearer ${OPERATOR_TOKEN:?Set OPERATOR_TOKEN}" \
  -H 'Content-Type: application/json' \
  --data '{"name":"sensor.set_sample_period","params":{"seconds":5}}'

MC generates requestId. Repeating this POST without a requestId creates a new command. Do not automatically retry after a timeout.

To retry a command with an unknown outcome as the same command, send the first attempt's ID as requestId (a UUID). A route that removes duplicates returns the first outcome and deduplicated: true. It returns deduplicated: false when it ran the command. If the route cannot tell, the response has no deduplicated field. Your device must still detect duplicates by request ID. You cannot send requestId together with via.

Check requestId, ack.state, and ack.reason. Then read a sample taken after the command: data.values["sensor.sample_period_s"] should report 5. For physical outputs, verify the effect with a measurement.

Find the requestId in your device log (see Log each command). Mission Control operators trace the same ID through MC and the bus. To read the commands that MC sent to the device, newest first, use GET /payloads/:id/command-audit. Add ?requestId= to find one command.

If it fails ​

  • 403: check adoption, command enablement, and permissions.
  • 400: compare names, values, and types with the stored manifest.
  • 502: check routing, mTLS, and acknowledgment format in Troubleshooting.
  • Accepted but unchanged: inspect the driver and measured state before retrying.

← Report telemetry · Next: Send logs and metrics →