Sync Files Across Systems

Build a file synchronization workflow with the INNORIX Transfer Builder. Configure connected systems, synchronization behavior, and generate the setup.

Sync Files Across Systems monitors a folder and automatically transfers files when they are created or changed. Once created, it runs continuously as a persistent automation. There are two key points to note in the request.

  • The request's transferType is "sync".
  • The user does not select a start time. However, the API request format requires the schedules field, so pass the default now value as-is. Actual execution is determined by folder monitoring, not the schedule.

Getting started#

Prerequisites#

  1. API Key — Generate it from the profile menu at the bottom left of the product → Developer. The Workspace ID is also displayed on the same screen. To generate one via API: POST /api/auth/api-keys (Bearer access token, no body) → data.apiKey.
  2. Two deviceIds — One for the monitored side (Source) and one for the destination side (Target). Select a device under Devices in the product, and its ID is displayed at the top right.
  3. Paths — The folder to monitor (sourceItem[].filePath) and the destination folder (targetPath). Both use absolute paths separated by slashes (/), and because the monitored item must be a folder, use isDir: true. targetPath must not be empty or /.

Use one of the following two authentication methods.

http
x-api-key: <API Key>                  # long-lived key (recommended)
Authorization: Bearer <accessToken>   # short-lived token from login

Add one more header only when you need to specify a workspace. This header is not an authentication method; it specifies the target workspace.

http
x-workspace-id: <Workspace ID>        # optional

The base URL is https://app.innorix.com.

Quick start#

Follow these steps to run the bundle downloaded through Get API Code in the builder.

  1. Choose options in the Transfer Builder → Get API Code → select a language → download the zip
  2. Extract the archive, open .env, and fill in INNORIX_API_KEY, SOURCE_ID, TARGET_ID, the monitored folder (SOURCE_PATH), and the destination folder (TARGET_PATH)
  3. Run it with the command below
  4. Use the returned automationId to check the transfer status
Language Requirements Run
Python Python 3.8+ pip install requests → python combo_builder.py
Node.js Node.js 18+ (no dependencies) node combo_builder.js
Java JDK 11+ (no dependencies) java ComboBuilder.java or javac ComboBuilder.java && java ComboBuilder
C# .NET 8+ dotnet run

