API recipes

Needs Edge Server 5.4.2 or later.

Each recipe is a complete flow with the request and the answer. We cut fields that do not matter to the step. "...": "..." marks a cut.

Before you start

You need:

  • An Edge Server, and a user account on it.
  • Shell access to the Edge Server computer, to make a token.
  • curl and jq on the computer that calls the API.

Terms

TermMeaning
AnswerThe HTTP response that the Edge Server sends.
DevEUIThe 16-character ID on the Outlet label.
Wired link (RS-485)The cable link between the Controller and the Edge Server or Modbus devices. In the API, transport is rs485.
Bus masterThe Modbus device that asks the questions. The other devices on the bus answer. In the API, mode is master or slave.
CoilA Modbus point that holds on or off.
RegisterA Modbus point that holds a number.
Long pollingA request that the Edge Server holds open until the result is final or the wait time ends.
etagA version tag of the Modbus configuration. You send it back in If-Match.
Idempotency keyYour ID for one request. The Edge Server does a request with the same key one time only.
OperationOne deploy: the transfer of one configuration version to the Controller.

Set two shell variables. Use your Edge Server address and the token from recipe 1:

EDGE="https://<your Edge Server address>:8443"
TOKEN="<token from recipe 1>"

On the Edge Server computer itself, the address is localhost. The Edge Server certificate is self-signed. If your client does not trust it, add -k to each curl command.

The examples use the Controller serial F1000000. No Controller has this serial. Use the serial printed on your Controller.

1. Make a token

Make the token

  1. Open a terminal on the Edge Server computer.
  2. Go to the install folder (/opt/growbud-edge on Linux and macOS).
  3. Run this command. On Linux, put sudo in front of it.
docker compose exec outlets node /app/edge-outlets.mjs token create \
  --email you@example.com --name "BMS" --days 365 --scope control

The answer:

Token for you@example.com ("BMS"), id <token id>, scopes sensing control, expires 2027-11-08T10:00:00.000Z.
It is shown once. Keep it secret.

<token>
  1. Copy the token. The command shows it one time only.
OptionMeaning
--emailThe user the token acts as. The user must exist on this Edge Server.
--nameA label, 1 to 100 characters.
--days1 to 3650. The default is 365.
--scope controlAdds the control scope. Without it, the token can only read.

A user can have 10 active tokens. List and revoke tokens with these commands:

docker compose exec outlets node /app/edge-outlets.mjs token list
docker compose exec outlets node /app/edge-outlets.mjs token revoke <token id>

What each token can do

RoutesSensing tokenControl tokenControl token of an organization admin
GET /sensing/v1/...YesYesYes
Switch an output, write a Modbus point403YesYes
Change the Modbus configuration or a profile403403Yes
  • A token works only on the Edge Server that made it.
  • The token acts as its user. It sees only the Controllers of the user's organization. Any other serial gives 404, not 403.
  • A revoked or expired token gets 401.
  • The Edge Server records each control call, refused or not.

Check the token

curl -s -H "Authorization: Bearer $TOKEN" "$EDGE/sensing/v1/controllers" | jq '.controllers[].serial'

A good token gives the serials. A bad token gives 401:

{ "error": { "code": "unauthenticated", "message": "Send a valid Edge Server API token or app session: Authorization: Bearer <token>." } }

After 30 failed calls in one minute from one client, the Edge Server answers 429 rate_limited. Wait one minute.

2. Read Controllers

List the Controllers

curl -s -H "Authorization: Bearer $TOKEN" "$EDGE/sensing/v1/controllers"

Answer 200:

