INNORIX
Transfer BuilderTransfer FinderDevelopersResourcesCustomers
Start Free
INNORIX

LET FILES
MOVE THEMSELVES

INNORIX provides enterprise file infrastructure for moving and automating files across every system and environment.
Trusted by more than 5,000 enterprise and government agencies.

START HERE

  • Build the Transfer You Need
  • Find the Transfer You Need

POPULAR TRANSFERS

  • Sync Team Folders
  • Send Large Files to Clients
  • Explore Files Across Systems
  • Migrate FTP, SFTP, SCP & rsync
  • Add Transfer to Any App
  • Add Web Upload & Download
  • Build AI & Data Workflows
  • Browse All Transfers→

DEVELOPERS

  • Developer Center
  • Examples
  • API Quickstart
  • Developer Guide
  • API Reference
  • GitHub

RESOURCES

  • Resource Center
  • Product Guide
  • Integrations
  • Deploy & Manage
  • Help Center

CUSTOMERS

  • Government
  • Public Sector
  • Manufacturing
  • Engineering
  • Finance
  • Distribution
  • IT/Telecom
  • Media
  • Healthcare
  • Education

PLANS

  • Pricing

COMPANY

About Us

OTHER INNORIX PRODUCT

Al.bert — Smart Traffic AI

GLOBAL OFFICES

  • New York, USA
  • Seoul, South Korea
  • Ho Chi Minh City, Vietnam
  • View Office Locations→

(C)2026 INNORIX. All rights reserved.

  • Security
  • Status
  • Terms
  • Privacy
  • Cookies
  1. Developers
  2. Examples
  3. Sync Files Across Systems

Sync Files Across Systems

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

Deploy & Manage
Exabyter
  • Send Files to One
  • Send Files to Many
  • Collect Files from Many
  • Sync Files Across Systems
  • Web Upload & Download

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)")
// JDK 17+ (no external dependencies)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class FolderSync {

    static final String BASE = env("INNORIX_BASE_URL", "https://app.innorix.com");
    static final String API_KEY = env("INNORIX_API_KEY", "");
    static final HttpClient HTTP = HttpClient.newHttpClient();

    // Every setting comes from an environment variable (second argument is the default).
    // syncType        : 1=One-Way, 2=Two-Way
    // watchFolderType : 1=File Created, 2=File Modified, 3=Both (some server versions only)
    static final int SYNC_TYPE = env("SYNC_DIRECTION", "one_way").equals("two_way") ? 2 : 1;
    static final int WATCH_FOLDER_TYPE = switch (env("SYNC_WATCH", "file_created")) {
        case "file_modified" -> 2;
        case "both" -> 3;
        default -> 1;
    };

    /** Reads an environment variable, falling back to the default when it is empty. */
    static String env(String key, String dflt) {
        String v = System.getenv(key);
        return (v == null || v.isBlank()) ? dflt : v;
    }

    /** Extracts one string field from the response. Like the downloaded example, it needs no
     *  external JSON library. Use Jackson or Gson when you need to walk deeper structures. */
    static String jsonString(String json, String key) {
        Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json);
        return m.find() ? m.group(1) : null;
    }

    /** Extracts one integer field from the response. */
    static int jsonInt(String json, String key, int dflt) {
        Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*(-?\\d+)").matcher(json);
        return m.find() ? Integer.parseInt(m.group(1)) : dflt;
    }

    static String call(String method, String path, String body) throws Exception {
        HttpRequest.BodyPublisher pub = body == null
                ? HttpRequest.BodyPublishers.noBody()
                : HttpRequest.BodyPublishers.ofString(body);
        HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
                .header("Content-Type", "application/json")
                .header("x-api-key", API_KEY)
                .method(method, pub)
                .build();
        HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() >= 400) throw new RuntimeException("API " + res.statusCode() + ": " + res.body());
        return res.body();
    }

    public static void main(String[] args) throws Exception {
        String sourceId   = env("SOURCE_ID", "");
        String sourcePath = env("SOURCE_PATH", "D:/hotfolder");
        String targetId   = env("TARGET_ID", "");
        String targetPath = env("TARGET_PATH", "E:/mirror");
        String nowIso = Instant.now().truncatedTo(ChronoUnit.SECONDS).toString().replace("Z", ".000Z");

        String body = """
                {
                  "name": "hot-folder-sync",
                  "flowName": "hot-folder-sync",
                  "transferType": "sync",
                  "timezone": "Asia/Seoul",
                  "details": [{
                    "senderId": "%s",
                    "receiverId": "%s",
                    "sourceItem": [{ "filePath": "%s", "isDir": true }],
                    "targetPath": "%s",
                    "step": 1,
                    "transferOptions": {
                      "noSchedule": false,
                      "target-action": "overwrite",
                      "syncType": %d,
                      "watchFolderType": %d,
                      "checkIntegrity": true
                    }
                  }],
                  "schedules": [{
                    "type": "none", "startDateType": "now",
                    "startDate": "%s", "timezone": "Asia/Seoul"
                  }],
                  "step": 1,
                  "isUpcoming": false
                }
                """.formatted(sourceId, targetId, sourcePath, targetPath,
                              SYNC_TYPE, WATCH_FOLDER_TYPE, nowIso);

        String res = call("POST", "/api/automations", body);
        System.out.println("sync automation created: " + jsonString(res, "automationId"));
    }
}
// Node.js 18+ (uses the built-in fetch)
const BASE = process.env.INNORIX_BASE_URL || 'https://app.innorix.com';
const HEADERS = {
  'x-api-key': process.env.INNORIX_API_KEY,
  'Content-Type': 'application/json',
};

