Sync — Keep folders continuously synchronized

Sync 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 body'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
LanguageRequirementsRun
PythonPython 3.8+pip install requestspython combo_builder.py
Node.jsNode.js 18+ (no dependencies)node combo_builder.js
JavaJDK 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

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

Watch condition — watchFolderType

ValueBehavior
1Newly created files only
2Modified files only
3Both 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 Sync 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 can use the same file options in details[].transferOptions and the same processors[].

ItemKeyNotes
Extension filtersend-fileoption.extensionUseful when syncing only specific extensions
Size filtersend-fileoption.fileSizeUseful for excluding temporary files
Name exclusionsend-fileoption.fileNameExcludes in-progress files such as .tmp and ~$
Duplicate handlingtarget-actionoverwrite is typical for synchronization
Integrity verificationcheckIntegrityVerifies 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 automationprocessors[] 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 integrationsPOST /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.envAPI
Tab = SyncTRANSFER_TYPE=synctransferType: "sync"
From deviceSOURCE_IDdetails[].senderId
Monitored folderSOURCE_PATHdetails[].sourceItem[].filePath (isDir: true)
To deviceTARGET_IDdetails[].receiverId
To pathTARGET_PATHdetails[].targetPath
One-Way / Two-WaySYNC_DIRECTION=one_way|two_waytransferOptions.syncType = 1 / 2
File Created / Modified / BothSYNC_WATCH=file_created|file_modified|bothtransferOptions.watchFolderType = 1 / 2 / 3
(No Start option)START_WHEN ignoredschedules uses the default value

Common errors

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