{
  "controllers": [
    {
      "serial": "F1000000",
      "firmwareVersion": "5.4.0",
      "mode": "auto",
      "lastSeen": "2026-11-08T10:00:05.000Z",
      "links": { "wired": false, "wiredSeenAt": null, "lorawan": true },
      "telemetryAt": "2026-11-08T10:00:05.000Z",
      "outputs": [
        {
          "channel": 0,
          "label": "Pump",
          "deviceType": "pump",
          "state": "off",
          "reportedState": "off",
          "levelPct": null,
          "currentMa": 0,
          "fault": "none",
          "reportedAt": "2026-11-08T10:00:05.000Z",
          "pending": null,
          "desired": null
        }
      ],
      "faults": [],
      "schedule": { "version": 7, "ackedVersion": 7, "status": "in_sync" }
    }
  ]
}

The example shows one output. outputs always has four entries, channels 0 to 3.

The app numbers the outputs 1 to 4. The API numbers them 0 to 3.

FieldMeaning
stateon, off or unknown. A valve reports open or closed in reportedState, and state shows it as on or off.
pendingThe open command for the output, or null.
desiredThe state that a switch with "retry": true holds, or null.
links.wiredtrue when the Edge Server saw the Controller on the wired link (RS-485) in the last 5 minutes. Commands then use the wired link.
schedule.statusin_sync, pending or never_confirmed.

Read one Controller

curl -s -H "Authorization: Bearer $TOKEN" "$EDGE/sensing/v1/controllers/F1000000"

Answer 200: { "controller": { ... } }, with the same fields as one list entry. The serial is not case sensitive. An unknown serial, or a serial of another organization, gives 404:

{ "error": { "code": "not_found", "message": "No Controller with this serial." } }

3. Switch an output

Send one command

curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"on": true}' "$EDGE/control/v1/controllers/F1000000/outputs/0/switch?wait=10"

The Edge Server sends one output_on command. It does not send it again.

Answer 200 when the Controller confirmed in the wait time, or 202 when it did not answer yet:

{
  "command": {
    "id": "7b0c2f8e-3c1d-4e0a-9a51-2f4d6f1d9a10",
    "serial": "F1000000",
    "type": "output_on",
    "channel": 0,
    "requested": "on",
    "transport": "lora",
    "status": "pending",
    "final": false,
    "stage": "sent",
    "requestedAt": "2026-11-08T10:01:00.000Z",
    "sentAt": "2026-11-08T10:01:00.400Z",
    "resolvedAt": null,
    "expiresAt": "2026-11-08T10:06:00.000Z",
    "latencyMs": null,
    "reason": null,
    "error": null
  },
  "poll": "/sensing/v1/controllers/F1000000/commands/7b0c2f8e-3c1d-4e0a-9a51-2f4d6f1d9a10"
}

Read the poll path to get the result (recipe 4).

statusMeaning
pendingNo answer yet. stage is queued or sent.
confirmedThe Controller did the command.
refusedThe Controller refused the command. reason gives the cause, for example LEAK_INTERLOCK.
timed_outNo answer by expiresAt (5 minutes). A late answer can still change it to confirmed or refused.
failedThe Edge Server could not send the command.

transport is lora or rs485 (the wired link, RS-485).

Hold a state until the Controller confirms

Add "retry": true to make the Edge Server send again until the Controller confirms:

curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"on": true, "retry": true}' "$EDGE/control/v1/controllers/F1000000/outputs/0/switch?wait=10"

Answer 202 while the Edge Server sends, or 200 when it is done:

{
  "desired": {
    "on": true,
    "status": "converging",
    "setAt": "2026-11-08T10:01:00.000Z",
    "attempts": 1,
    "lastCommandId": "7b0c2f8e-3c1d-4e0a-9a51-2f4d6f1d9a10",
    "convergedAt": null,
    "endedAt": null,
    "error": null
  },
  "command": { "id": "7b0c2f8e-3c1d-4e0a-9a51-2f4d6f1d9a10", "status": "pending", "...": "..." },
  "poll": "/sensing/v1/controllers/F1000000"
}

The Edge Server sends at most 5 commands in 10 minutes. It stops when the Controller confirms or refuses. The Edge Server does not send again after a refusal. A refusal is a safety decision.