// Every setting comes from an environment variable (the value after || is the default).
const TZ = process.env.SCHEDULE_TZ || 'Asia/Seoul';
const SOURCE_ID = process.env.SOURCE_ID;
const SOURCE_PATH = process.env.SOURCE_PATH || 'D:/hotfolder';   // folder to watch
const TARGET_ID = process.env.TARGET_ID;
const TARGET_PATH = process.env.TARGET_PATH || 'E:/mirror';

const SYNC_TYPE = { one_way: 1, two_way: 2 };
const WATCH = { file_created: 1, file_modified: 2, both: 3 };   // both(3) is supported on some server versions only
const SYNC_DIRECTION = process.env.SYNC_DIRECTION || 'one_way';
const SYNC_WATCH = process.env.SYNC_WATCH || 'file_created';

const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, '.000Z');

async function call(method, path, body, params) {
  let url = BASE + path;
  if (params) url += '?' + new URLSearchParams(params).toString();
  const res = await fetch(url, {
    method,
    headers: HEADERS,
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const payload = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`API ${res.status}: ${JSON.stringify(payload)}`);
  return payload.data;
}

const { automationId } = await call('POST', '/api/automations', {
  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
    targetPath: TARGET_PATH,
    step: 1,
    transferOptions: {
      noSchedule: false,
      'target-action': 'overwrite',
      syncType: SYNC_TYPE[SYNC_DIRECTION],
      watchFolderType: WATCH[SYNC_WATCH],
      checkIntegrity: true,
    },
  }],
  // Required by the request format; folder watching is what triggers a run.
  schedules: [{ type: 'none', startDateType: 'now', startDate: nowIso(), timezone: TZ }],
  step: 1,
  isUpcoming: false,
});

console.log('sync automation created:', automationId);
// .NET 8+ (standard library only)
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

static class FolderSync
{
    // Every setting comes from an environment variable (second argument is the default).
    static string Env(string key, string dflt = "") =>
        Environment.GetEnvironmentVariable(key) is { Length: > 0 } v ? v : dflt;

    static readonly string Base = Env("INNORIX_BASE_URL", "https://app.innorix.com");
    static readonly HttpClient Http = new();

    const int OneWay = 1, TwoWay = 2;                     // syncType
    const int OnCreate = 1, OnModify = 2, OnBoth = 3;     // watchFolderType (3: some server versions only)
    static readonly int SyncType = Env("SYNC_DIRECTION", "one_way") == "two_way" ? TwoWay : OneWay;
    static readonly int WatchFolderType = Env("SYNC_WATCH", "file_created") switch
    {
        "file_modified" => OnModify,
        "both" => OnBoth,
        _ => OnCreate,
    };

