Instant Transfer
Instant transfer is a method that sends files and folders directly from a source device to a target device with a single API call, without scheduling or automation.
Overview
What Is Instant Transfer?
Transfer a specific file/folder from a source device to a designated path on a target device in a single operation. When you create a transfer, a monitorId is issued. Use this ID to check progress and perform controls including pause, resume, cancel, and retry.
Common Setup
Base URL
https://app.innorix.com
Authentication Headers — The following headers are required for every request.
| Header | Description |
|---|---|
Authorization: Bearer {accessToken} | Access token (JWT) issued at login |
x-workspace-id: {workspaceId} | ID of the target workspace |
Content-Type: application/json | Request body format |
Obtain an access token with POST /api/auth/login (email, password) and use data.user.accessToken from the response. When it expires, refresh it with POST /api/auth/token/refresh (the X-Refresh-Token header). For long-term integrations, you can issue an API key with POST /api/auth/api-keys.
Device Concept — Both the source and target of a transfer are devices with an agent installed. Retrieve the list with GET /api/devices to obtain a deviceId. Each device has properties including os (windows·linux·mac) and online status (status).
Specifying Transfer Items — Instant transfer (POST /api/transfers/manual) specifies items to send with sourcePaths (an array of path strings). The automation and synchronization APIs specify them in the sourceItem array with a hash identifier.
| Field | Type | Description |
|---|---|---|
sourcePaths | string[] | List of paths to send in an instant transfer |
sourceItem[].hash | string | Automation·synchronization item identifier — {deviceId}_ino_{base64(UTF-8 path)} |
sourceItem[].isDir | boolean | Whether the item is a folder |
Use sendAllFolder to send all contents under a folder.
Device Lookup — GET /api/devices/resolve — Immediately look up a deviceId by name·IP·MAC (at least one of name·ip·mac is required).
{
"status_code": 200,
"message": "OK",
"data": {
"matchCount": 1,
"devices": [
{
"deviceId": "dev_01H8...",
"name": "OfficePC",
"ipAddress": "192.168.0.9",
"osType": "windows",
"state": 1, "stateName": "CONNECTED", "stateLabel": "Connected",
"isConnected": true
}
]
}
}
If multiple device names match, the response is 409 + data.candidates[], so use an unambiguous name.
Folder Listing (Non-Streaming) — GET /api/devices/{deviceId}/files — Retrieve the direct contents of a folder as JSON (use files/search SSE for recursive search). The response provides both path and fileToken, and fileToken can be used directly as the item identifier for transfer and file operations.
{
"status_code": 200,
"message": "OK",
"data": {
"path": "/data", "total": 128, "page": 1, "size": 50, "lastPage": 3,
"items": [
{
"name": "report.pdf",
"path": "/data/report.pdf",
"fileToken": "L2RhdGEv...",
"isDir": false, "size": 20480,
"modifiedAt": "2026-08-01T09:12:00Z"
}
]
}
}
Enum Value Labels — Enum fields in single-item and detail responses provide the integer value together with the constant name and label (state/stateName/stateLabel). Derived flags such as connection status (isConnected) are also provided.
Key Endpoints
| Purpose | Method | Endpoint |
|---|---|---|
| Login | POST | /api/auth/login |
| Refresh token | POST | /api/auth/token/refresh |
| Device list | GET | /api/devices |
| Device lookup (name·IP·MAC) | GET | /api/devices/resolve |
| Path capacity preview | GET | /api/devices/{deviceId}/path-stats |
| Source file search | POST | /api/devices/{deviceId}/files/search |
| Folder listing (non-streaming) | GET | /api/devices/{deviceId}/files |
| Path pre-validation | POST | /api/transfers/validate-path |
| Create instant transfer | POST | /api/transfers/manual |
| Get transfer files | GET | /api/transfers/{monitorId}/files |
| Transfer controls | POST | /api/transfers/{monitorId}/pause · resume · cancel · retry |
Basic Flow
- Log in to obtain an access token
- Use
GET /api/devicesto identifysourceDeviceandtargetDevice - (Optional) Build
sourcePathswith file search - (Optional) Validate paths with
validate-path - Create the transfer with
POST /api/transfers/manual→ receivemonitorId - Use
monitorIdto monitor and control progress
Shared Client (Java·C#)
The Node.js·Python examples run independently as a single file. The Java·C# examples use helpers from a shared class (InnorixClient, with Json included for Java). The sections below summarize the commonly used parts; the full source is available in the collapsible section.
// InnorixClient — shared helper the examples `import static`
// ── Auth & call (used by nearly every example)
void login(String email, String password); // obtain & store the access token
Object api(String method, String path, Object body); // JSON call -> returns data; throws on 4xx
// ── Path & status
String encodePath(String deviceId, String path); // item token: {deviceId}_ino_{base64(path)}
boolean isTerminal(Object detail); // finished (complete/failed) status?
// status codes: COMPLETE=2 · PAUSE=3 · TRANSFERRING=6 · PARTIAL=9 · FAIL=99
// ── JSON helpers
Map<String,Object> obj(Object... kv); List<Object> arr(Object... items); // builders
String str(Object n, String key); int intv(Object n, String key, int def); // value access
List<Object> pageItems(Object page); // normalize list-or-{items}/{data}
// ── Env & JSON: env(key[, def]) · requireEnv(keys...) | Json.write() · Json.parse()1:1 Transfer
Description
This is the most basic pattern for immediately transferring a specified file from one source device to one target device. Specify the source, target, and items to send with sourceDevice·targetDevice and sourcePaths (an array of path strings), then use monitorId to check status until completion.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Login | POST | /api/auth/login |
| Create transfer | POST | /api/transfers/manual |
| Get status | GET | /api/transfers/{monitorId} |
Request
POST /api/transfers/manual
{
"sourceDevice": "device-source-01",
"targetDevice": "device-target-01",
"targetPath": "/data/incoming",
"sourcePaths": ["/data/report.pdf"],
"sendAllFolder": false
}
sourceDevice·targetDevicecan be specified using adeviceId, device name, or IP.sourcePathsis an array of path strings for the files and folders to send (no hash encoding required).sendAllFolderis a boolean.
ℹ️
sourceDevice/targetDevicecan be specified using not only adeviceIdbut also a device name or IP; the server resolves it internally.
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
Processing Steps
- Obtain an access token with
POST /api/auth/login(data.user.accessToken) - Call
POST /api/transfers/manual—sourceDevice,targetDevice,targetPath,sourcePaths→ receivedata.monitorId - Poll
GET /api/transfers/{monitorId}to checkstatus—2=complete,4·5·9·99=failure and terminate
Implementation Examples
"""Example 01 - One-to-one transfer.
Send a single file from a source device to a target device, then poll until it finishes.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "01-1to1-transfer/example.py"
"""
import os
import sys
import time
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# INNORIX_SOURCE_DEVICE / TARGET_DEVICE accept a device ID, device name, or IP address.
SOURCE_DEVICE = os.getenv("INNORIX_SOURCE_DEVICE", "device-source-01")
TARGET_DEVICE = os.getenv("INNORIX_TARGET_DEVICE", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/report.pdf")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
# Transfer status codes and the subset that means "no longer running".
TRANSFER_STATUS = {
"transferComplete": 2,
"transferError": 4,
"transferCancel": 5,
"transferPartialComplete": 9,
"transferFail": 99,
}
TERMINAL_TRANSFER_STATUSES = set(TRANSFER_STATUS.values())
def api(method, path, token=None, body=None):
"""Minimal JSON API helper shared by every example."""
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def main():
require_env()
# Log in once; the same token and workspace header are reused for every call.
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Send one file from the source device to the target device.
transfer = api("POST", "/api/transfers/manual", token, {
"sourceDevice": SOURCE_DEVICE,
"targetDevice": TARGET_DEVICE,
"targetPath": TARGET_PATH,
"sourcePaths": [SOURCE_PATH],
"sendAllFolder": False,
})
monitor_id = transfer["monitorId"]
print("transfer created", {
"monitorId": monitor_id,
"status": transfer.get("status"),
"statusName": transfer.get("statusName"),
})
# Poll the combined active/history detail endpoint until the transfer reaches a terminal status.
while True:
detail = api("GET", f"/api/transfers/{monitor_id}", token)
is_terminal = detail.get("isTerminal", detail.get("status") in TERMINAL_TRANSFER_STATUSES)
print({
"monitorId": monitor_id,
"status": detail.get("status"),
"statusName": detail.get("statusName"),
"isTerminal": is_terminal,
"percent": detail.get("percent", 0),
})
if is_terminal:
if detail.get("status") != TRANSFER_STATUS["transferComplete"]:
raise RuntimeError(detail.get("errorCode") or "Transfer failed")
print("Transfer completed")
return
time.sleep(2)
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001 - surface a clean message to the CLI
print(error, file=sys.stderr)
sys.exit(1)File Collection
Description
This pattern collects folders from multiple sources (branches) into one target path. Search for the target folder at each branch, create transfers, and aggregate completion across all transfers.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Login | POST | /api/auth/login |
| Create transfer | POST | /api/transfers/manual |
| Get status | GET | /api/transfers/{monitorId} |
Request
POST /api/transfers/manual (repeat for each source)
{
"sourceDevice": "branch-01",
"targetDevice": "device-target-01",
"targetPath": "/collect/logs",
"sourcePaths": ["/var/log"],
"sendAllFolder": true,
"transferOptions": { "target-action": "numbering" }
}
- Change only
sourceDevicefor each branch; keeptargetDevice·targetPathfixed. - Use
target-action: numberingintransferOptionsto append a number when name conflicts occur while collecting into the same path. - Because the entire folder is collected,
sendAllFolderistrue.
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
Processing Steps
- Obtain an access token with
POST /api/auth/login - For each branch, call
POST /api/transfers/manual(sourceDevice=branch,targetDevice=headquarters,sourcePaths=collection path) → collectmonitorIdvalues - Poll every
monitorIdwithGET /api/transfers/{monitorId}and aggregate completion
Implementation Examples
"""Example 02 - File collect.
Collect the same source path from several branch devices into one HQ device,
running the transfers in parallel and waiting for all of them to finish.
INNORIX_SOURCE_DEVICE / TARGET_DEVICE (and branch names) accept a device ID,
device name, or IP address.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "02-file-collect/example.py"
"""
import os
import sys
import time
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# Comma-separated list of branch (source) devices to collect from.
BRANCH_DEVICES = [
name.strip()
for name in os.getenv("INNORIX_BRANCH_DEVICES", "branch-01,branch-02,branch-03").split(",")
if name.strip()
]
# Single HQ (target) device that receives everything.
HQ_DEVICE = os.getenv("INNORIX_HQ_DEVICE", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/var/log")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/collect/logs")
TRANSFER_STATUS = {
"transferComplete": 2,
"transferError": 4,
"transferCancel": 5,
"transferPartialComplete": 9,
"transferFail": 99,
}
TERMINAL_TRANSFER_STATUSES = set(TRANSFER_STATUS.values())
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Start one collect transfer per branch: branch (source) -> HQ (target).
pending = {} # branch_name -> monitor_id
for branch_name in BRANCH_DEVICES:
transfer = api("POST", "/api/transfers/manual", token, {
"sourceDevice": branch_name,
"targetDevice": HQ_DEVICE,
"targetPath": TARGET_PATH,
"sourcePaths": [SOURCE_PATH],
"sendAllFolder": True,
"transferOptions": {"target-action": "numbering"},
})
print(branch_name, {
"monitorId": transfer["monitorId"],
"status": transfer.get("status"),
"targetPath": TARGET_PATH,
})
pending[branch_name] = transfer["monitorId"]
# Poll every branch transfer until each one reaches a terminal status.
total = len(pending)
while pending:
for branch_name, monitor_id in list(pending.items()):
detail = api("GET", f"/api/transfers/{monitor_id}", token)
print(branch_name, {
"status": detail.get("status"),
"statusName": detail.get("statusName"),
"percent": detail.get("percent", 0),
})
if detail.get("isTerminal", detail.get("status") in TERMINAL_TRANSFER_STATUSES):
if detail.get("status") != TRANSFER_STATUS["transferComplete"]:
raise RuntimeError(f"{branch_name}: {detail.get('errorCode') or 'failed'}")
del pending[branch_name]
if pending:
time.sleep(2)
print(f"Collected logs from {total} branches")
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)File Distribution
Description
This pattern distributes one set of source files to multiple target devices. List the distribution folder once on the source, transfer the same package to each target, and aggregate successes and failures.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Login | POST | /api/auth/login |
| Device lookup (name) | GET | /api/devices/resolve |
| Folder listing (non-streaming) | GET | /api/devices/{deviceId}/files |
| Create transfer | POST | /api/transfers/manual |
| Get status | GET | /api/transfers/{monitorId} |
Request
POST /api/transfers/manual (repeat for each target)
{
"sourceDevice": "dev_01H8SRC...",
"targetDevice": "dev_01H8BRANCH01...",
"targetPath": "/deploy",
"sourcePaths": ["/deploy/deployment-package"],
"sendAllFolder": true,
"transferOptions": { "target-action": "overwrite" }
}
- Change only
targetDevicefor each branch; keepsourceDevice·sourcePathsfixed. - Use
target-action: overwriteintransferOptionsto overwrite existing files on the target. - Because the entire folder is distributed,
sendAllFolderistrue.
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
Processing Steps
- Obtain an access token with
POST /api/auth/login - Obtain the source
deviceIdwithGET /api/devices/resolve?name= - Use
GET /api/devices/{deviceId}/filesto search the distribution package folder and obtain its path - For each target, call
POST /api/transfers/manual(targetDevice=branch,sourcePaths=package path) → onemonitorIdper target - Poll each
monitorIdwithGET /api/transfers/{monitorId}and aggregate successes and failures
Implementation Examples
"""Example 03 - File distribution.
Distribute one package from a source device to many target devices in parallel,
then wait for all of them and report which succeeded and which failed.
Targets can be supplied via numbered environment variables:
INNORIX_TARGET_DEVICE_1 / INNORIX_TARGET_PATH_1, _2, _3, ... (up to 50)
If none are set, the DEFAULT_TARGETS below are used.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "03-file-distribution/example.py"
"""
import os
import sys
import time
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# INNORIX_SOURCE_DEVICE accepts a device ID, device name, or IP address.
SOURCE_DEVICE = os.getenv("INNORIX_SOURCE_DEVICE", "device-source-01")
SOURCE_ROOT = os.getenv("INNORIX_SOURCE_ROOT", "/deploy")
PACKAGE_NAME = os.getenv("INNORIX_PACKAGE_NAME", "deployment-package")
TRANSFER_STATUS = {
"transferComplete": 2,
"transferError": 4,
"transferCancel": 5,
"transferPartialComplete": 9,
"transferFail": 99,
}
TERMINAL_TRANSFER_STATUSES = set(TRANSFER_STATUS.values())
# Used only when no INNORIX_TARGET_DEVICE_N variables are configured.
DEFAULT_TARGETS = [
{"device": "branch-01", "path": "/deploy/branch-01"},
{"device": "branch-02", "path": "/deploy/branch-02"},
{"device": "branch-03", "path": "/deploy/branch-03"},
{"device": "branch-04", "path": "/deploy/branch-04"},
{"device": "branch-05", "path": "/deploy/branch-05"},
]
def join_path(root, name):
normalized_root = str(root or "").replace("\\", "/").rstrip("/")
normalized_name = str(name or "").replace("\\", "/").lstrip("/")
if not normalized_root:
return normalized_name or "/"
if not normalized_name:
return normalized_root
return f"{normalized_root}/{normalized_name}"
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH") or join_path(SOURCE_ROOT, PACKAGE_NAME)
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def distribution_targets():
"""Read INNORIX_TARGET_DEVICE_N / INNORIX_TARGET_PATH_N pairs, or fall back to DEFAULT_TARGETS."""
targets = []
for index in range(1, 51):
device = os.getenv(f"INNORIX_TARGET_DEVICE_{index}")
path = os.getenv(f"INNORIX_TARGET_PATH_{index}")
if not device and not path:
continue
if not device or not path:
raise RuntimeError(
f"INNORIX_TARGET_DEVICE_{index} and INNORIX_TARGET_PATH_{index} must be set together"
)
targets.append({"device": device.strip(), "path": path.replace("\\", "/")})
return targets if targets else DEFAULT_TARGETS
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Start one transfer per target, reusing the same source package path.
pending = {} # target_device -> monitor_id
for target in distribution_targets():
transfer = api("POST", "/api/transfers/manual", token, {
"sourceDevice": SOURCE_DEVICE,
"targetDevice": target["device"],
"targetPath": target["path"],
"sourcePaths": [SOURCE_PATH],
"sendAllFolder": True,
"transferOptions": {"target-action": "overwrite"},
})
print(target["device"], {
"monitorId": transfer["monitorId"],
"status": transfer.get("status"),
"targetPath": target["path"],
})
pending[target["device"]] = transfer["monitorId"]
# Poll every pending transfer until each one reaches a terminal status.
results = {} # target_device -> "success" | error_code
while pending:
for target_name, monitor_id in list(pending.items()):
detail = api("GET", f"/api/transfers/{monitor_id}", token)
print(target_name, {
"status": detail.get("status"),
"statusName": detail.get("statusName"),
"percent": detail.get("percent", 0),
})
if detail.get("isTerminal", detail.get("status") in TERMINAL_TRANSFER_STATUSES):
results[target_name] = (
"success"
if detail.get("status") == TRANSFER_STATUS["transferComplete"]
else (detail.get("errorCode") or "failed")
)
del pending[target_name]
if pending:
time.sleep(2)
# Split the outcome into succeeded devices and failed devices with their reasons.
succeeded = [name for name, value in results.items() if value == "success"]
failed = {name: value for name, value in results.items() if value != "success"}
print({"succeeded": succeeded, "failed": failed})
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Transfer Rules
Description
This section organizes the request components that determine what / where / how a transfer sends data. Source filters and target options provide additional capabilities in automation.
| Category | Related Fields | Details |
|---|---|---|
| What (source) | sourcePaths, sendAllFolder | Source Filter |
| Where (target) | targetDevice, targetPath | Target Options |
| On conflict | transferOptions.target-action | Conflict Handling |
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Validate path | POST | /api/transfers/validate-path |
| Create transfer | POST | /api/transfers/manual |
Request
POST /api/transfers/validate-path
{
"sourceDevice": "device-src-001",
"targetDevice": "device-dst-002",
"targetPath": "/data/incoming",
"sourcePaths": ["/data/report.pdf"],
"sendAllFolder": false
}
Response
{
"status_code": 200,
"message": "success",
"data": { "valid": true }
}
Processing Steps
- Validate the source·target·paths with
POST /api/transfers/validate-path - Configure the rules (
sourcePaths/sendAllFolder/transferOptions.target-action) and callPOST /api/transfers/manual - Use
data.monitorIdfrom the response for subsequent processing
Implementation Examples
"""Example 04 - Transfer rules.
Validate the source/target paths first, then start a manual transfer and wait for it to finish.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "04-transfer-rules/example.py"
"""
import os
import sys
import time
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# INNORIX_SOURCE_DEVICE / TARGET_DEVICE accept a device ID, device name, or IP address.
SOURCE_DEVICE = os.getenv("INNORIX_SOURCE_DEVICE", "device-source-01")
TARGET_DEVICE = os.getenv("INNORIX_TARGET_DEVICE", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/report.pdf").replace("\\", "/")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
TRANSFER_STATUS = {
"transferComplete": 2,
"transferError": 4,
"transferCancel": 5,
"transferPartialComplete": 9,
"transferFail": 99,
}
TERMINAL_TRANSFER_STATUSES = set(TRANSFER_STATUS.values())
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def wait_for_completion(monitor_id, token):
while True:
detail = api("GET", f"/api/transfers/{monitor_id}", token)
is_terminal = detail.get("isTerminal", detail.get("status") in TERMINAL_TRANSFER_STATUSES)
print({
"monitorId": monitor_id,
"status": detail.get("status"),
"statusName": detail.get("statusName"),
"isTerminal": is_terminal,
"percent": detail.get("percent", 0),
})
if is_terminal:
if detail.get("status") != TRANSFER_STATUS["transferComplete"]:
raise RuntimeError(detail.get("errorCode") or "Transfer failed")
return detail
time.sleep(2)
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
request = {
"sourceDevice": SOURCE_DEVICE,
"targetDevice": TARGET_DEVICE,
"targetPath": TARGET_PATH,
"sourcePaths": [SOURCE_PATH],
"sendAllFolder": False,
}
# 1) Validate the transfer request before running it (checks devices and paths).
validation = api("POST", "/api/transfers/validate-path", token, request)
print("validate-path", validation)
# 2) Start the manual transfer using the same request.
transfer = api("POST", "/api/transfers/manual", token, request)
print("transfer created", {"monitorId": transfer["monitorId"], "status": transfer.get("status")})
# 3) Wait until the transfer reaches a terminal status.
wait_for_completion(transfer["monitorId"], token)
print("Transfer completed")
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Conflict Handling
Description
This controls what happens when a file with the same name already exists in the target path. Specify one of numbering (numbering), skip (nosend), or overwrite (overwrite) with target-action in transferOptions. Files that fail during transfer can be resumed through retry.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Create transfer | POST | /api/transfers/manual |
| Retry failures | POST | /api/transfers/{monitorId}/retry |
transferOptions.target-action values:
| Value | Description |
|---|---|
numbering | On a name conflict, append a number and preserve both ((1), (2)) |
nosend | If the file already exists, do not send it and skip it |
overwrite | Overwrite the existing file |
Request
POST /api/transfers/manual
{
"sourceDevice": "device-source-01",
"targetDevice": "device-target-01",
"targetPath": "/data/incoming",
"sourcePaths": ["/data/report.pdf"],
"transferOptions": { "target-action": "numbering" }
}
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
Use data.monitorId from the response for subsequent status queries (GET /api/transfers/{monitorId}/files) and controls (pause·resume·cancel·retry).
Processing Steps
- Call
POST /api/transfers/manualwithtransferOptions.target-action - Receive
data.monitorIdfrom the response - Retransfer failed files with
POST /api/transfers/{monitorId}/retry
Implementation Examples
"""Example 05 - Conflict handling.
Send the same file to the same location under each conflict policy and compare the
file-level results.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "05-conflict-handling/example.py"
"""
import os
import sys
import time
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# INNORIX_SOURCE_DEVICE / TARGET_DEVICE accept a device ID, device name, or IP address.
SOURCE_DEVICE = os.getenv("INNORIX_SOURCE_DEVICE", "device-source-01")
TARGET_DEVICE = os.getenv("INNORIX_TARGET_DEVICE", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/report.pdf")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
# Conflict policies to compare: add a number suffix, skip, or overwrite on conflict.
CONFLICT_POLICIES = ["numbering", "nosend", "overwrite"]
TRANSFER_STATUS = {
"transferComplete": 2,
"transferError": 4,
"transferCancel": 5,
"transferPartialComplete": 9,
"transferFail": 99,
}
TERMINAL_TRANSFER_STATUSES = set(TRANSFER_STATUS.values())
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def wait_for_terminal(monitor_id, token):
"""The 'nosend'/'fail' policies may end without success, so wait for any terminal status."""
while True:
detail = api("GET", f"/api/transfers/{monitor_id}", token)
if detail.get("isTerminal", detail.get("status") in TERMINAL_TRANSFER_STATUSES):
return detail
time.sleep(2)
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
for policy in CONFLICT_POLICIES:
# Same file, same destination - only the conflict policy changes.
transfer = api("POST", "/api/transfers/manual", token, {
"sourceDevice": SOURCE_DEVICE,
"targetDevice": TARGET_DEVICE,
"targetPath": TARGET_PATH,
"sourcePaths": [SOURCE_PATH],
"sendAllFolder": False,
"transferOptions": {"target-action": policy},
})
detail = wait_for_terminal(transfer["monitorId"], token)
# Inspect the per-file result for this policy. The /files response returns the rows
# under `children`, and each row names the file/state as sourceFileName / statusName.
files = api("GET", f"/api/transfers/{transfer['monitorId']}/files?state=any&size=100", token)
rows = (files or {}).get("children") or (files or {}).get("items") or []
print(
f"policy={policy} status={detail.get('status')}",
[
{
"name": f.get("sourceFileName") or f.get("targetFileName") or f.get("sourceFilePath"),
"state": f.get("statusName") or f.get("status"),
}
for f in rows
],
)
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Transfer Automation
Configure transfers to run automatically according to schedules, file events, or workflows without a person calling them each time.
Recurring Automation
Description
Run the same transfer repeatedly according to a schedule. When you create an automation, an automationId is issued; use it to pause the automation and retrieve details for operations.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Create automation | POST | /api/automations |
| List automations | GET | /api/automations |
| Automation details | GET | /api/automations/{automationId}/details |
| Pause automation | POST | /api/automations/{automationId}/pause |
Request
POST /api/automations
{
"name": "Daily Settlement Transfer",
"transferType": "normal",
"timezone": "Asia/Seoul",
"details": [
{
"senderId": "device-src-001",
"receiverId": "device-hq-001",
"targetPath": "/collect/logs",
"sourceItem": [{ "hash": "device-src-001_ino_...", "isDir": false }],
"step": 1,
"transferOptions": { "noSchedule": false, "target-action": "numbering", "send-fileoption": {} }
}
],
"schedules": [
{ "type": "day", "startDateType": "now", "hour": "02", "minute": "00", "ampm": "am", "startDate": "2026-08-25T00:00:00Z", "timezone": "Asia/Seoul" }
]
}
Response
{
"status_code": 201,
"message": "Created",
"data": { "automationId": "auto-abc123" }
}
Processing Steps
- Call
POST /api/automations—name,schedules(array of schedule objects),details(array of transfer definitions),timezone,transferType - Receive
data.automationIdfrom the response - Check details and progress with
GET /api/automations/{automationId}/details - If needed, pause with
POST /api/automations/{automationId}/pause
Implementation Examples
"""Example 06 - Recurring automation.
Full lifecycle of a scheduled automation: create, read, update the schedule,
list past executions, pause, then delete.
INNORIX_SOURCE_ID / TARGET_ID must be the exact device IDs - they are encoded into the
path token, so a device name will not work here (unlike the manual-transfer examples).
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "06-Recurring Automation/example.py"
"""
import base64
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
SOURCE_ID = os.getenv("INNORIX_SOURCE_ID", "device-source-01")
TARGET_ID = os.getenv("INNORIX_TARGET_ID", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/report.pdf").replace("\\", "/")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
TIMEZONE = os.getenv("INNORIX_TIMEZONE", "UTC")
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def encode_path(device_id, raw_path):
"""Build the `<deviceId>_ino_<base64(path)>` token used by the automation API."""
normalized = str(raw_path or "").replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def now_iso():
"""UTC timestamp in the same ISO 8601 format Node's Date#toISOString() produces."""
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Schedule: run every day at 02:00 AM to send the previous day's settlement file.
schedule = {
"type": "day",
"startDateType": "now",
"hour": "02",
"minute": "00",
"ampm": "am",
"startDate": now_iso(),
"timezone": TIMEZONE,
}
# 1) Create the automation.
created = api("POST", "/api/automations", token, {
"name": "Daily Settlement Transfer",
"details": [
{
"sourceItem": [{"hash": encode_path(SOURCE_ID, SOURCE_PATH), "isDir": False}],
"targetPath": TARGET_PATH,
"senderId": SOURCE_ID,
"receiverId": TARGET_ID,
"step": 1,
"transferOptions": {
"noSchedule": False,
"target-action": "numbering",
"send-fileoption": {},
},
}
],
"transferType": "normal",
"timezone": TIMEZONE,
"schedules": [schedule],
})
automation_id = created["automationId"]
print("automation created", automation_id)
# 2) Read the automation.
print("detail", api("GET", f"/api/automations/{automation_id}", token))
# 3) Update the schedule.
api("PATCH", f"/api/automations/{automation_id}", token, {
"name": "Daily Settlement Transfer",
"isUpdateSchedule": True,
"schedules": [schedule],
})
# 4) List past executions.
print("executions", api("GET", f"/api/automations/{automation_id}/executions", token))
# 5) Pause the automation.
api("POST", f"/api/automations/{automation_id}/pause", token, {"pause": True})
# 6) Delete the automation (example cleanup).
api("DELETE", f"/api/automations/{automation_id}", token)
print("automation deleted", automation_id)
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Webhook
Description
Notify an external system about transfer and automation executions. If you add a webhook processor (processors) to an automation, the server calls the specified URL when the automation runs (events: "Run").
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Create automation with webhook processor | POST | /api/automations |
| Automation details (including processors) | GET | /api/automations/{automationId}/details |
processors[] fields:
| Field | Type | Description |
|---|---|---|
events | string | Trigger event (e.g., Run) |
type | string | Processor type (e.g., https) |
method | string | HTTP method (e.g., POST) |
url | string | URL to call |
body | string | Request body (optional) |
Request
POST /api/automations
{
"name": "Webhook Automation",
"flowName": "Webhook Automation",
"transferType": "normal",
"isUpcoming": false,
"timezone": "Asia/Seoul",
"details": [
{
"sourceItem": [{ "hash": "device-source-01_ino_...", "isDir": false }],
"targetPath": "/data/incoming",
"senderId": "device-source-01",
"receiverId": "device-target-01",
"step": 1,
"transferOptions": { "noSchedule": false, "target-action": "numbering", "send-fileoption": {} }
}
],
"schedules": [{ "type": "none", "startDateType": "now", "hour": "00", "minute": "00", "ampm": "am", "startDate": "2026-08-25T00:00:00Z", "timezone": "Asia/Seoul" }],
"processors": [
{ "events": "Run", "type": "https", "method": "POST", "url": "https://example.com/webhook", "body": "" }
]
}
Response
{
"status_code": 201,
"message": "Created",
"data": { "automationId": "auto-abc123" }
}
Processing Steps
- Create an automation with
POST /api/automations, including theprocessors(webhook) array - Receive
data.automationIdfrom the response - When the automation runs, the server calls
processors[].url - Verify the configured webhook processor with
GET /api/automations/{automationId}/details
Implementation Examples
"""Example 07 - Webhook.
Create an automation that fires a webhook when it runs. The webhook is defined as a
processor on the automation (events: "Run"), so the server calls your URL on execution.
INNORIX_SOURCE_ID / TARGET_ID must be the exact device IDs - they are encoded into the
path token, so a device name will not work here (unlike the manual-transfer examples).
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "07-Webhook/example.py"
"""
import base64
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
SOURCE_ID = os.getenv("INNORIX_SOURCE_ID", "device-source-01")
TARGET_ID = os.getenv("INNORIX_TARGET_ID", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/report.pdf").replace("\\", "/")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
TIMEZONE = os.getenv("INNORIX_TIMEZONE", "UTC")
# Webhook processor settings.
WEBHOOK_TYPE = os.getenv("INNORIX_WEBHOOK_TYPE", "https")
WEBHOOK_URL = os.getenv("INNORIX_WEBHOOK_URL", "https://example.com/webhook")
WEBHOOK_METHOD = os.getenv("INNORIX_WEBHOOK_METHOD", "POST")
WEBHOOK_BODY = os.getenv("INNORIX_WEBHOOK_BODY", "")
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def encode_path(device_id, raw_path):
"""Build the `<deviceId>_ino_<base64(path)>` token used by the automation API."""
normalized = str(raw_path or "").replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def now_iso():
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Create an automation with a webhook processor that fires on run.
automation = api("POST", "/api/automations", token, {
"name": "Webhook Automation",
"flowName": "Webhook Automation",
"details": [
{
"sourceItem": [{"hash": encode_path(SOURCE_ID, SOURCE_PATH), "isDir": False}],
"targetPath": TARGET_PATH,
"senderId": SOURCE_ID,
"receiverId": TARGET_ID,
"step": 1,
"transferOptions": {
"noSchedule": False,
"target-action": "numbering",
"send-fileoption": {},
},
}
],
"transferType": "normal",
"isUpcoming": False,
"timezone": TIMEZONE,
"schedules": [
{
"type": "none",
"startDateType": "now",
"hour": "00",
"minute": "00",
"ampm": "am",
"startDate": now_iso(),
"timezone": TIMEZONE,
}
],
# Webhook: the server calls this URL when the automation runs.
"processors": [
{
"events": "Run",
"type": WEBHOOK_TYPE,
"method": WEBHOOK_METHOD,
"url": WEBHOOK_URL,
"body": WEBHOOK_BODY,
}
],
})
automation_id = automation["automationId"]
print("automation created", automation_id)
# Read the automation details, including the configured webhook processor.
print("details", api("GET", f"/api/automations/{automation_id}/details", token))
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Workflow
Description
Connect multiple stages into one flow. Create each hop as an automation and group them under the same flowId; connect the next hop to the previous hop with triggerAutomation. When A→B completes, the server automatically runs B→C (the client does not assemble it).
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Create automation (per hop) | POST | /api/automations |
| Automation details | GET | /api/automations/{automationId}/details |
| Execution history | GET | /api/automations/{automationId}/executions |
Request
POST /api/automations (for each hop, sharing the same flowId)
{
"name": "B to C Workflow",
"flowName": "B-C Workflow",
"transferType": "normal",
"isUpcoming": false,
"timezone": "Asia/Seoul",
"flowId": "shared-flow-uuid",
"details": [
{
"sourceItem": [{ "hash": "device-middle-01_ino_...", "isDir": false }],
"targetPath": "/data/incoming",
"senderId": "device-middle-01",
"receiverId": "device-target-01",
"step": 1,
"transferOptions": { "noSchedule": false, "target-action": "numbering", "send-fileoption": {} }
}
],
"schedules": [
{ "type": "none", "startDateType": "now", "hour": "00", "minute": "00", "ampm": "am", "startDate": "2026-08-25T00:00:00Z", "timezone": "Asia/Seoul", "triggerAutomation": { "value": "auto-ab-id" } }
]
}
- All hops belong to one workflow under the same
flowId. - If you specify the previous hop's
automationIdinschedules[].triggerAutomation.valuefor the next hop, it runs automatically after the previous hop completes.
Response
{
"status_code": 201,
"message": "Created",
"data": { "automationId": "auto-flow-abc123" }
}
Processing Steps
- Create a shared
flowId - Create the A→B automation → receive
automationId - When creating the B→C automation, set
triggerAutomation.value=A→BautomationIdand specify the sameflowId - Check stages and executions with
GET /api/automations/{automationId}/details·/executions
Implementation Examples
"""Example 08 - Workflow (A -> B -> C chain).
Define a relayed transfer as two automations sharing one flowId: A -> B, then B -> C.
Both are marked isUpcoming; B -> C is chained to A -> B via `triggerAutomation`, so the
server runs it automatically once A -> B completes (the client does not orchestrate the
hand-off).
INNORIX_SOURCE_ID / MIDDLE_ID / TARGET_ID must be the exact device IDs - they are encoded
into the path token, so a device name will not work here.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "08-Workflow/example.py"
"""
import base64
import os
import random
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# A -> B -> C: source -> middle (relay) -> target.
SOURCE_ID = os.getenv("INNORIX_SOURCE_ID", "device-source-01")
MIDDLE_ID = os.getenv("INNORIX_MIDDLE_ID", "device-middle-01")
TARGET_ID = os.getenv("INNORIX_TARGET_ID", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/report.pdf").replace("\\", "/")
MIDDLE_PATH = os.getenv("INNORIX_MIDDLE_PATH", SOURCE_PATH).replace("\\", "/")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
TIMEZONE = os.getenv("INNORIX_TIMEZONE", "UTC")
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def encode_path(device_id, raw_path):
"""Build the `<deviceId>_ino_<base64(path)>` token used by the automation API."""
normalized = str(raw_path or "").replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def now_iso():
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
def generate_transfer_id():
"""Generate a transfer id like 'T1234-5678-9012' from time, pid, and randomness."""
ts = str(int(time.time() * 1000))
pid = f"{os.getpid() % 1000:03d}"
rand = f"{random.randint(0, 999):03d}"
raw = (ts + pid + rand)[-12:]
return f"T{raw[0:4]}-{raw[4:8]}-{raw[8:12]}"
def generate_flow_id():
"""Flow id derived from a transfer id, e.g. 'F-1234-5678-9012'."""
return f"F-{generate_transfer_id()[1:]}"
def build_automation(name, flow_name, sender_id, sender_path, receiver_id, flow_id,
trigger_automation_id=None):
"""Build one automation body for a single hop (sender -> receiver).
Pass trigger_automation_id to chain this automation after another one completes.
"""
schedule = {
"type": "none",
"startDateType": "now",
"hour": "00",
"minute": "00",
"ampm": "am",
"startDate": now_iso(),
"timezone": TIMEZONE,
}
if trigger_automation_id:
schedule["triggerAutomation"] = {"value": trigger_automation_id}
return {
"name": name,
"flowName": flow_name,
"details": [
{
"sourceItem": [{"hash": encode_path(sender_id, sender_path), "isDir": False}],
"targetPath": TARGET_PATH,
"senderId": sender_id,
"receiverId": receiver_id,
"step": 1,
"transferOptions": {
"noSchedule": False,
"target-action": "numbering",
"send-fileoption": {},
},
}
],
"transferType": "normal",
"isUpcoming": True,
"timezone": TIMEZONE,
"schedules": [schedule],
"flowId": flow_id,
}
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Both hops share one flowId so they belong to the same workflow.
flow_id = generate_flow_id()
# 1) A -> B.
transfer_ab = api("POST", "/api/automations", token, build_automation(
name="A to B Workflow",
flow_name="A-B Workflow",
sender_id=SOURCE_ID,
sender_path=SOURCE_PATH,
receiver_id=MIDDLE_ID,
flow_id=flow_id,
))
automation_id = transfer_ab["automationId"]
print("A->B automation created", automation_id)
# 2) B -> C, triggered automatically after the A -> B automation completes.
transfer_bc = api("POST", "/api/automations", token, build_automation(
name="B to C Workflow",
flow_name="B-C Workflow",
sender_id=MIDDLE_ID,
sender_path=MIDDLE_PATH,
receiver_id=TARGET_ID,
flow_id=flow_id,
trigger_automation_id=automation_id,
))
print("B->C automation created", transfer_bc["automationId"])
# 3) Inspect the per-step definition and execution history of the A -> B automation.
print("details", api("GET", f"/api/automations/{automation_id}/details", token))
print("executions", api("GET", f"/api/automations/{automation_id}/executions", token))
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Source Filter
Description
Select items to send from the source based on conditions. Specify filter options in transferOptions for an automation (POST /api/automations) to filter server-side by extension, size, or name before transfer. If a folder is specified as the source, matching contents below it are collected recursively.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| (Optional) File search | POST | /api/devices/{deviceId}/files/search |
| Create filtered automation | POST | /api/automations |
Filter-related transferOptions fields:
| Field | Type | Description |
|---|---|---|
send-filetype-cus | string | Regular expression for file types to include (e.g., \\.(log|csv)$) |
send-fileoption.fileSize | object | Size criteria { size, over, equal } (bytes) |
send-fileoption.fileName | object | Name pattern { name, allow, isMatchCase } — excluded when allow:false |
savepath / optionPath | boolean / string | Preserve source folder structure (optionPath: "relative") |
Request
POST /api/automations
{
"name": "Source Filter Transfer",
"flowName": "Source Filter Transfer",
"transferType": "no_schedule",
"timezone": "Asia/Seoul",
"callbackURL": "",
"details": [
{
"sourceItem": [{ "hash": "device-source-01_ino_L3Zhci9sb2c=", "isDir": true }],
"targetPath": "device-target-01_ino_L2NvbGxlY3QvbG9ncw==",
"senderId": "device-source-01",
"receiverId": "device-target-01",
"step": 1,
"transferOptions": {
"noSchedule": true,
"send-filetype-cus": "\\.(log|csv)$",
"savepath": true,
"optionPath": "relative",
"target-action": "numbering",
"send-fileoption": {
"fileSize": { "size": 1024, "over": true, "equal": true },
"fileName": { "name": "*.tmp", "allow": false, "isMatchCase": false }
}
}
}
],
"schedules": [{ "type": "none", "startDateType": "now", "hour": "05", "minute": "00", "ampm": "pm", "startDate": "2026-08-25T00:00:00Z", "timezone": "Asia/Seoul" }]
}
- The
hashandtargetPathinsourceItemare{deviceId}_ino_{base64(path)}tokens. Specify the exactdeviceIdinsenderId·receiverId. send-filetype-cusis a regular expression for types to include;allow:falseinsend-fileoption.fileNameis an exclusion pattern.- Use
over·equalinsend-fileoption.fileSizeto define a minimum size criterion.
Response
{
"status_code": 201,
"message": "Created",
"data": { "automationId": "auto-abc123" }
}
Processing Steps
- (Optional) Check candidate items with
POST /api/devices/{deviceId}/files/search - Call
POST /api/automationswith filtertransferOptions - Use
data.automationIdfrom the response to check automation details and progress
Implementation Examples
"""Example 09 - Source filter.
Create an automation that transfers only files matching a filter (e.g. *.log / *.csv),
while excluding others (e.g. *.tmp) and applying size rules.
INNORIX_SOURCE_ID / TARGET_ID must be the exact device IDs - they are encoded into the
path token, so a device name will not work here (unlike the manual-transfer examples).
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "09-source-filter/example.py"
"""
import base64
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
SOURCE_ID = os.getenv("INNORIX_SOURCE_ID", "device-source-01")
TARGET_ID = os.getenv("INNORIX_TARGET_ID", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/var/log").replace("\\", "/")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/collect/logs").replace("\\", "/")
TIMEZONE = os.getenv("INNORIX_TIMEZONE", "UTC")
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def encode_path(device_id, raw_path):
"""Build the `<deviceId>_ino_<base64(path)>` token used by the automation API."""
normalized = str(raw_path or "").replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def now_iso():
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
automation = api("POST", "/api/automations", token, {
"name": "Source Filter Transfer",
"flowName": "Source Filter Transfer",
"details": [
{
"sourceItem": [{"hash": encode_path(SOURCE_ID, SOURCE_PATH), "isDir": True}],
"targetPath": encode_path(TARGET_ID, TARGET_PATH),
"senderId": SOURCE_ID,
"receiverId": TARGET_ID,
"step": 1,
"transferOptions": {
"noSchedule": True,
"send-filetype-cus": r"\.(log|csv)$", # include only .log and .csv files
"savepath": True,
"optionPath": "relative", # keep the folder structure under the target path
"target-action": "numbering", # on conflict, append a number
"send-fileoption": {
# only files >= 1024 bytes
"fileSize": {"size": 1024, "over": True, "equal": True},
# exclude *.tmp
"fileName": {"name": "*.tmp", "allow": False, "isMatchCase": False},
},
},
}
],
"transferType": "no_schedule",
"timezone": TIMEZONE,
"callbackURL": "",
"schedules": [
{
"type": "none",
"startDateType": "now",
"hour": "05",
"minute": "00",
"ampm": "pm",
"startDate": now_iso(),
"timezone": TIMEZONE,
}
],
})
print("automation created", {
"automationId": automation.get("automationId"),
"flowName": automation.get("flowName") or automation.get("automationName"),
})
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Target Options
Description
Control how files are placed on the target. Use transferOptions for an automation (POST /api/automations) to specify a subfolder below the destination path (savePath), the number of source path levels to preserve (optionPath), and conflict handling (target-action).
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Create target-options automation | POST | /api/automations |
| Get automation | GET | /api/automations/{automationId} |
Target-related transferOptions fields:
| Field | Type | Description |
|---|---|---|
savePath | string | Relative subfolder to create below the destination path (targetPath) (e.g., /incoming/2026/08) |
optionPath | number | Number of source path levels to preserve |
target-action | string | Conflict handling policy (e.g., numbering) |
global | object | Apply the same options to the entire automation |
Request
POST /api/automations
{
"name": "Target Options Transfer",
"flowName": "Target Options Transfer",
"transferType": "no_schedule",
"timezone": "Asia/Seoul",
"callbackURL": "",
"details": [
{
"sourceItem": [{ "hash": "device-source-01_ino_L3Zhci9sb2c=", "isDir": true }],
"targetPath": "device-target-01_ino_L2NvbGxlY3QvbG9ncw==",
"senderId": "device-source-01",
"receiverId": "device-target-01",
"step": 1,
"transferOptions": { "noSchedule": true, "send-fileoption": {}, "target-action": "numbering", "savePath": "/incoming/2026/08", "optionPath": 3 }
}
],
"global": { "noSchedule": true, "send-fileoption": {}, "target-action": "numbering", "savePath": "/incoming/2026/08", "optionPath": 3 },
"schedules": [{ "type": "none", "startDateType": "now", "hour": "05", "minute": "00", "ampm": "pm", "startDate": "2026-08-25T00:00:00Z", "timezone": "Asia/Seoul" }]
}
savePathis not a full path; it is a relative subfolder created belowtargetPath.optionPathis the number of source path levels to preserve (integer).- Specify the same options in both
details[].transferOptionsandglobal.
Response
{
"status_code": 201,
"message": "Created",
"data": { "automationId": "auto-abc123" }
}
Processing Steps
- Call
POST /api/automationswith targettransferOptions(+global) - Receive
data.automationIdfrom the response - Verify the configuration with
GET /api/automations/{automationId}
Implementation Examples
"""Example 10 - Target options.
Create an automation that controls how files land on the target: a fixed save path,
conflict numbering, and how much of the source path structure to keep.
INNORIX_SOURCE_ID / TARGET_ID must be the exact device IDs - they are encoded into the
path token, so a device name will not work here (unlike the manual-transfer examples).
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "10-target-options/example.py"
"""
import base64
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
SOURCE_ID = os.getenv("INNORIX_SOURCE_ID", "device-source-01")
TARGET_ID = os.getenv("INNORIX_TARGET_ID", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/var/log").replace("\\", "/")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/collect/logs").replace("\\", "/")
# Relative subfolder created under the target path (not a full path), e.g. "/incoming/2026/08".
SAVE_PATH = os.getenv("INNORIX_SAVE_PATH", "/incoming/2026/08")
TIMEZONE = os.getenv("INNORIX_TIMEZONE", "UTC")
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def encode_path(device_id, raw_path):
"""Build the `<deviceId>_ino_<base64(path)>` token used by the automation API."""
normalized = str(raw_path or "").replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def now_iso():
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Target options: fixed save path, conflict numbering, and how many path levels to keep.
target_options = {
"noSchedule": True,
"send-fileoption": {},
"target-action": "numbering", # on conflict, append a number
"savePath": SAVE_PATH, # subfolder created under the target path (relative)
"optionPath": 3, # keep this many levels of the source path
}
automation = api("POST", "/api/automations", token, {
"name": "Target Options Transfer",
"flowName": "Target Options Transfer",
"details": [
{
"sourceItem": [{"hash": encode_path(SOURCE_ID, SOURCE_PATH), "isDir": True}],
"targetPath": encode_path(TARGET_ID, TARGET_PATH),
"senderId": SOURCE_ID,
"receiverId": TARGET_ID,
"step": 1,
"transferOptions": target_options,
}
],
"global": target_options,
"transferType": "no_schedule",
"timezone": TIMEZONE,
"callbackURL": "",
"schedules": [
{
"type": "none",
"startDateType": "now",
"hour": "05",
"minute": "00",
"ampm": "pm",
"startDate": now_iso(),
"timezone": TIMEZONE,
}
],
})
print("automation created", {
"automationId": automation.get("automationId"),
"flowName": automation.get("flowName") or automation.get("automationName"),
})
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)File Synchronization
Continuously keep the file states of two locations consistent. Use a hot folder for real-time monitoring and automation for periodic synchronization.
Synchronization Overview
Description
Configure synchronization by creating an automation with transferType: "sync". In each details item, specify isSync: true and transferOptions.syncType (1=one-way, 2=two-way) to keep the source and target states consistent.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Create synchronization automation | POST | /api/automations |
| Get automation | GET | /api/automations/{automationId} |
| Delete automation | DELETE | /api/automations/{automationId} |
Request
POST /api/automations
{
"name": "Synchronization Overview",
"transferType": "sync",
"timezone": "Asia/Seoul",
"isUpcoming": false,
"schedules": [],
"details": [
{
"sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvc2hhcmVk", "filePath": "/data/shared", "isDir": true, "fileSize": 0 }],
"targetPath": "device-target-01_ino_L2RhdGEvbWlycm9y",
"senderId": "device-source-01",
"receiverId": "device-target-01",
"step": 1,
"fileCount": 0,
"folderCount": 1,
"sizeCount": 0,
"isSync": true,
"transferOptions": { "syncType": 1 }
}
]
}
Response
{
"status_code": 201,
"message": "Created",
"data": { "automationId": "auto-abc123" }
}
Processing Steps
- Call
POST /api/automationswithtransferType: "sync",details[].isSync: true, andtransferOptions.syncType - Receive
data.automationIdfrom the response - Check synchronization status with
GET /api/automations/{automationId}andDELETEit when no longer needed
Implementation Examples
"""Example 11 - Synchronization overview.
A synchronization is an automation whose detail has isSync=true (syncType 1 = one-way).
This example creates one, reads it back, then deletes it.
INNORIX_SOURCE_ID / TARGET_ID must be the exact device IDs - they are encoded into the
path token, so a device name will not work here (unlike the manual-transfer examples).
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "11-Synchronization-Overview/example.py"
"""
import base64
import os
import sys
from pathlib import Path
from urllib.parse import quote
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
SOURCE_ID = os.getenv("INNORIX_SOURCE_ID", "device-source-01")
TARGET_ID = os.getenv("INNORIX_TARGET_ID", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/shared").replace("\\", "/")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/mirror").replace("\\", "/")
TIMEZONE = os.getenv("INNORIX_TIMEZONE", "UTC")
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def encode_path(device_id, raw_path):
"""Build the `<deviceId>_ino_<base64(path)>` token used by the automation API."""
normalized = str(raw_path or "").replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# 1) Create a one-way synchronization automation (isSync=true, syncType=1).
print("Create automation")
automation = api("POST", "/api/automations", token, {
"name": "Synchronization Overview",
"transferType": "sync",
"timezone": TIMEZONE,
"isUpcoming": False,
"schedules": [],
"details": [
{
"sourceItem": [
{
"hash": encode_path(SOURCE_ID, SOURCE_PATH),
"filePath": SOURCE_PATH,
"isDir": True,
"fileSize": 0,
}
],
"targetPath": encode_path(TARGET_ID, TARGET_PATH),
"senderId": SOURCE_ID,
"receiverId": TARGET_ID,
"step": 1,
"fileCount": 0,
"folderCount": 1,
"sizeCount": 0,
"isSync": True,
"transferOptions": {"syncType": 1}, # 1 = one-way, 2 = two-way
}
],
})
automation_id = automation.get("automationId")
if not automation_id:
raise RuntimeError("Automation creation response did not include an automationId")
print("automation created", automation_id)
# 2) Read the automation back; its status reflects the current sync state.
print("Get automation")
print(api("GET", f"/api/automations/{quote(str(automation_id))}", token))
# 3) Delete the automation (example cleanup).
print("Delete automation")
api("DELETE", f"/api/automations/{quote(str(automation_id))}", token)
print("automation deleted", automation_id)
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Real-Time
Description
When you create a watch folder as a synchronization automation, synchronization is triggered as soon as a file enters the folder. File insertion can be simulated with the Explorer copy API (POST /api/explorer/copyFile/{deviceId}), which is the same call used by paste in the UI Explorer.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Create watch-folder automation | POST | /api/automations |
| Copy file (trigger) | POST | /api/explorer/copyFile/{deviceId} |
Processing Steps
- Create a watch-folder automation with
POST /api/automationsusingtransferType: "sync",syncType: 1(one-way) - When a file is created or copied into the watch folder, synchronization is applied automatically
- Trigger it by pasting a file into the watch folder with
POST /api/explorer/copyFile/{deviceId}
Implementation Examples
"""Example 12 - Real-time (watch folder).
Create a one-way watch-folder sync automation, then trigger it by copying a file
into the watched source folder (the same call the UI explorer makes on paste).
INNORIX_SOURCE_ID / TARGET_ID must be the exact device IDs - they are encoded into the
path token, so a device name will not work here (unlike the manual-transfer examples).
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "12-Real-Time/example.py"
"""
import base64
import os
import re
import sys
import time
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
SOURCE_ID = os.getenv("INNORIX_SOURCE_ID", "device-source-01")
TARGET_ID = os.getenv("INNORIX_TARGET_ID", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/watch")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
COPY_SOURCE_PATH = os.getenv("INNORIX_COPY_SOURCE_PATH", "/data/report.pdf")
COPY_SOURCE_IS_DIR = os.getenv("INNORIX_COPY_SOURCE_IS_DIR") == "true"
TIMEZONE = os.getenv("INNORIX_TIMEZONE", "UTC")
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def encode_path(device_id, raw_path):
"""Build the `<deviceId>_ino_<base64(path)>` token used by the automation and explorer APIs."""
if not raw_path:
return ""
if "_ino_" in raw_path:
return raw_path # already encoded
normalized = str(raw_path).replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def basename_of(file_path):
parts = [p for p in re.split(r"[\\/]", file_path) if p]
return parts[-1] if parts else "item"
def parent_path_of(file_path):
normalized = file_path.replace("\\", "/")
index = normalized.rfind("/")
if index <= 0:
return f"{normalized[:2]}/" if re.match(r"^[A-Za-z]:", normalized) else "/"
return normalized[:index]
def now_ms():
return int(time.time() * 1000)
def main():
require_env()
# 1) Log in.
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
print("Logged in successfully")
# 2) Create a one-way watch-folder sync automation (syncType 1 = one-way).
automation = api("POST", "/api/automations", token, {
"transferType": "sync",
"timezone": TIMEZONE,
"isUpcoming": False,
"schedules": [],
"details": [
{
"sourceItem": [
{
"hash": encode_path(SOURCE_ID, SOURCE_PATH),
"filePath": SOURCE_PATH,
"isDir": True,
"fileSize": 0,
}
],
"targetPath": encode_path(TARGET_ID, TARGET_PATH),
"senderId": SOURCE_ID,
"receiverId": TARGET_ID,
"step": 1,
"fileCount": 0,
"folderCount": 1,
"sizeCount": 0,
"transferOptions": {"syncType": 1}, # 1 = one-way, 2 = two-way
}
],
})
automation_id = automation.get("automationId") or automation.get("id")
print("Watch folder automation created:", automation_id)
# 3) Trigger the sync by copying a file into the watched folder.
# This mirrors the payload the UI explorer sends on paste.
file_name = basename_of(COPY_SOURCE_PATH)
copy_source_parent_path = parent_path_of(COPY_SOURCE_PATH)
copy_file_payload = {
"uuid": f"user{now_ms()}",
"path": encode_path(SOURCE_ID, SOURCE_PATH), # destination folder to paste into
"overwrite": False,
"listFiles": [
{
"name": file_name,
"size": 0,
"modificationTime": now_ms(),
"hash": encode_path(SOURCE_ID, COPY_SOURCE_PATH),
"filePath": COPY_SOURCE_PATH,
"isDir": COPY_SOURCE_IS_DIR,
"isTransfer": False,
"isWait": False,
"nextTransfer": None,
"isNew": True,
"isAccessPermission": True,
"phash": encode_path(SOURCE_ID, copy_source_parent_path),
}
],
"statusInfoUrl": f"/explorer/copyFile/{SOURCE_ID}",
}
copy_result = api("POST", f"/api/explorer/copyFile/{SOURCE_ID}", token, copy_file_payload)
print("copyFile result:", copy_result)
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Incremental
Description
Instead of sending everything every time, transfer only files changed since the last synchronization. Configure this by adding change criteria to a recurring automation.
ℹ️
incrementalis a preview flag. The request format is finalized, but the actual delta calculation is performed by the agent and may not be enabled depending on the server.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Create incremental transfer | POST | /api/transfers/manual (incremental: true) |
| Periodic synchronization | POST | /api/automations |
| Get status | GET | /api/transfers/{monitorId} |
Processing Steps
- Send only changes by calling
POST /api/transfers/manualwithincremental: true(usePOST /api/automationsfor periodic execution) - Select and transfer only files changed since the last synchronization
- Verify the applied result with
GET /api/transfers/{monitorId}
Implementation Examples
"""Example 13 - Incremental transfer.
Sync a large folder but send only the files that changed since the last run.
NOTE: `incremental` is a preview flag. The request interface is finalized, but the
actual delta calculation is performed by the agent and may not be active on every server.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "13-Incremental/example.py"
"""
import os
import sys
import time
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# INNORIX_SOURCE_DEVICE / TARGET_DEVICE accept a device ID, device name, or IP address.
SOURCE_DEVICE = os.getenv("INNORIX_SOURCE_DEVICE", "device-source-01")
TARGET_DEVICE = os.getenv("INNORIX_TARGET_DEVICE", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/bigfolder")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
TRANSFER_STATUS = {
"transferComplete": 2,
"transferError": 4,
"transferCancel": 5,
"transferPartialComplete": 9,
"transferFail": 99,
}
TERMINAL_TRANSFER_STATUSES = set(TRANSFER_STATUS.values())
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def wait_for_completion(monitor_id, token):
while True:
detail = api("GET", f"/api/transfers/{monitor_id}", token)
is_terminal = detail.get("isTerminal", detail.get("status") in TERMINAL_TRANSFER_STATUSES)
print({"monitorId": monitor_id, "status": detail.get("status"), "percent": detail.get("percent", 0)})
if is_terminal:
if detail.get("status") != TRANSFER_STATUS["transferComplete"]:
raise RuntimeError(detail.get("errorCode") or "Transfer failed")
return detail
time.sleep(2)
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Sync the whole folder daily, but send only changed files via the `incremental` flag.
transfer = api("POST", "/api/transfers/manual", token, {
"sourceDevice": SOURCE_DEVICE,
"targetDevice": TARGET_DEVICE,
"targetPath": TARGET_PATH,
"sourcePaths": [SOURCE_PATH],
"sendAllFolder": True,
"incremental": True, # send only the delta
})
print("transfer created", {"monitorId": transfer["monitorId"], "status": transfer.get("status")})
wait_for_completion(transfer["monitorId"], token)
print("Transfer completed")
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Advanced Usage
These use cases combine instant transfer, automation, and synchronization for real development scenarios.
Files Outside Git
Description
Transfer and synchronize files that are difficult to manage with Git, including large binaries, media, and datasets, between devices.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Create transfer | POST | /api/transfers/manual |
| Periodic synchronization | POST | /api/automations |
| Get status | GET | /api/transfers/{monitorId}/files |
Processing Steps
- Specify the managed folder/file as the source path
- Use
POST /api/transfers/manualfor one-time transfer andPOST /api/automationsfor ongoing management - Verify the applied result with
GET /api/transfers/{monitorId}/files
Build Artifacts
Description
Deliver build artifacts produced by a CI/CD pipeline to deployment target devices. Trigger the transfer from the pipeline and receive completion notification by webhook.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Create transfer | POST | /api/transfers/manual |
| Automated deployment | POST | /api/automations |
| Completion notification (webhook) | POST | /api/automations (processors) |
Processing Steps
- After the build completes, call
POST /api/transfers/manualfrom the pipeline (for multiple targets, use the File Distribution pattern) - Receive
data.monitorIdfrom the response - Receive deployment completion notification through automation
processors(webhook) (the Webhook pattern)
AI·Data
Description
Move large data including training datasets and inference results to collection servers or processing nodes. Use transfer completion as a trigger to connect a subsequent pipeline.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Data collection | POST | /api/transfers/manual |
| Periodic collection | POST | /api/automations |
| Subsequent trigger (webhook) | POST | /api/automations (processors) |
Processing Steps
- Specify the data location in
sourcePathsand callPOST /api/transfers/manual(for collection, use the File Collection pattern) - After completion, check status with
data.monitorId - Trigger subsequent processing (training·inference) with automation
processors(webhook) (the Webhook pattern)
Results·Operations
This section covers status checks, transfer controls, error handling, monitoring, and record management after a transfer is created.
Status·Results
Description
Retrieve per-file status for transfers in progress and results for completed transfers.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Transfer status·progress | GET | /api/transfers/{monitorId} |
| Get in-progress files | GET | /api/transfers/{monitorId}/files |
| Completed history details | GET | /api/transfer-history/{monitorId} |
| Completed history files | GET | /api/transfers/{monitorId}/files?state=history |
Response
{
"status_code": 200,
"message": "success",
"data": {
"monitorId": "mon-abc123",
"status": 2, "statusName": "COMPLETED", "statusLabel": "Complete",
"files": [
{
"path": "/data/report.pdf",
"fileToken": "L2RhdGEv...",
"status": 2, "statusName": "DONE", "statusLabel": "Complete",
"size": 20480
}
]
}
}
Processing Steps
- Check transfer status·progress with
GET /api/transfers/{monitorId}—status2=complete,4·5·9·99=failure and terminate. For per-file progress, useGET /api/transfers/{monitorId}/files - After completion, check the result summary with
GET /api/transfer-history/{monitorId} - For per-file details, use
GET /api/transfers/{monitorId}/files?state=history
Implementation Examples
"""Example 14 - Status & results.
List failed/error transfers for a device over the last 7 days, walking cursor-based pages.
INNORIX_TARGET_ID must be the exact device ID used by the transfer-history query.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "14-status-results/example.py"
"""
import json
import os
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from urllib.parse import urlencode
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
DEVICE_ID = os.getenv("INNORIX_TARGET_ID", "device-target-01")
PAGE_SIZE = int(os.getenv("INNORIX_PAGE_SIZE", "50"))
# Failure statuses (4 = error, 99 = fail).
TRANSFER_STATUS = {"transferError": 4, "transferFail": 99}
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def page_items(page):
"""The history endpoint may return a list or an {items}/{data} wrapper - normalize it."""
if isinstance(page, list):
return page
if isinstance(page, dict):
if isinstance(page.get("items"), list):
return page["items"]
if isinstance(page.get("data"), list):
return page["data"]
return []
def next_cursor(page):
if isinstance(page, dict):
return (page.get("pagination") or {}).get("nextCursor")
return None
def iso_days_ago(days):
dt = datetime.now(timezone.utc) - timedelta(days=days)
return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
def build_history_path(start_date, status_filter, cursor):
params = {
"deviceId": DEVICE_ID,
"statusFilter": status_filter,
"startDate": start_date,
"limit": str(PAGE_SIZE),
}
if cursor:
params["cursor"] = json.dumps(cursor)
return "/api/transfer-history?" + urlencode(params)
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Look at failed/error transfers for this device over the last 7 days.
start_date = iso_days_ago(7)
status_filter = ",".join(str(v) for v in [TRANSFER_STATUS["transferError"], TRANSFER_STATUS["transferFail"]])
# Walk cursor-based pages and collect every failed transfer.
failures = []
cursor = None
while True:
page = api("GET", build_history_path(start_date, status_filter, cursor), token)
items = page_items(page)
failures.extend(items)
cursor = next_cursor(page)
if len(items) < PAGE_SIZE:
break
if not cursor:
break
print(f"Failed transfers in last 7 days: {len(failures)}")
for item in failures:
print({
"monitorId": item.get("monitorId"),
"status": item.get("status"),
"statusName": item.get("statusName"),
"targetPath": item.get("targetPath"),
"finishedAt": item.get("finishedAt") or item.get("createdAt") or item.get("sortKey"),
})
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Transfer Control
Description
Control an in-progress transfer by pausing, resuming, or canceling it. When you start a transfer, a monitorId is issued; use this ID to check status and request pause/resume/cancel. Pause and resume are asynchronous, so pause when the transfer is actually in progress (transferring=6), then resume after the pause has taken effect (transferPause=3).
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Start transfer | POST | /api/transfers/manual |
| Get status | GET | /api/transfers/{monitorId}/automation-detail |
| Pause·resume·cancel | POST | /api/transfers/{monitorId}/pause · resume · cancel |
- Control actions (
pause·resume·cancel) arePOSTrequests with no body.
Processing Steps
- Start the transfer with
POST /api/transfers/manual→monitorId - Wait until
transferring(6), then pause withPOST .../pause - Wait until
transferPause(3), then resume withPOST .../resume(retry a few times if the state has not settled yet) - If needed, cancel with
POST .../cancel
Implementation Examples
"""Example 15 - Transfer control.
Create a transfer, then pause it, resume it, and once it is running again, cancel it.
Timing note: pause/resume are asynchronous. Send pause only after the transfer is
actually running (status transferring=6), and resume only after the pause has settled
(status transferPause=3). Resuming too early may be rejected while the state is still
settling, so we poll for the right state and retry resume a few times.
Endpoints:
POST /api/transfers/manual
GET /api/transfers/{monitorId} (transfer status)
POST /api/transfers/{monitorId}/pause
POST /api/transfers/{monitorId}/resume
POST /api/transfers/{monitorId}/cancel
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "15-transfer-control/example.py"
"""
import os
import sys
import time
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# INNORIX_SOURCE_DEVICE / TARGET_DEVICE accept a device ID, device name, or IP address.
SOURCE_DEVICE = os.getenv("INNORIX_SOURCE_DEVICE", "device-source-01")
TARGET_DEVICE = os.getenv("INNORIX_TARGET_DEVICE", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/report.pdf").replace("\\", "/")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
TRANSFER_STATUS = {
"transferComplete": 2,
"transferPause": 3,
"transferError": 4,
"transferCancel": 5,
"transferring": 6,
"transferPartialComplete": 9,
"transferFail": 99,
}
TERMINAL_TRANSFER_STATUSES = {
TRANSFER_STATUS["transferComplete"],
TRANSFER_STATUS["transferError"],
TRANSFER_STATUS["transferCancel"],
TRANSFER_STATUS["transferPartialComplete"],
TRANSFER_STATUS["transferFail"],
}
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def is_terminal(detail):
return detail.get("isTerminal", detail.get("status") in TERMINAL_TRANSFER_STATUSES)
def wait_for_status(monitor_id, token, wanted_status):
"""Poll the transfer detail until it reaches wanted_status or any terminal status."""
while True:
detail = api("GET", f"/api/transfers/{monitor_id}", token)
terminal = is_terminal(detail)
print({
"monitorId": monitor_id,
"status": detail.get("status"),
"statusName": detail.get("statusName"),
"isTerminal": terminal,
"percent": detail.get("percent", 0),
})
if detail.get("status") == wanted_status or terminal:
return detail
time.sleep(2)
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Create the transfer (sourcePaths is the plain-string convenience form of sourceItem).
transfer = api("POST", "/api/transfers/manual", token, {
"sourceDevice": SOURCE_DEVICE,
"targetDevice": TARGET_DEVICE,
"targetPath": TARGET_PATH,
"sourcePaths": [SOURCE_PATH],
})
monitor_id = transfer["monitorId"]
print("transfer created", {"monitorId": monitor_id, "status": transfer.get("status")})
# Wait until the transfer is actually running before pausing.
detail = wait_for_status(monitor_id, token, TRANSFER_STATUS["transferring"])
if is_terminal(detail):
print("transfer finished before it could be paused (too small/fast)")
return
# Pause, then wait until the pause has settled.
api("POST", f"/api/transfers/{monitor_id}/pause", token)
detail = wait_for_status(monitor_id, token, TRANSFER_STATUS["transferPause"])
if is_terminal(detail):
print("transfer already finished; nothing to resume")
return
# Resume, retrying while the server still reports the pause hasn't settled.
attempt = 0
while True:
try:
api("POST", f"/api/transfers/{monitor_id}/resume", token)
break
except Exception as error: # noqa: BLE001
if attempt >= 4:
raise
print(f"resume not ready yet ({error}); retrying...")
attempt += 1
time.sleep(2)
# Once the resumed transfer is running again, cancel it.
detail = wait_for_status(monitor_id, token, TRANSFER_STATUS["transferring"])
if not is_terminal(detail):
api("POST", f"/api/transfers/{monitor_id}/cancel", token)
# Wait until the transfer reaches a terminal status.
detail = wait_for_status(monitor_id, token, TRANSFER_STATUS["transferCancel"])
print("final", {"status": detail.get("status"), "statusName": detail.get("statusName")})
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Errors·Retry
Description
Classify and inspect failure causes by code, then retransfer only failed files or cancel multiple transfers at once.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Retry failures | POST | /api/transfers/{monitorId}/retry |
| Bulk cancel | POST | /api/transfers/bulk/cancel |
| Cancel single transfer | POST | /api/transfers/{monitorId}/cancel |
Request
POST /api/transfers/{monitorId}/retry
{
"filesRetry": ["/data/report.pdf", "/data/image.png"]
}
Response
{
"status_code": 200,
"message": "success",
"data": { "monitorId": "mon-abc123", "retried": 2 }
}
Processing Steps
- Check failed files with
GET /api/transfers/{monitorId}/files - Retransfer with
filesRetryinPOST /api/transfers/{monitorId}/retry - To clean up multiple transfers, use
POST /api/transfers/bulk/cancel(monitorIds)
Implementation Examples
"""Example 16 - Errors.
Find the most recent failed transfer, read its error code, and list the per-file
failure reasons, mapping error codes to a human-readable category.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "16-Errors/example.py"
"""
import os
import sys
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# Failure statuses (4 = error, 99 = fail).
TRANSFER_STATUS = {"transferError": 4, "transferFail": 99}
# Map an error-code prefix to a human-readable cause.
ERROR_CATEGORY = {
"NETWORK": "Network",
"PERMISSION": "Permission",
"CAPACITY": "Capacity",
}
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def rows(page):
"""Responses may wrap rows as a list or as {items}/{data}/{children} - normalize it."""
if isinstance(page, list):
return page
if isinstance(page, dict):
for key in ("items", "data", "children"):
if isinstance(page.get(key), list):
return page[key]
return []
def categorize(error_code):
if not error_code:
return "Other"
return ERROR_CATEGORY.get(error_code.split("_")[0], "Other")
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# 1) Find the single most recent failed transfer.
status_filter = ",".join(str(v) for v in [TRANSFER_STATUS["transferError"], TRANSFER_STATUS["transferFail"]])
history = api("GET", f"/api/transfer-history?statusFilter={status_filter}&limit=1", token)
failures = rows(history)
if not failures:
print("No failed transfers found")
return
monitor_id = failures[0]["monitorId"]
# 2) Read the transfer detail and classify its error code.
detail = api("GET", f"/api/transfers/{monitor_id}", token)
print({
"monitorId": monitor_id,
"status": detail.get("status"),
"errorCode": detail.get("errorCode"),
"cause": categorize(detail.get("errorCode")),
})
# 3) List the failed files and each file's failure reason.
files = api("GET", f"/api/transfers/{monitor_id}/files?state=any&size=100", token)
for file in rows(files):
print({
"name": file.get("sourceFileName") or file.get("targetFileName") or file.get("sourceFilePath"),
"state": file.get("statusName") or file.get("status"),
"errorCode": file.get("errorCode"),
"cause": categorize(file.get("errorCode")),
})
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Integrity Verification
Description
Verify that transferred files are identical to the source. If you specify checkIntegrity: true when creating a transfer, the server verifies the file checksum after transfer.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Integrity-verified transfer | POST | /api/transfers/manual (checkIntegrity: true) |
| Get status | GET | /api/transfers/{monitorId} |
Request
POST /api/transfers/manual
{
"sourceDevice": "device-source-01",
"targetDevice": "device-target-01",
"targetPath": "/data/incoming",
"sourcePaths": ["/data/report.pdf"],
"checkIntegrity": true
}
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
Processing Steps
- Create a transfer with
POST /api/transfers/manual, includingcheckIntegrity: true - Check status until completion with
GET /api/transfers/{monitorId} - After completion, check the verification result (
integrityVerified) — if there is a mismatch, correct it by retransferring
Implementation Examples
"""Example 17 - Integrity verification.
Transfer a file with integrity checking enabled, then wait until it completes.
With `checkIntegrity`, the server verifies the file checksum after transfer.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "17-Integrity Verification/example.py"
"""
import os
import sys
import time
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# INNORIX_SOURCE_DEVICE / TARGET_DEVICE accept a device ID, device name, or IP address.
SOURCE_DEVICE = os.getenv("INNORIX_SOURCE_DEVICE", "device-source-01")
TARGET_DEVICE = os.getenv("INNORIX_TARGET_DEVICE", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/report.pdf").replace("\\", "/")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
TRANSFER_STATUS = {
"transferComplete": 2,
"transferError": 4,
"transferCancel": 5,
"transferPartialComplete": 9,
"transferFail": 99,
}
TERMINAL_TRANSFER_STATUSES = set(TRANSFER_STATUS.values())
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def wait_for_completion(monitor_id, token):
while True:
detail = api("GET", f"/api/transfers/{monitor_id}", token)
is_terminal = detail.get("isTerminal", detail.get("status") in TERMINAL_TRANSFER_STATUSES)
print({
"monitorId": monitor_id,
"status": detail.get("status"),
"statusName": detail.get("statusName"),
"isTerminal": is_terminal,
"percent": detail.get("percent", 0),
})
if is_terminal:
if detail.get("status") != TRANSFER_STATUS["transferComplete"]:
raise RuntimeError(detail.get("errorCode") or "Transfer failed")
return detail
time.sleep(2)
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# Start a manual transfer with integrity checking enabled.
transfer = api("POST", "/api/transfers/manual", token, {
"sourceDevice": SOURCE_DEVICE,
"targetDevice": TARGET_DEVICE,
"targetPath": TARGET_PATH,
"sourcePaths": [SOURCE_PATH],
"checkIntegrity": True, # verify the file checksum after transfer
})
print("transfer created", {"monitorId": transfer["monitorId"], "status": transfer.get("status")})
# Wait until the transfer reaches a terminal status.
detail = wait_for_completion(transfer["monitorId"], token)
verified = detail.get("integrityVerified", detail.get("checkIntegrity", True))
print("Transfer completed", {"integrityVerified": verified})
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Monitoring
Description
Continuously observe transfer, automation, and device status.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| Transfer progress | GET | /api/transfers/{monitorId}/files |
| Automation details | GET | /api/automations/{automationId}/details |
| Device connectivity status | GET | /api/devices/{deviceId}/connectivity |
Processing Steps
- Periodically poll in-progress transfers with
GET /api/transfers/{monitorId}/files - Check automation progress with
GET /api/automations/{automationId}/details - Check whether devices are online with
GET /api/devices/{deviceId}/connectivity
Implementation Examples
"""Example 18 - Monitoring.
Start a transfer, then poll the active-transfers list and print each one's progress,
speed, and estimated remaining time until the transfer we started finishes.
INNORIX_SOURCE_DEVICE / TARGET_DEVICE accept a device ID, device name, or IP address.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "18-Monitoring/example.py"
"""
import os
import sys
import time
from pathlib import Path
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
# INNORIX_SOURCE_DEVICE / TARGET_DEVICE accept a device ID, device name, or IP address.
SOURCE_DEVICE = os.getenv("INNORIX_SOURCE_DEVICE", "device-source-01")
TARGET_DEVICE = os.getenv("INNORIX_TARGET_DEVICE", "device-target-01")
SOURCE_PATH = os.getenv("INNORIX_SOURCE_PATH", "/data/report.pdf").replace("\\", "/")
TARGET_PATH = os.getenv("INNORIX_TARGET_PATH", "/data/incoming")
POLL_COUNT = int(os.getenv("INNORIX_POLL_COUNT", "10"))
POLL_INTERVAL_MS = int(os.getenv("INNORIX_POLL_INTERVAL_MS", "2000"))
# Status 6 = transferring (currently active); the rest are terminal (no longer running).
TRANSFER_STATUS = {
"transferComplete": 2,
"transferError": 4,
"transferCancel": 5,
"transferring": 6,
"transferPartialComplete": 9,
"transferFail": 99,
}
TERMINAL_TRANSFER_STATUSES = {
TRANSFER_STATUS["transferComplete"],
TRANSFER_STATUS["transferError"],
TRANSFER_STATUS["transferCancel"],
TRANSFER_STATUS["transferPartialComplete"],
TRANSFER_STATUS["transferFail"],
}
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def page_items(page):
"""The list endpoint may return a list or an {items}/{data} wrapper - normalize it."""
if isinstance(page, list):
return page
if isinstance(page, dict):
if isinstance(page.get("items"), list):
return page["items"]
if isinstance(page.get("data"), list):
return page["data"]
return []
def is_terminal(detail):
return detail.get("isTerminal", detail.get("status") in TERMINAL_TRANSFER_STATUSES)
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# 1) Start a transfer so there is something to monitor.
transfer = api("POST", "/api/transfers/manual", token, {
"sourceDevice": SOURCE_DEVICE,
"targetDevice": TARGET_DEVICE,
"targetPath": TARGET_PATH,
"sourcePaths": [SOURCE_PATH],
"sendAllFolder": False,
})
monitor_id = transfer["monitorId"]
print("transfer created", {"monitorId": monitor_id, "status": transfer.get("status")})
# 2) Poll the active-transfers list until the transfer we started reaches a terminal status.
for tick in range(1, POLL_COUNT + 1):
active = api("GET", f"/api/transfers?statusFilter={TRANSFER_STATUS['transferring']}&limit=50", token)
transfers = page_items(active)
print(f"--- poll {tick}/{POLL_COUNT}: {len(transfers)} active transfers ---")
for item in transfers:
print({
"monitorId": item.get("monitorId"),
"status": item.get("status"),
"statusName": item.get("statusName"),
"percent": item.get("percent", item.get("progress", 0)),
"transferSpeed": item.get("transferSpeed", 0),
"estimateTime": item.get("estimateTime", 0),
})
# Stop once the transfer we started has finished.
detail = api("GET", f"/api/transfers/{monitor_id}", token)
if is_terminal(detail):
print("monitored transfer finished", {
"status": detail.get("status"),
"statusName": detail.get("statusName"),
"percent": detail.get("percent", 0),
})
break
if tick < POLL_COUNT:
time.sleep(POLL_INTERVAL_MS / 1000)
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)Audit History
Description
Retrieve and export transfer history to retain it as an operational record.
APIs Used
| Purpose | Method | Endpoint |
|---|---|---|
| History details | GET | /api/transfer-history/{monitorId} |
| Export history (CSV) | GET | /api/transfer-history/export |
Processing Steps
- Retrieve an individual transfer record with
GET /api/transfer-history/{monitorId} - Filter by period·status·keyword and export CSV with
GET /api/transfer-history/export- Query:
periodDays,status,searchKeyword,page,size,sort
- Query:
Implementation Examples
"""Example 19 - Audit history.
Resolve a device, list its transfer history for the last 30 days, then read the
file-level audit detail for the most recent record.
INNORIX_SOURCE_DEVICE accepts a device ID, device name, or IP address; the resolve
endpoint turns it into the exact device ID used by the history query.
Required environment variables: INNORIX_EMAIL, INNORIX_PASSWORD, INNORIX_WORKSPACE_ID
See ../.env.example for the full list and defaults.
Run: python "19-Audit History/example.py"
"""
import os
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from urllib.parse import quote
import requests
from dotenv import load_dotenv
# Load .env from the example's own folder first (so a single downloaded example works
# with a .env placed next to it), then fall back to the shared python/.env one level up.
# python-dotenv does not overwrite variables that are already set, so the folder-local
# .env wins and the shared one only fills in whatever is missing.
_here = Path(__file__).resolve().parent
load_dotenv(_here / ".env") # same folder as this script
load_dotenv(_here.parent / ".env") # shared python/.env (used by the full example set)
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
EMAIL = os.getenv("INNORIX_EMAIL")
PASSWORD = os.getenv("INNORIX_PASSWORD")
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")
SOURCE_DEVICE = os.getenv("INNORIX_SOURCE_DEVICE", "device-source-01")
HISTORY_DAYS = int(os.getenv("INNORIX_HISTORY_DAYS", "30"))
def api(method, path, token=None, body=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(method, BASE_URL + path, headers=headers, json=body)
try:
payload = response.json()
except ValueError:
payload = {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def require_env():
for name, value in {
"INNORIX_EMAIL": EMAIL,
"INNORIX_PASSWORD": PASSWORD,
"INNORIX_WORKSPACE_ID": WORKSPACE_ID,
}.items():
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
def page_items(page):
"""The history endpoint may return a list or an {items}/{data} wrapper - normalize it."""
if isinstance(page, list):
return page
if isinstance(page, dict):
if isinstance(page.get("items"), list):
return page["items"]
if isinstance(page.get("data"), list):
return page["data"]
return []
def audit_view(records):
"""Keep only the audit-relevant fields for readable output."""
view = []
for item in records[:10]:
view.append({
"monitorId": item.get("monitorId"),
"transferId": item.get("transferId") or item.get("id"),
"sourceDeviceName": item.get("sourceDeviceName"),
"targetDeviceName": item.get("targetDeviceName"),
"sourcePath": item.get("sourcePath") or item.get("name") or item.get("path"),
"targetPath": item.get("targetPath"),
"status": item.get("status"),
"statusName": item.get("statusName"),
"createdAt": item.get("createdAt"),
})
return view
def iso_days_ago(days):
dt = datetime.now(timezone.utc) - timedelta(days=days)
return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
def main():
require_env()
login = api("POST", "/api/auth/login", None, {"email": EMAIL, "password": PASSWORD})
token = login["user"]["accessToken"]
# 1) Resolve a device ID / name / IP into its exact device ID.
device = api("GET", f"/api/devices/resolve?name={quote(SOURCE_DEVICE)}", token)
# resolve returns {matchCount, devices: [{deviceId, ...}]}; fall back to a flat deviceId.
devices = (device or {}).get("devices") or []
device_id = devices[0].get("deviceId") if devices else (device or {}).get("deviceId")
if not device_id:
raise RuntimeError(f'No device matched "{SOURCE_DEVICE}"')
print("resolved device", {"input": SOURCE_DEVICE, "deviceId": device_id})
# 2) List the device's transfer history for the last N days.
start_date = iso_days_ago(HISTORY_DAYS)
history = api(
"GET",
f"/api/transfer-history?deviceId={quote(str(device_id))}&startDate={start_date}&limit=50",
token,
)
records = page_items(history)
print(f"history records in last {HISTORY_DAYS} days: {len(records)}")
print(audit_view(records))
# 3) Read file-level audit detail for the most recent record (if any).
monitor_id = records[0].get("monitorId") if records else None
if not monitor_id:
print("No transfer history record was found; nothing to audit.")
return
files = api("GET", f"/api/transfers/{monitor_id}/files?state=any&size=100", token)
# The /files response returns rows under `children` (sourceFileName / statusName fields).
file_rows = (files or {}).get("children") or page_items(files)
print(
f"file-level audit for {monitor_id}",
[
{
"name": f.get("sourceFileName") or f.get("targetFileName") or f.get("sourceFilePath"),
"state": f.get("statusName") or f.get("status"),
"status": f.get("status"),
}
for f in file_rows
],
)
if __name__ == "__main__":
try:
main()
except Exception as error: # noqa: BLE001
print(error, file=sys.stderr)
sys.exit(1)