desired.statusMeaning
convergingThe Edge Server is sending.
convergedThe Controller confirmed the state.
failedThe Controller refused, or 10 minutes passed.
overriddenThe output changed later without a command from the Edge Server, for example from a button, a schedule or a rule.
supersededA newer switch replaced this one.

To follow it, read outputs[].desired in GET /sensing/v1/controllers/{serial}.

The Controller card in the desktop app with Ch 1 on after an API switch to channel 0
API channel 0 is Ch 1 in the app. It is on.

Switch an Outlet

An Outlet uses the same body on its own route. {devEui} is the DevEUI of the Outlet, the 16-character ID on the Outlet label:

curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"on": false}' "$EDGE/control/v1/outlets/<devEui>/switch?wait=30"

The answer has command, poll and outlet. The poll path is /control/v1/outlets/{devEui}/commands/{commandId}.

4. Wait for a result (long polling)

Add ?wait=N to hold the answer until the result is final or N seconds pass. N is 0 to 30. The default is 0.

curl -s -H "Authorization: Bearer $TOKEN" \
  "$EDGE/sensing/v1/controllers/F1000000/commands/7b0c2f8e-3c1d-4e0a-9a51-2f4d6f1d9a10?wait=30"

Answer 200:

{
  "command": {
    "id": "7b0c2f8e-3c1d-4e0a-9a51-2f4d6f1d9a10",
    "status": "confirmed",
    "final": true,
    "stage": "answered",
    "resolvedAt": "2026-11-08T10:01:03.100Z",
    "latencyMs": 3100,
    "...": "..."
  }
}

Follow these rules:

  1. Send the control call with a wait value, for example wait=10.
  2. If the answer is 200, the result is final. Stop.
  3. If the answer is 202, read the poll path with wait=30.
  4. Read the poll path again until final is true.
Routewait holds until
GET .../commands/{commandId}The command is final.
GET .../modbus/operations/{operationId}The operation is final.
POST .../outputs/{ch}/switchThe command is final (with retry, the desired state ends).
POST .../modbus/points/{key}/writeThe write is final.
PUT or PATCH .../modbus/config, POST .../modbus/deployThe operation is final.

A wait value above 30 gives 400:

{ "error": { "code": "invalid_argument", "message": "wait must be 0 to 30 seconds." } }

A held request takes up to 30 seconds. Set your client timeout to more than 30 seconds.

5. Configure Modbus devices

This recipe connects a Waveshare Modbus RTU Relay board (8 relays) to the Controller's RS-485 port. The Controller is the bus master. Steps 4, 5 and 6 (cancel and deploy), and a profile change, need a control token of an organization admin. The reads in steps 1, 2, 3, 6 and 7 take any token. Step 8 takes any control token. A user that is not an admin gets 403 on an admin step:

{ "error": { "code": "permission_denied", "message": "Only an organization admin may change the Modbus configuration or profiles." } }

Step 1: Read the capabilities

curl -s -H "Authorization: Bearer $TOKEN" "$EDGE/sensing/v1/controllers/F1000000/modbus/capabilities"

Answer 200:

{
  "capabilities": {
    "contract": "flo-modbus-control",
    "device_id": "F1000000",
    "status": "reported",
    "firmware_version": "5.4.0",
    "limits": {
      "max_devices": 77,
      "max_profiles": 8,
      "max_entries_per_profile": 16,
      "max_active_read_slots": 105,
      "...": "..."
    },
    "read_function_codes": [1, 2, 3, 4],
    "write_function_codes": [5, 6, 15, 16],
    "data_types": ["bit", "u16", "i16", "u32", "i32", "float32"],
    "word_orders": ["ABCD", "CDAB", "BADC", "DCBA"],
    "features": { "...": "..." }
  }
}

status is reported, model_default or unknown. With unknown, a deploy that needs the limits gives 422 CAPABILITIES_UNKNOWN.

Step 2: Find the profile

A profile is the register map of one device model. A configuration names a profile by profile_id and revision.