ℹ️ The requirements above apply to the bundled examples. The Java excerpt in this document uses text blocks (""") for readability, so it requires JDK 17+. The bundled ComboBuilder.java works with JDK 11+.

ℹ️ The bundled combo_builder.* reads .env directly from the same folder (without an additional library). The excerpts in this document, however, read values from environment variables, so if you copy and run them directly, export the values as shown below before running them.

macOS, Linux

bash
export INNORIX_API_KEY=your-api-key
export SOURCE_ID=device-source-01
export SOURCE_PATH=D:/hotfolder        # folder to watch
export TARGET_ID=device-target-01
export TARGET_PATH=E:/mirror
export SYNC_DIRECTION=one_way          # one_way | two_way
export SYNC_WATCH=file_created         # file_created | file_modified

Windows PowerShell (in CMD, use the format set INNORIX_API_KEY=your-api-key)

powershell
$env:INNORIX_API_KEY="your-api-key"
$env:SOURCE_ID="device-source-01"
$env:SOURCE_PATH="D:/hotfolder"
$env:TARGET_ID="device-target-01"
$env:TARGET_PATH="E:/mirror"
$env:SYNC_DIRECTION="one_way"
$env:SYNC_WATCH="file_created"

Create a sync#

Sync settings#

Sync behavior is controlled by two values in transferOptions.

Direction — syncType

Value Behavior
1 One-Way — Source → Target
2 Two-Way — Changes on either side are reflected on the other

Watch condition — watchFolderType

Value Behavior
1 Newly created files only
2 Modified files only
3 Both created and modified files — supported only on some server versions

ℹ️ If the target server does not support 3, the request is rejected. If support has not been confirmed, create two sync automations using 1 or 2.

Create a sync#

json
{
  "name": "hot-folder-sync",
  "flowName": "hot-folder-sync",
  "transferType": "sync",
  "timezone": "Asia/Seoul",
  "details": [
    {
      "senderId": "<sourceDeviceId>",
      "receiverId": "<targetDeviceId>",
      "sourceItem": [{ "filePath": "D:/hotfolder", "isDir": true }],
      "targetPath": "E:/mirror",
      "step": 1,
      "transferOptions": {
        "noSchedule": false,
        "target-action": "overwrite",
        "syncType": 1,
        "watchFolderType": 1,
        "checkIntegrity": true
      }
    }
  ],
  "schedules": [
    { "type": "none", "startDateType": "now", "startDate": "2026-09-14T02:00:00.000Z", "timezone": "Asia/Seoul" }
  ],
  "step": 1,
  "isUpcoming": false
}
  • syncType, watchFolderType use the values described under Sync settings above.
  • transferOptions.target-action controls what happens when names conflict. Use one of overwrite (overwrite), numbering (append a number to the name), nosend (skip).
  • startDate is an example value. Use the current UTC time at the time of the request (the example code below calculates the current time each time it runs).

As described above, pass the default now value in schedules.

# pip install requests
import os, time, requests

BASE = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
HEADERS = {"x-api-key": os.environ["INNORIX_API_KEY"], "Content-Type": "application/json"}
# Every setting comes from an environment variable (second argument is the default).
TZ          = os.getenv("SCHEDULE_TZ", "Asia/Seoul")
SOURCE_ID   = os.environ["SOURCE_ID"]
SOURCE_PATH = os.getenv("SOURCE_PATH", "D:/hotfolder")   # folder to watch
TARGET_ID   = os.environ["TARGET_ID"]
TARGET_PATH = os.getenv("TARGET_PATH", "E:/mirror")

SYNC_TYPE = {"one_way": 1, "two_way": 2}
WATCH = {"file_created": 1, "file_modified": 2, "both": 3}   # both(3) is supported on some server versions only
SYNC_DIRECTION = os.getenv("SYNC_DIRECTION", "one_way")
SYNC_WATCH     = os.getenv("SYNC_WATCH", "file_created")

def now_iso():
    return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())

def call(method, path, body=None, params=None):
    r = requests.request(method, BASE + path, headers=HEADERS,
                         json=body, params=params, timeout=30)
    if not r.ok:
        raise RuntimeError(f"API {r.status_code}: {r.text[:500]}")
    return (r.json() or {}).get("data")

body = {
    "name": "hot-folder-sync",
    "flowName": "hot-folder-sync",
    "transferType": "sync",                       # "sync", not "normal"
    "timezone": TZ,
    "details": [{
        "senderId": SOURCE_ID,
        "receiverId": TARGET_ID,
        "sourceItem": [{"filePath": SOURCE_PATH, "isDir": True}],   # watched folder (always a folder)
        "targetPath": TARGET_PATH,
        "step": 1,
        "transferOptions": {
            "noSchedule": False,
            "target-action": "overwrite",
            "syncType": SYNC_TYPE[SYNC_DIRECTION],      # 1=One-Way, 2=Two-Way
            "watchFolderType": WATCH[SYNC_WATCH],       # 1=created, 2=modified, 3=both
            "checkIntegrity": True,
        },
    }],
    # Required by the request format; folder watching is what triggers a run.
    "schedules": [{"type": "none", "startDateType": "now",
                   "startDate": now_iso(), "timezone": TZ}],
    "step": 1,
    "isUpcoming": False,
}

automation_id = call("POST", "/api/automations", body)["automationId"]
print(f"sync automation created: {automation_id}  (one-way, on file create)")

Verify operation#

Because a sync automation triggers a transfer whenever a file is created, there may be no transfer in progress at the time you query it. Add a file to the monitored folder and query the list; a transfer row will appear.

http
GET /api/transfers?automationId=<automationId>     -> rows in data.data[] whose type is not automation|history|flow
GET /api/transfers/<monitorId>                     → status, percent, isTerminal

A transfer currently syncing is shown with status code 12 (syncing). When it ends with 2 (complete), that individual file has finished syncing, while the automation itself continues monitoring.

SKIP_ROW_TYPES = {"automation", "history", "flow"}
STATUS = {-1: "queued", 0: "waiting", 1: "started", 2: "complete", 3: "paused",
          4: "error", 5: "cancelled", 6: "transferring", 7: "skipped", 8: "retry",
          9: "partial-complete", 11: "virus-scanning", 12: "syncing", 99: "fail"}

def watch(automation_id, seconds=60, interval=5):
    """Drop a file into the watched folder, then follow the transfers it triggers."""
    deadline, seen = time.time() + seconds, set()
    while time.time() < deadline:
        result = call("GET", "/api/transfers", params={"automationId": automation_id})
        records = result.get("data") if isinstance(result, dict) else result
        for r in records or []:
            if r.get("type") in SKIP_ROW_TYPES:
                continue
            mid = r.get("monitorId") or r.get("id")
            detail = call("GET", f"/api/transfers/{mid}") or {}
            status = detail.get("status")
            key = (mid, status)
            if key not in seen:
                seen.add(key)
                print(f"  {mid}: {STATUS.get(status, status)} ({detail.get('percent', 0)}%)")
        time.sleep(interval)

watch(automation_id)

Transfer options#

File options#

Sync automations can use the same file options in details[].transferOptions and the same processors[].

Item Key Notes
Extension filter send-fileoption.extension Useful when syncing only specific extensions
Size filter send-fileoption.fileSize Useful for excluding temporary files
Name exclusion send-fileoption.fileName Excludes in-progress files such as .tmp and ~$
Duplicate handling target-action overwrite is typical for synchronization
Integrity verification checkIntegrity Verifies each file

File option values use the following format.

json
{
  "noSchedule": false,
  "target-action": "overwrite",
  "syncType": 1,
  "watchFolderType": 1,
  "checkIntegrity": true,
  "send-fileoption": {
    "extension": { "extension": ["pdf", "xlsx"], "allow": true },
    "fileSize": { "size": 1048576, "over": true, "equal": true },
    "fileName": { "name": "tmp", "allow": false }
  }
}

target-action controls what happens when names conflict — overwrite (overwrite), numbering (append a number to the name), nosend (skip).

ℹ️ If the monitored folder contains many temporary files that are still being worked on, configure name and extension filters first. Without filters, files that are still being saved may also be transferred.

After-transfer actions#

Actions after a transfer completes fall into two categories.

① Processors attached to the automation — processors[] in the body

json
{
  "processors": [
    { "events": "Run", "type": "https", "method": "POST",
      "url": "https://api.example.com/webhook", "body": "{\"event\":\"done\"}" },
    { "category": "monitoring", "type": "grafana", "name": "builder-grafana",
      "config": { "baseUrl": "https://grafana.company.com", "apiToken": "***" },
      "notificationConfig": { "events": { "completed": true, "error": true } } }
  ]
}
  • Run API — An HTTP hook called for each transfer.
  • Monitoring (Grafana, Datadog, Prometheus, etc.) — Attached to this automation rather than the entire workspace. Available events are started, completed, paused, recovered, deviceConnected, deviceDisconnected.

② Workspace-wide integrations — POST /api/integrations

Message (Slack, Teams, Discord …), Virus scan (ClamAV, Microsoft Defender …), and Email (SES, SendGrid) are registered at the workspace level rather than on an individual transfer.

json
{
  "name": "builder-slack",
  "type": "slack",
  "category": "notification",
  "config": { "webhookUrl": "https://hooks.slack.com/services/XXX", "channel": "#transfers" },
  "notificationConfig": { "events": { "completed": true, "error": true } }
}

category is Message → notification, Virus scan → security, Email → email. You can check the required settings for each provider with GET /api/integrations/rules/{type}. Event names are started, completed, paused, resumed, recovered, canceled, error, skipped.

Reference#

Builder UI ↔ .env ↔ API mapping#

Builder UI .env API
Tab = Sync Files Across Systems TRANSFER_TYPE=sync transferType: "sync"
From device SOURCE_ID details[].senderId
Monitored folder SOURCE_PATH details[].sourceItem[].filePath (isDir: true)
To device TARGET_ID details[].receiverId
To path TARGET_PATH details[].targetPath
One-Way / Two-Way SYNC_DIRECTION=one_way|two_way transferOptions.syncType = 1 / 2
File Created / Modified / Both SYNC_WATCH=file_created|file_modified|both transferOptions.watchFolderType = 1 / 2 / 3
(No Start option) START_WHEN ignored schedules uses the default value

Common errors#

Symptom Cause and solution
No transfer starts after adding a file Monitoring does not work if isDir is false. The monitored item must always be a folder.
watchFolderType: 3 rejected Both requires server support. Create separate sync automations using 1 (created) or 2 (modified).
Two-Way only syncs one side Both agents must be online. Check the target device connection status.
Temporary files are also transferred Exclude values such as tmp with send-fileoption.fileName, or specify an extension allowlist.
Behavior is unchanged after changing the schedule Sync automations are not triggered by a schedule. The schedules value is ignored.
Transfer status remains 12 12 (syncing) is a normal operating state. Each file is marked 2 (complete) individually.