How to push a configuration to an agent
Stage a configuration change, sync the device and then push; you will learn how to reproduce these actions with Python code.
Introduction
When you change an agent's configuration through the API (add a data source, add variables, add a VNC server, and more), the change is staged in IXON Cloud. It only reaches the device once you push the configuration.
On this page, you will learn about the API equivalent of the Push config to device action in Fleet Manager.
The AgentListConfigurationPush endpoint
AgentListConfigurationPush endpointPushing is done with AgentListConfigurationPush:
curl --request POST \
--url https://portal.ixon.cloud:443/api/agents/configuration/push \
--header 'Api-Application: <your_application_id>' \
--header 'Api-Company: <your_company_id>' \
--header 'Api-Version: 2' \
--header 'accept: application/json' \
--header 'authorization: Bearer <your_bearer_token>' \
--header 'content-type: application/json' \
--data '
[
{
"publicId": "<your_agent_id>"
}
]
'A successful call returns:
{
"type": "Null",
"data": null,
"status": "success"
}"Success" does not mean "done"
The push call returns success immediately, but the gateway applies the configuration in the background. In Fleet Manager, this is the moment the config button turns blue ("Synchronizing..."). When the device has finished applying and reported back, the button turns white again ("Device config in sync").
This background gap can cause problems, so the safest option is to push, then wait until the device reports back before doing anything else.
When can a configuration be pushed?
Two different conditions matter before a push:
mdrServeris notnull.- The device must not be mid-apply from a previous push.
Keep in mind that an agent can be online while a push still fails. The Agent not online push error can appear when:
- The config button is blue: a previous push is still being applied (config not yet in sync). Pushing again during this window fails and may generate an
Agent not onlineerror. Pushing once the button is white succeeds. mdrServerisnull: without the MDR/MQTT connection, a push cannot be delivered even if the VPN connection is active.
Undoing a change still requires a push!If a variable is created, the button turns blue and fidplays "Push device config". If the variable gets deleted, the button will retain its state. This happens because other changes have been applied in the background and, despite the deletion of the variable, they remain pending in the config file.
To clear this state, all you need to do is pushing once more.
How to read the sync state
To know whether a configuration is in sync, the developer can perform a check on two different conditions:
1. config.differentConfigs (boolean)
This field can be found in the agent's config, and the values can be read as such:
false: in sync. The "Device config in sync" white button is visible in the Fleet Manager.true: not in sync. The configuration still needs to be pushed. The blue "Push device config" button is visible in the Fleet Manager.
curl --request GET \
--url 'https://portal.ixon.cloud/api/agents/<agent_id>?fields=config.differentConfigs' \
--header 'Api-Version: 2' \
--header 'Api-Application: <application_id>' \
--header 'Api-Company: <company_id>' \
--header 'Authorization: Bearer <bearer_token>'Example: differentConfigs is false, so the device is in sync:
{
"status": "success",
"type": "Agent",
"data": {
"publicId": "ay403xfX4zGi",
"config": {
"differentConfigs": false
}
}
}2. config.configPushedOn & config.configReportedOn (timestamps)
Two timestamps on the agent's config:
configPushedOn: when the portal last sent config to the device.configReportedOn: when the device last reported back which configuration it has.
Whichever timestamp is more recent tells you the state:
- If
configPushedOnis the more recent one: the device hasn't reported back since that push, so it's not in sync (blue button). - If
configReportedOnis the more recent one: it has already sent back the latest config, so it is in sync (white button).
Written as a check, that's simply configReportedOn >= configPushedOn for "in sync".
curl --request GET \
--url 'https://portal.ixon.cloud/api/agents/<agent_id>?fields=config(configPushedOn,configReportedOn)' \
--header 'Api-Version: 2' \
--header 'Api-Application: <application_id>' \
--header 'Api-Company: <company_id>' \
--header 'Authorization: Bearer <bearer_token>'Example: the config was sent at 12:18:56 and the device reported at 12:19:04. The report is more recent, so the device is in sync:
{
"status": "success",
"type": "Agent",
"data": {
"publicId": "ay403xfX4zGi",
"config": {
"configPushedOn": "2026-07-22T12:18:56Z",
"configReportedOn": "2026-07-22T12:19:04Z"
}
}
}Which one should you use?
Pick either option by the question you are answering:
- Are there changes waiting to be pushed? (including changes staged but not yet pushed): As this resembles a general check,
differentConfigsis the easiest, simplest option to display a current synced/not synced state. - Did my push apply / is the device back in sync?:
differentConfigsworks too, but prefer using theconfigReportedOn >= configPushedOncondition when confirmation is critical. It proves that the device reported back after your push.
Recommended flow for integrations
- Stage the change with the relevant API call (e.g. create a variable on a data source).
- Confirm the change was staged (e.g. the create call returned a
publicId). - Wait until the agent is in sync (
differentConfigs == false, orconfigReportedOn >= configPushedOn) so you are not pushing on top of an in-flight push. - Push the configuration.
- Wait until in sync again to confirm the device applied your push (button white) before reporting success or issuing the next push.
Step-by-step with code
The example below stages a new variable, verifies it, checks the device is reachable, pushes, and polls until the device reports back in sync. Set the environment variables (or edit the defaults) before running.
Prerequisites
pip install requestsEnvironment variables used:
IXON_BASE_URL (default https://portal.ixon.cloud/api)
IXON_API_VERSION (default 2)
IXON_APPLICATION_ID
IXON_BEARER_TOKEN
IXON_COMPANY_ID
IXON_AGENT_ID
IXON_DATA_SOURCE_ID
IXON_NEW_VARIABLE_NAME (e.g. Temperature)
The script
import os
import sys
import time
import logging
from datetime import datetime
import requests
BASE_URL = "https://portal.ixon.cloud/api"
API_VERSION = os.getenv("IXON_API_VERSION", "2")
APPLICATION_ID = os.getenv("IXON_APPLICATION_ID", "<your-application-id>")
BEARER_TOKEN = os.getenv("IXON_BEARER_TOKEN", "<your-bearer-token>")
COMPANY_ID = os.getenv("IXON_COMPANY_ID", "<your-company-publicId>")
AGENT_ID = os.getenv("IXON_AGENT_ID", "<your-agent-publicId>")
DATA_SOURCE_ID = os.getenv("IXON_DATA_SOURCE_ID", "<your-data-source-publicId>")
_VARIABLE_NAME = os.getenv("IXON_NEW_VARIABLE_NAME", "ABCTest")
NEW_VARIABLE = {
"name": _VARIABLE_NAME,
"slug": os.getenv("IXON_NEW_VARIABLE_SLUG", _VARIABLE_NAME.lower()),
"type": "int", # "int" | "bool" | "str" | ...
"signed": True,
"width": "16",
"address": os.getenv("IXON_NEW_VARIABLE_ADDRESS", "1.8"),
}
POLL_INTERVAL = 5 # seconds between sync checks
POLL_TIMEOUT = 300 # fail-safe (logging changes can take longer to apply)
REQUEST_TIMEOUT = 30
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-7s %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger("ixon")
# HTTP helpers
def build_headers() -> dict:
return {
"Api-Version": API_VERSION,
"Api-Application": APPLICATION_ID,
"Api-Company": COMPANY_ID,
"Authorization": f"Bearer {BEARER_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
}
def api_request(method: str, url: str, **kwargs) -> dict:
resp = requests.request(
method, url, headers=build_headers(), timeout=REQUEST_TIMEOUT, **kwargs
)
resp.raise_for_status()
if not resp.content:
return {}
body = resp.json()
if isinstance(body, dict) and body.get("status") == "error":
raise RuntimeError(f"IXAPI error on {method} {url}: {body}")
return body
def get_agent(agent_id: str, fields: str) -> dict:
"""GET a single agent and return its `data` object."""
url = f"{BASE_URL}/agents/{agent_id}"
return api_request("GET", url, params={"fields": fields}).get("data") or {}
def parse_ts(value: str | None) -> datetime | None:
"""Parse an IXON ISO-8601 timestamp like '2026-07-22T12:18:56Z'."""
if not value:
return None
return datetime.fromisoformat(value.replace("Z", "+00:00"))
# Config / sync state
def get_config_timestamps(agent_id: str) -> tuple:
"""
Return (configPushedOn, configReportedOn) as datetimes (or None).
These are the signal the portal itself polls to decide the button colour.
"""
config = get_agent(
agent_id, "config(configPushedOn,configReportedOn)"
).get("config", {}) or {}
return parse_ts(config.get("configPushedOn")), parse_ts(config.get("configReportedOn"))
# Guard: is the agent ready to receive a push?
def is_ready_to_push(agent_id: str) -> bool:
"""
A push is delivered over MDR/MQTT, so mdrServer must be populated. An agent
can be 'online' via VPN alone yet still not be pushable.
"""
mdr = get_agent(agent_id, "mdrServer").get("mdrServer")
if not mdr:
log.error("Agent not ready to push (mdrServer is empty).")
return False
return True
def add_variable(agent_id: str, source_id: str, variable: dict) -> str | None:
"""
Create a variable on a data source and return its new publicId.
Endpoint: AgentDataVariableList — POST /agents/{agentId}/data-variables
"""
url = f"{BASE_URL}/agents/{agent_id}/data-variables"
payload = {**variable, "source": {"publicId": source_id}}
log.info("Creating variable %r (slug=%r) on data source %s",
variable.get("name"), variable.get("slug"), source_id)
resp = api_request("POST", url, json=payload)
return (resp.get("data") or {}).get("publicId")
# The push ("press the button")
def trigger_push(agent_id: str) -> None:
"""
Trigger the configuration push, matching exactly what Fleet Manager sends:
a POST to /agents/configuration/push with the AGENT's publicId as a bare
object. (The endpoint also accepts an array of such objects to push several
agents at once.) Returns immediately with status "success"; the device
applies the config in the background.
"""
url = f"{BASE_URL}/agents/configuration/push"
payload = {"publicId": agent_id}
log.info("Triggering configuration push (agent=%s)", agent_id)
api_request("POST", url, json=payload)
log.info("Push queued.")
# Wait until the device reports back after the push (button white)
def wait_for_sync(agent_id: str, reported_after: datetime | None = None) -> bool:
"""
Poll until the device is in sync: configReportedOn is at or after
configPushedOn (and, when reported_after is given, strictly newer than it).
Passing reported_after right after a push proves the device reported THIS
push, not an earlier sync. When configPushedOn is null (never pushed), the
device is treated as already in sync.
"""
deadline = time.monotonic() + POLL_TIMEOUT
log.info("Polling for sync (interval=%ss, timeout=%ss)", POLL_INTERVAL, POLL_TIMEOUT)
while time.monotonic() < deadline:
pushed_on, reported_on = get_config_timestamps(agent_id)
in_sync = pushed_on is None or (reported_on is not None and reported_on >= pushed_on)
advanced = reported_after is None or (reported_on is not None and reported_on > reported_after)
if in_sync and advanced:
log.info("In sync (pushed=%s, reported=%s).", pushed_on, reported_on)
return True
log.info("Still syncing (pushed=%s, reported=%s). Waiting %ss...",
pushed_on, reported_on, POLL_INTERVAL)
time.sleep(POLL_INTERVAL)
log.error("Timed out after %ss — device did not report after the push.", POLL_TIMEOUT)
return False
# Orchestration: change -> verify change -> wait in sync -> push -> verify sync
def apply_change_and_sync(agent_id: str) -> bool:
"""
Create the variable, confirm it was created (the create call returns its
publicId), make sure we are not pushing on top of an in-flight push, then
push and wait until the device reports in sync.
"""
# Make the change. A returned publicId confirms the resource was created.
new_variable_id = add_variable(agent_id, DATA_SOURCE_ID, NEW_VARIABLE)
if not new_variable_id:
log.error("Variable not created — no publicId returned, nothing to push.")
return False
log.info("Resource changed: created variable %s", new_variable_id)
# Guard: is the agent reachable for a push?
if not is_ready_to_push(agent_id):
log.error("Aborting: agent not ready for push.")
return False
# Make sure any previous push has settled (button white) before pushing.
if not wait_for_sync(agent_id):
log.error("Aborting: previous configuration still not in sync.")
return False
# Record the current report time, then push. We then wait for a NEWER report,
# so we confirm THIS push applied rather than an earlier sync.
_, reported_before = get_config_timestamps(agent_id)
trigger_push(agent_id)
return wait_for_sync(agent_id, reported_after=reported_before)
def main() -> int:
try:
if apply_change_and_sync(AGENT_ID):
print("Device in sync")
return 0
return 2
except requests.HTTPError as exc:
log.error("HTTP error: %s — %s", exc, getattr(exc.response, "text", ""))
return 3
except (requests.RequestException, RuntimeError) as exc:
log.error("Request failed: %s", exc)
return 3
if __name__ == "__main__":
sys.exit(main())Notes
- Reconnects during setup. Changing anything WAN-related (Ethernet, Wi-Fi, cellular) can cause the device to reconnect, briefly dropping
activeVpnSession/mdrServer. Handle this by waiting for the connection to return before pushing again, rather than treating the momentary drop as a hard failure. - Repeated pushes. Do not fire a second push while the first is still applying (button blue). Always wait for in sync between pushes.
- Do you even need to push? If your only change is a value on an agent's custom field, a
PATCHto the Agent is usually enough and no push is required. Pushing is needed when you change the device configuration itself (data sources, variables, servers, alarms, network configurations and more).
Related
- AgentListConfigurationPush endpoint reference
- Manage devices — configuration difference
- How to check if an agent is online
Updated about 1 hour ago