curl -s -H "Authorization: Bearer $TOKEN" "$EDGE/sensing/v1/controllers/F1000000/modbus/profiles" \
  | jq '.profiles[] | {profile_id, revision, scope, name}'

Answer 200 (one profile shown):

{
  "profiles": [
    {
      "profile_id": "9bcb80cf-41df-4dc2-a45e-ae090d7f0bd9",
      "revision": 1,
      "scope": "system",
      "preset_key": "waveshare_modbus_rtu_relay_8ch",
      "name": "Waveshare Modbus RTU Relay (8ch)",
      "manufacturer": "Waveshare",
      "model": "Modbus RTU Relay",
      "entries": [
        {
          "entry_id": "relay_1",
          "name": "Relay 1",
          "read": { "function_code": 1, "...": "..." },
          "write": { "function_code": 5, "...": "..." },
          "...": "..."
        }
      ],
      "created_at": "2026-11-01T09:00:00.000Z",
      "...": "..."
    }
  ]
}

Without a query, the answer has the current revision of each profile. Add ?profile_id=<id>&revision=<n> to read one revision. Keep profile_id, revision and the entry_id values you want to read.

A system profile comes from the Growbud device library and is read-only. To change a register map, add your own profile with PUT /control/v1/controllers/{serial}/modbus/profiles (admin). A saved revision never changes. A newer revision than your base_revision gives 409 with reason MODBUS_PROFILE_REVISION_CONFLICT.

Step 3: Read the configuration

curl -s -H "Authorization: Bearer $TOKEN" "$EDGE/sensing/v1/controllers/F1000000/modbus/config"

Answer 200:

{
  "config": {
    "contract": "flo-modbus-control",
    "device_id": "F1000000",
    "etag": "modbus:0:sha256:...",
    "desired_version": 0,
    "desired_digest": "sha256:...",
    "device_acked_version": 0,
    "device_acked_digest": "sha256:...",
    "sync_status": "in_sync",
    "last_error": null,
    "config": {
      "mode": "slave",
      "bus": { "slave_address": 1, "baud": 9600, "parity": "none", "stop_bits": 1 },
      "devices": []
    },
    "capacity": { "...": "..." },
    "last_operation": null
  }
}

A Controller with no configuration gives this empty default at version 0, not 404.

  1. Keep etag. Send it in the If-Match header of the change.
  2. Keep desired_version. Send it as base_desired_version in the body of the change.
sync_statusMeaning
in_syncThe Controller runs the desired version.
draftThe Edge Server saved the configuration. No operation sends it.
queuedAn operation exists. It has sent nothing yet.
deployingThe operation is sending.
awaiting_device_commitThe transfer is done. The Controller has not confirmed it.
blockedA capability or safety rule stops the deploy.
errorThe last deploy ended without the Controller applying it.

Step 4: Validate the change

Validate saves nothing. Send the full configuration as snapshot:

curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d @relay-config.json "$EDGE/control/v1/controllers/F1000000/modbus/config/validate"

relay-config.json:

{
  "base_desired_version": 0,
  "snapshot": {
    "mode": "master",
    "bus": { "slave_address": 1, "baud": 9600, "parity": "none", "stop_bits": 1 },
    "devices": [
      {
        "id": "ws8",
        "name": "Waveshare relay",
        "address": 1,
        "profile": { "profile_id": "9bcb80cf-41df-4dc2-a45e-ae090d7f0bd9", "revision": 1 },
        "enabled": true,
        "read_entry_ids": ["relay_1", "relay_2"]
      }
    ]
  }
}

Answer 200:

{
  "validation": {
    "valid": true,
    "deployable": true,
    "capacity": { "...": "..." },
    "warnings": [],
    "errors": [],
    "projected_config": { "...": "..." }
  }
}

