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
transferTypeis"sync". - The user does not select a start time. However, the API request format requires the
schedulesfield, so pass the defaultnowvalue as-is. Actual execution is determined by folder monitoring, not the schedule.
Getting started
Prerequisites
- 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. - 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.
- 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, useisDir: true.targetPathmust not be empty or/.
Use one of the following two authentication methods.
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.
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.
- Choose options in the Transfer Builder → Get API Code → select a language → download the zip
- Extract the archive, open
.env, and fill inINNORIX_API_KEY,SOURCE_ID·TARGET_ID, the monitored folder (SOURCE_PATH), and the destination folder (TARGET_PATH) - Run it with the command below
- Use the returned
automationIdto 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 bundledComboBuilder.javaworks with JDK 11+.
ℹ️ The bundled
combo_builder.*reads.envdirectly 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
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)
$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 using1or2.
Create a sync
{
"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·watchFolderTypeuse the values described under Sync settings above.transferOptions.target-actioncontrols what happens when names conflict. Use one ofoverwrite(overwrite) ·numbering(append a number to the name) ·nosend(skip).startDateis 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.
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[].
| 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.
{
"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
{
"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.
{
"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 | 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 is 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. |