    static async Task<JsonElement> Call(string method, string path, object? body = null)
    {
        var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
        req.Headers.Add("x-api-key", Env("INNORIX_API_KEY"));
        if (body != null)
            req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");

        var res = await Http.SendAsync(req);
        var text = await res.Content.ReadAsStringAsync();
        if (!res.IsSuccessStatusCode) throw new Exception(
quot;API {(int)res.StatusCode}: {text}"
); return JsonDocument.Parse(text).RootElement.GetProperty("data"); } static async Task Main() { var tz = Env("SCHEDULE_TZ", "Asia/Seoul"); var nowIso = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.000Z"); var transferOptions = new Dictionary<string, object> { ["noSchedule"] = false, ["target-action"] = "overwrite", ["syncType"] = SyncType, ["watchFolderType"] = WatchFolderType, ["checkIntegrity"] = true, }; var data = await Call("POST", "/api/automations", new { name = "hot-folder-sync", flowName = "hot-folder-sync", transferType = "sync", timezone = tz, details = new[] { new { senderId = Env("SOURCE_ID"), receiverId = Env("TARGET_ID"), sourceItem = new[] { new { filePath = Env("SOURCE_PATH", "D:/hotfolder"), isDir = true } }, targetPath = Env("TARGET_PATH", "E:/mirror"), step = 1, transferOptions, } }, schedules = new[] { new { type = "none", startDateType = "now", startDate = nowIso, timezone = tz } }, step = 1, isUpcoming = false, }); Console.WriteLine("sync automation created: " + data.GetProperty("automationId").GetString()); } }

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)
// Reuses call(), jsonString() and jsonInt() from the example above.
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

static final Set<Integer> TERMINAL = Set.of(2, 4, 5, 9, 99);

/** Collects every monitorId in the list response.
 *  Summary rows (type=automation/history/flow) carry no monitorId, so they drop out. */
static List<String> monitorIds(String automationId) throws Exception {
    String json = call("GET", "/api/transfers?automationId=" + automationId, null);
    List<String> ids = new ArrayList<>();
    Matcher m = Pattern.compile("\"monitorId\"\\s*:\\s*\"([^\"]+)\"").matcher(json);
    while (m.find()) if (!ids.contains(m.group(1))) ids.add(m.group(1));
    return ids;
}

/** Drop a file into the watched folder, then follow the transfers it triggers.
 *  status 12=syncing, 2=complete, 4=error */
static void watch(String automationId, int seconds) throws Exception {
    long deadline = System.currentTimeMillis() + seconds * 1000L;
    Set<String> seen = new java.util.HashSet<>();
    while (System.currentTimeMillis() < deadline) {
        for (String monitorId : monitorIds(automationId)) {
            String json = call("GET", "/api/transfers/" + monitorId, null);
            int status = jsonInt(json, "status", -1);
            if (seen.add(monitorId + ":" + status)) {
                System.out.println("  " + monitorId + ": status=" + status
                        + " (" + jsonInt(json, "percent", 0) + "%)");
            }
        }
        Thread.sleep(5000);
    }
}

// Usage
watch(automationId, 60);
const SKIP_ROW_TYPES = new Set(['automation', 'history', 'flow']);
const STATUS = { 2: 'complete', 4: 'error', 6: 'transferring', 12: 'syncing' };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function watch(automationId, seconds = 60, intervalMs = 5000) {
  const deadline = Date.now() + seconds * 1000;
  const seen = new Set();
  while (Date.now() < deadline) {
    const result = await call('GET', '/api/transfers', undefined, { automationId });
    const records = Array.isArray(result) ? result : result?.data || [];
    for (const r of records) {
      if (SKIP_ROW_TYPES.has(r.type)) continue;
      const mid = r.monitorId || r.id;
      const detail = (await call('GET', `/api/transfers/${mid}`)) || {};
      const key = `${mid}:${detail.status}`;
      if (seen.has(key)) continue;
      seen.add(key);
      console.log(`  ${mid}: ${STATUS[detail.status] ?? detail.status} (${detail.percent ?? 0}%)`);
    }
    await sleep(intervalMs);
  }
}

await watch(automationId);
static readonly HashSet<string> SkipRowTypes = new() { "automation", "history", "flow" };

static async Task Watch(string automationId, int seconds = 60, int intervalMs = 5000)
{
    var deadline = DateTime.UtcNow.AddSeconds(seconds);
    var seen = new HashSet<string>();

    while (DateTime.UtcNow < deadline)
    {
        var result = await Call("GET", "/api/transfers?automationId=" + automationId);
        var records = result.ValueKind == JsonValueKind.Object && result.TryGetProperty("data", out var inner)
            ? inner : result;

        foreach (var r in records.EnumerateArray())
        {
            var type = r.TryGetProperty("type", out var t) ? t.GetString() : null;
            if (type != null && SkipRowTypes.Contains(type)) continue;

            var mid = r.TryGetProperty("monitorId", out var m) ? m.GetString()
                    : r.TryGetProperty("id", out var i) ? i.GetString() : null;
            if (mid == null) continue;

            var detail = await Call("GET", "/api/transfers/" + mid);
            var status = detail.GetProperty("status").GetInt32();   // 12=syncing, 2=complete
            if (seen.Add(
quot;{mid}:{status}"
)) Console.WriteLine(
quot; {mid}: status={status}"
); } await Task.Delay(intervalMs); } }

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.
PreviousCollect Files from ManyNextWeb Upload & Download

On this page

  • Getting started
  • Prerequisites
  • Quick start
  • Create a sync
  • Sync settings
  • Create a sync
  • Verify operation
  • Transfer options
  • File options
  • After-transfer actions
  • Reference
  • Builder UI ↔ .env ↔ API mapping
  • Common errors