A configuration that is not valid gives 422. details.validation.errors lists each error with code, path and message. Planned bus use above 70 percent gives the warning BUS_UTILIZATION_HIGH. An unknown field in snapshot gives 422 with the error UNKNOWN_FIELD. An unknown field in the body gives 400 UNKNOWN_FIELD.

The device id is your own stable name for the device. It is not the Modbus address. Each point key starts with it, for example ws8.relay_1.

Step 5: Replace the configuration

Send the same body with PUT. Put the etag from step 3 in If-Match:

curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -H 'If-Match: "modbus:0:sha256:..."' \
  -H 'Idempotency-Key: 6f1d9a10-2f4d-4e0a-9a51-3c1d7b0c2f8e' \
  -d @relay-config.json "$EDGE/control/v1/controllers/F1000000/modbus/config?wait=30"

Answer 202 while the Controller has not applied it, or 200 when the operation is final in the wait time:

{
  "result": {
    "accepted": true,
    "device_id": "F1000000",
    "previous_version": 0,
    "desired_version": 1,
    "desired_digest": "sha256:...",
    "etag": "modbus:1:sha256:...",
    "operation_id": "160ece7b-d72b-4014-9503-f944e4a1e435",
    "sync_status": "queued",
    "capacity": { "...": "..." },
    "warnings": []
  },
  "operation": { "operation_id": "160ece7b-d72b-4014-9503-f944e4a1e435", "status": "transferring", "...": "..." },
  "poll": "/sensing/v1/controllers/F1000000/modbus/operations/160ece7b-d72b-4014-9503-f944e4a1e435"
}
  • operation is null when wait is 0.
  • If the content is the same as the saved content, the answer is 200 with "content_unchanged": true and no operation_id. The Edge Server sends nothing.
  • A large change can take some minutes on LoRaWAN. Follow the operation in step 6.

To change one part only, use PATCH on the same path. The body has base_desired_version and operations (1 to 16 items):

{
  "base_desired_version": 1,
  "operations": [
    { "op": "upsert_device", "device": { "id": "ws8", "name": "Waveshare relay", "address": 1, "profile": { "profile_id": "9bcb80cf-41df-4dc2-a45e-ae090d7f0bd9", "revision": 1 }, "enabled": true, "read_entry_ids": ["relay_1"] } }
  ]
}
opOther field
set_modemode: master or slave
set_busbus: slave_address, baud, parity, stop_bits
upsert_devicedevice: one complete device
delete_devicedevice_id

Step 6: Follow the operation

curl -s -H "Authorization: Bearer $TOKEN" \
  "$EDGE/sensing/v1/controllers/F1000000/modbus/operations/160ece7b-d72b-4014-9503-f944e4a1e435?wait=30"

Answer 200:

{
  "operation": {
    "operation_id": "160ece7b-d72b-4014-9503-f944e4a1e435",
    "device_id": "F1000000",
    "target_version": 1,
    "status": "applied",
    "progress": { "pages_total": 2, "pages_sent": 2, "pages_acked": 2, "...": "..." },
    "cancellation": { "cancellable": false, "mode": "none", "blocker": "terminal" },
    "applied_at": "2026-11-08T10:17:40.000Z",
    "error": null,
    "...": "..."
  }
}

Read the operation again until status is final.

Kindstatus values
Activequeued, dispatching, transferring, awaiting_device_commit, cancel_requested
Finalapplied, rejected, failed, expired, superseded, cancelled

applied means the Controller confirmed the new version. Then sync_status in the configuration is in_sync.

To list operations, use GET .../modbus/operations?view=active. view is active, history or all. limit is up to 100 (default 50). Use cursor with the next_cursor of the last answer.

The firmware version point and the audit trail with one applied operation
The same operation in the Growbud app: version 0 to 1, applied.

To stop an operation that the Controller has not taken yet:

curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"target_version": 1}' \
  "$EDGE/control/v1/controllers/F1000000/modbus/operations/160ece7b-d72b-4014-9503-f944e4a1e435/cancel"

target_version must be the target_version of the operation. Answer 200: { "result": { "operation_id": "...", "status": "cancelled", "sync_status": "draft", "...": "..." } }. The saved configuration stays. To send it again, use POST .../modbus/deploy with If-Match and the body { "target_version": 1 }.

Step 7: List the points

curl -s -H "Authorization: Bearer $TOKEN" "$EDGE/sensing/v1/controllers/F1000000/modbus/points"

Answer 200:

{
  "configVersion": 1,
  "points": [
    {
      "key": "ws8.relay_1",
      "id": "3f9a1c52-6b7e-4d1a-9c0f-2a8e5b7d4c11",
      "device": "ws8",
      "entry": "relay_1",
      "name": "Relay 1",
      "measurement": "generic",
      "unit": "none",
      "valueType": "boolean",
      "access": "read_write",
      "writable": true
    }
  ]
}
  • The list follows the configuration that the Controller confirmed.
  • configVersion is null and points is empty until the Controller confirms a configuration.
  • A read-only point is in the list only when the configuration reads it. A writable point is always in the list.
Process points for the Waveshare relay, with Relay 1 to Relay 4 and their stable keys
The same points in the Growbud app. The key is what the API uses.

Step 8: Write a point

Any user with a control token can write a point. It does not need an admin.

curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: relay-1-on-20261108-1020' \
  -d '{"value": true}' "$EDGE/control/v1/controllers/F1000000/modbus/points/ws8.relay_1/write?wait=30"
FieldMeaning
valuetrue or false for a coil (on or off). A number for a register. The label for a register with named values.
verifyOptional. Read the register back after the write. The default is true.

In the path, use the point key or its id.

Step 9: Read the value back

The point list has no current value. The confirmed value is in the write result. Answer 200 when the write is final in the wait time, or 202 before:

{
  "command": {
    "id": "9d2e7a40-1b2c-4d3e-8f90-a1b2c3d4e5f6",
    "type": "modbus_write",
    "status": "confirmed",
    "final": true,
    "transport": "lora",
    "modbus": {
      "point": "ws8.relay_1",
      "value": true,
      "configVersion": 1,
      "verify": true,
      "deviceStatus": "ok",
      "exceptionCode": null,
      "attempts": 1,
      "verified": true,
      "readbackRaw": 1
    },
    "...": "..."
  },
  "poll": "/sensing/v1/controllers/F1000000/commands/9d2e7a40-1b2c-4d3e-8f90-a1b2c3d4e5f6"
}
FieldMeaning
deviceStatusThe answer of the Modbus device, for example ok, exception or timeout. null until the Controller reports.
exceptionCodeThe Modbus exception code, or null.
verifiedtrue when the read-back value is the written value. null when verify is false.
readbackRawThe raw value that the Controller read back.

A 202 answer has a poll path. Read it with wait=30 (recipe 4).

The write goes over LoRaWAN only. The write is fixed to the configuration version that the Controller confirmed, so it cannot land on a register of a newer map.

6. ETag and Idempotency-Key rules

If-Match (Modbus configuration only)

RouteIf-Match
PUT .../modbus/configRequired
PATCH .../modbus/configRequired
POST .../modbus/deployRequired
POST .../modbus/config/validateNot used
POST .../modbus/operations/{operationId}/cancelNot used
PUT .../modbus/profilesNot used. Send base_revision in the body.
  1. Read etag from the body of GET .../modbus/config. The API does not send an ETag header.
  2. Send it in quotes: If-Match: "modbus:0:sha256:...". The tag has the form modbus:<desired_version>:sha256:<digest>. Send it exactly as you read it.
  3. Send one value only. A weak tag (W/) or a list gives 400 with reason INVALID_IF_MATCH.
  4. After each change, use the new etag from result.etag.

Idempotency-Key

The header is optional on every route that takes it. It has 8 to 128 characters. A shorter or longer key gives 400.

You sendThe Edge Server does
No keyMakes a new key for the one call. A retry is a new request.
A new keyDoes the request and keeps the answer.
The same key and the same bodyGives the first answer again. It does not do the request again.
The same key and a different bodyGives 409 with reason IDEMPOTENCY_KEY_REUSED.

Send your own key when a retry must not act twice. Make a new key for each new request. A point write is the most important case: without a key, a retry after a network error writes the point again.

The output switch takes no Idempotency-Key. Each call sends a new command.

7. Handle errors

Each error has this shape:

{
  "error": {
    "code": "failed_precondition",
    "message": "The desired configuration changed; read it again before retrying",
    "reason": "CONFIG_REVISION_CONFLICT",
    "details": { "current_etag": "modbus:1:sha256:...", "current_desired_version": 1 }
  }
}

code is the API code. reason and details are present when the Controller software gives them. For 409, act on reason. One reason can come with more than one code.

409 Conflict

codereasonDo this
managed_elsewhereThis Edge Server is on standby. Growbud Cloud manages the Controller. Do not send again.
failed_preconditionCAPABILITY_MISMATCHThe channel has no load. Configure the channel in the app.
failed_preconditionMODE_LOCKEDThe Controller cannot take the command on its current link. Check links.wired and links.lorawan.
failed_precondition or conflictMODBUS_CONFIG_NOT_ACKNOWLEDGEDThe Controller has not confirmed a Modbus configuration. Wait. Then read the point list again.
conflictIDEMPOTENCY_KEY_REUSEDYou used the key for a different request. Make a new key.
conflictCONFIG_OPERATION_ACTIVEAnother deploy is active. Follow it (step 6), then deploy.
conflictCONFIG_OPERATION_TARGET_MISMATCHThe cancel target_version is not the operation's. Read the operation again.
conflictCONFIG_OPERATION_NOT_CANCELLABLEThe operation cannot stop now. Follow it to the end.
conflictMODBUS_PROFILE_REVISION_CONFLICTA newer profile revision exists. Read the profile again.

412 and 428 Precondition

HTTPreasonDo this
428PRECONDITION_REQUIREDAdd the If-Match header.
412CONFIG_REVISION_CONFLICTThe configuration changed since you read it. Do the three steps below.

Both have code failed_precondition. Do not send the same request again without a new read. It fails the same way.

After CONFIG_REVISION_CONFLICT:

  1. Read the configuration again (step 3).
  2. Apply your change to the new configuration.
  3. Send the change with the new etag and desired_version.

422 Not valid

All have code invalid_argument.

reasonDo this
A validation code, with details.validationFix each item in details.validation.errors. Validate again (step 4).
CAPABILITIES_UNKNOWNThe Controller has not reported its capabilities. Wait for it to report.
CONFIG_ABORT_UNSUPPORTEDThe operation is in flight and cannot stop. Follow it to the end.
MODBUS_ENTRY_NOT_WRITABLEThe point is read-only.
MODBUS_WRITE_VALUE_OUT_OF_RANGE, MODBUS_WRITE_VALUE_TYPE_MISMATCH, MODBUS_ENUM_VALUE_INVALIDThe value does not fit the point. Check valueType.
MODBUS_MASTER_MODE_REQUIREDThe Controller is not the bus master. Set mode to master (step 5).
MODBUS_WRITE_CAPABILITY_UNSUPPORTEDThe Controller firmware cannot do this write.

Other errors

HTTPcodeDo this
400invalid_argumentFix the body or query. The message says which field. A body that is not JSON gives Send JSON.
401unauthenticatedMake a new token (recipe 1).
403permission_deniedUse a token with the control scope. For Modbus configuration, use an admin's token.
404not_foundCheck the serial, channel, point key or id. Check that the Edge Server release serves the route. See Compatibility.
405invalid_argumentUse the method in the message.
413invalid_argumentreason TOO_MANY_OPERATIONS: the change has more operations than details.limit. Send it in parts.
429rate_limitedWait one minute.
502, 503unavailableTry again after a short time.

Next