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. Collect Files from Many

Collect Files from Many

Build a many-to-one file collection workflow with the INNORIX Transfer Builder. Configure multiple sources, one target, 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

Collect Files from Many collects files from multiple devices into a single location. Typical use cases include collecting end-of-day data from all branches at headquarters or gathering logs from each production line on an analysis server.

The API request is a single POST /api/automations call. For each item in details[], keep receiverId and targetPath the same, and vary only senderId and sourceItem, and files from multiple locations will be collected into one place.

Collection is often used with Repeat start conditions (daily or weekly), and because collected file names can easily overlap, the Save Path (savepath · optionPath) and Duplicated Name (target-action) settings are especially important.

Getting started#

Requirements#

  1. API Key — Generate it from the Developer menu under the profile menu at the bottom left of the product. The Workspace ID is also displayed on the same screen. To generate it through the API, use POST /api/auth/api-keys (Bearer access token, no request body) → data.apiKey.
  2. deviceId — N sending devices and 1 receiving device. In Devices, select a device and use the ID displayed at the top right.
  3. Paths — The source path (sourceItem[].filePath) and destination path (targetPath). Use absolute paths separated by slashes (/) for both. 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 the workspace. This header is not an authentication method; it identifies the target workspace.

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

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

Quick start#

Use the following steps to run the bundle exactly as downloaded through Get API Code in the builder.

  1. In the Transfer Builder, choose the options, then select Get API Code → choose a language → download the zip file
  2. Extract the archive, open .env, and fill in INNORIX_API_KEY, SOURCE_IDS, TARGET_ID, and the paths (SOURCE_PATHS · 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 downloaded bundle example. For readability, the Java excerpt in this document uses text blocks (""") and therefore requires JDK 17+. The bundle's ComboBuilder.java runs on JDK 11+.

ℹ️ The bundle's combo_builder.* reads the .env file 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 as-is, export the values first as shown below.

macOS · Linux

bash
export INNORIX_API_KEY=your-api-key
export SOURCE_IDS=branch-01,branch-02,branch-03
export SOURCE_PATHS=C:/out             # 1 entry = same for all, N = one per source
export TARGET_ID=device-target-01
export TARGET_PATH=D:/collected

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

powershell
$env:INNORIX_API_KEY="your-api-key"
$env:SOURCE_IDS="branch-01,branch-02,branch-03"
$env:SOURCE_PATHS="C:/out"
$env:TARGET_ID="device-target-01"
$env:TARGET_PATH="D:/collected"

Create a transfer#

Create a transfer#

This request collects data from three branch locations into one headquarters server.

json
{
  "name": "branch-collect",
  "flowName": "branch-collect",
  "transferType": "normal",
  "timezone": "Asia/Seoul",
  "details": [
    {
      "senderId": "<branch-01>",
      "receiverId": "<hqDeviceId>",
      "sourceItem": [{ "filePath": "C:/out", "isDir": true }],
      "targetPath": "D:/collected",
      "step": 1,
      "transferOptions": {
        "noSchedule": false,
        "target-action": "numbering",
        "savepath": true,
        "optionPath": 2
      }
    },
    {
      "senderId": "<branch-02>",
      "receiverId": "<hqDeviceId>",
      "sourceItem": [{ "filePath": "C:/out", "isDir": true }],
      "targetPath": "D:/collected",
      "step": 1,
      "transferOptions": {
        "noSchedule": false,
        "target-action": "numbering",
        "savepath": true,
        "optionPath": 2
      }
    },
    {
      "senderId": "<branch-03>",
      "receiverId": "<hqDeviceId>",
      "sourceItem": [{ "filePath": "C:/out", "isDir": true }],
      "targetPath": "D:/collected",
      "step": 1,
      "transferOptions": {
        "noSchedule": false,
        "target-action": "numbering",
        "savepath": true,
        "optionPath": 2
      }
    }
  ],
  "schedules": [
    { "type": "day", "hour": "02", "minute": "00", "ampm": "am",
      "startDateType": "specific", "startDate": "2026-09-15T02:00:00", "timezone": "Asia/Seoul" }
  ],
  "step": 1,
  "isUpcoming": false
}
  • savepath controls whether the original folder structure is preserved, while optionPath defines the subfolder rule to create beneath it. 1 = date (YYMMDD), 2 = device name, 3 = custom folder (put the folder name in savepath). optionPath does not work on its own and must be sent together with savepath.
  • transferOptions.target-action defines what happens when file names conflict. Use one of overwrite (overwrite), numbering (append a number to the name), or nosend (skip).
  • startDate is an example value. Because this request repeats every day at 02:00 (Asia/Seoul), specify the actual date and time when the recurrence should begin.

In the request above, optionPath: 2 creates a device-name subfolder. Files are separated by branch into folders like D:/collected/branch-01/, D:/collected/branch-02/, and D:/collected/branch-03/, which eliminates file-name conflicts.

# 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_IDS   = [x.strip() for x in os.getenv("SOURCE_IDS", "branch-01,branch-02,branch-03").split(",") if x.strip()]
SOURCE_PATHS = [x.strip() for x in os.getenv("SOURCE_PATHS", "C:/out").split(",") if x.strip()]
TARGET_ID    = os.environ["TARGET_ID"]
TARGET_PATH  = os.getenv("TARGET_PATH", "D:/collected")
# SOURCE_PATHS: 1 entry = same for every device, N = one per SOURCE_IDS entry

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")

def expand(paths, count):
    if len(paths) == 1:
        return paths * count
    if len(paths) == count:
        return paths
    raise ValueError(f"SOURCE_PATHS must have 1 entry or exactly {count}")

# Collecting hits name conflicts often - a device-name subfolder (optionPath=2) plus rename is recommended.
options = {
    "noSchedule": False,
    "target-action": "numbering",      # Skip=nosend, Overwrite=overwrite
    "savepath": True,
    "optionPath": 2,                   # 1=date (YYMMDD), 2=device name, 3=custom
}

paths = expand(SOURCE_PATHS, len(SOURCE_IDS))
details = [{
    "senderId": source_id,
    "receiverId": TARGET_ID,
    "sourceItem": [{"filePath": paths[i], "isDir": True}],
    "targetPath": TARGET_PATH,
    "step": 1,
    "transferOptions": options,
} for i, source_id in enumerate(SOURCE_IDS)]

body = {
    "name": "branch-collect",
    "flowName": "branch-collect",
    "transferType": "normal",
    "timezone": TZ,
    "details": details,
    # Repeats daily at 02:00 - switch this to a "now" schedule to run immediately.
    "schedules": [{
        "type": "day", "hour": "02", "minute": "00", "ampm": "am",
        "startDateType": "specific", "startDate": "2026-09-15T02:00:00", "timezone": TZ,
    }],
    "step": 1,
    "isUpcoming": False,
}

automation_id = call("POST", "/api/automations", body)["automationId"]
print(f"automation created: {automation_id}  ({len(details)} sources)")
// 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.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class CollectFromMany {

    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).
    static final List<String> SOURCE_IDS = envList("SOURCE_IDS", "branch-01,branch-02,branch-03");
    static final List<String> SOURCE_PATHS = envList("SOURCE_PATHS", "C:/out");  // 1 = same for all, N = one each
    static final String TARGET_PATH = env("TARGET_PATH", "D:/collected");

    /** 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;
    }

    /** Splits a comma-separated environment variable into a list. */
    static List<String> envList(String key, String dflt) {
        return Arrays.stream(env(key, dflt).split(","))
                .map(String::trim).filter(x -> !x.isEmpty()).toList();
    }

    /** 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();
    }

    static List<String> expand(List<String> paths, int count) {
        if (paths.size() == 1) return IntStream.range(0, count).mapToObj(i -> paths.get(0)).toList();
        if (paths.size() == count) return paths;
        throw new IllegalArgumentException("SOURCE_PATHS must have 1 entry or exactly " + count);
    }

    public static void main(String[] args) throws Exception {
        String targetId = env("TARGET_ID", "");
        List<String> paths = expand(SOURCE_PATHS, SOURCE_IDS.size());

        // Collecting hits name conflicts often - device-name subfolder (optionPath=2) plus rename (numbering)
        String options = """
                { "noSchedule": false, "target-action": "numbering", "savepath": true, "optionPath": 2 }
                """;

        String details = IntStream.range(0, SOURCE_IDS.size())
                .mapToObj(i -> """
                        {
                          "senderId": "%s",
                          "receiverId": "%s",
                          "sourceItem": [{ "filePath": "%s", "isDir": true }],
                          "targetPath": "%s",
                          "step": 1,
                          "transferOptions": %s
                        }
                        """.formatted(SOURCE_IDS.get(i), targetId, paths.get(i), TARGET_PATH, options))
                .collect(Collectors.joining(","));

        String body = """
                {
                  "name": "branch-collect",
                  "flowName": "branch-collect",
                  "transferType": "normal",
                  "timezone": "Asia/Seoul",
                  "details": [%s],
                  "schedules": [{
                    "type": "day", "hour": "02", "minute": "00", "ampm": "am",
                    "startDateType": "specific", "startDate": "2026-09-15T02:00:00",
                    "timezone": "Asia/Seoul"
                  }],
                  "step": 1,
                  "isUpcoming": false
                }
                """.formatted(details);

        String res = call("POST", "/api/automations", body);
        System.out.println("automation created: " + jsonString(res, "automationId")
                + " (" + SOURCE_IDS.size() + " sources)");
    }
}
// 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 (second argument is the default).
const envList = (key, dflt) =>
  (process.env[key] || dflt).split(',').map((x) => x.trim()).filter(Boolean);

const TZ = process.env.SCHEDULE_TZ || 'Asia/Seoul';
const SOURCE_IDS = envList('SOURCE_IDS', 'branch-01,branch-02,branch-03');
const SOURCE_PATHS = envList('SOURCE_PATHS', 'C:/out');   // 1 = same for all, N = one each
const TARGET_ID = process.env.TARGET_ID;
const TARGET_PATH = process.env.TARGET_PATH || 'D:/collected';

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;
}

function expand(paths, count) {
  if (paths.length === 1) return Array(count).fill(paths[0]);
  if (paths.length === count) return paths;
  throw new Error(`SOURCE_PATHS must have 1 entry or exactly ${count}`);
}

// Collecting hits name conflicts often - device-name subfolder (optionPath=2) plus rename (numbering)
const options = {
  noSchedule: false,
  'target-action': 'numbering',
  savepath: true,
  optionPath: 2,
};

const paths = expand(SOURCE_PATHS, SOURCE_IDS.length);
const details = SOURCE_IDS.map((senderId, i) => ({
  senderId,
  receiverId: TARGET_ID,
  sourceItem: [{ filePath: paths[i], isDir: true }],
  targetPath: TARGET_PATH,
  step: 1,
  transferOptions: options,
}));

const { automationId } = await call('POST', '/api/automations', {
  name: 'branch-collect',
  flowName: 'branch-collect',
  transferType: 'normal',
  timezone: TZ,
  details,
  schedules: [{
    type: 'day', hour: '02', minute: '00', ampm: 'am',
    startDateType: 'specific', startDate: '2026-09-15T02:00:00', timezone: TZ,
  }],
  step: 1,
  isUpcoming: false,
});

console.log(`automation created: ${automationId} (${details.length} sources)`);
// .NET 8+ (standard library only)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

static class CollectFromMany
{
    // 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 string[] EnvList(string key, string dflt) =>
        Env(key, dflt).Split(',').Select(x => x.Trim()).Where(x => x.Length > 0).ToArray();

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

    static readonly string[] SourceIds = EnvList("SOURCE_IDS", "branch-01,branch-02,branch-03");
    static readonly string[] SourcePaths = EnvList("SOURCE_PATHS", "C:/out");  // 1 = same for all, N = one each
    static readonly string TargetPath = Env("TARGET_PATH", "D:/collected");

    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 string[] Expand(string[] paths, int count) { if (paths.Length == 1) return Enumerable.Repeat(paths[0], count).ToArray(); if (paths.Length == count) return paths; throw new ArgumentException(
quot;SourcePaths must have 1 entry or exactly {count}"
); } static async Task Main() { var tz = Env("SCHEDULE_TZ", "Asia/Seoul"); var targetId = Env("TARGET_ID"); var paths = Expand(SourcePaths, SourceIds.Length); // Collecting hits name conflicts often - device-name subfolder (optionPath=2) plus rename (numbering) var options = new Dictionary<string, object> { ["noSchedule"] = false, ["target-action"] = "numbering", ["savepath"] = true, ["optionPath"] = 2, }; var details = SourceIds.Select((senderId, i) => new { senderId, receiverId = targetId, sourceItem = new[] { new { filePath = paths[i], isDir = true } }, targetPath = TargetPath, step = 1, transferOptions = options, }).ToArray(); var data = await Call("POST", "/api/automations", new { name = "branch-collect", flowName = "branch-collect", transferType = "normal", timezone = tz, details, schedules = new[] { new { type = "day", hour = "02", minute = "00", ampm = "am", startDateType = "specific", startDate = "2026-09-15T02:00:00", timezone = tz, } }, step = 1, isUpcoming = false, }); Console.WriteLine(
quot;automation created: {data.GetProperty("automationId").GetString()} ({details.Length} sources)"
); } }

Check progress#

If there are N collection sources, there will also be N transfers.

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 repeating (Repeat) automation creates a new transfer each time it runs, so the list may be empty depending on when you query it. Check the automation execution log for results from previous runs.

SKIP_ROW_TYPES = {"automation", "history", "flow"}
TERMINAL = {2, 4, 5, 9, 99}
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 active_transfers(automation_id):
    result = call("GET", "/api/transfers", params={"automationId": automation_id})
    records = result.get("data") if isinstance(result, dict) else result
    return [r for r in (records or []) if r.get("type") not in SKIP_ROW_TYPES]

for row in active_transfers(automation_id):
    mid = row.get("monitorId") or row.get("id")
    detail = call("GET", f"/api/transfers/{mid}") or {}
    status = detail.get("status")
    print(f"  {mid}: {STATUS.get(status, status)} ({detail.get('percent', 0)}%)")
// 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;
}

/** Polls every 3 seconds until the transfer reaches a terminal status. */
static int waitFor(String monitorId, int timeoutSeconds) throws Exception {
    long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;
    while (System.currentTimeMillis() < deadline) {
        String json = call("GET", "/api/transfers/" + monitorId, null);
        int status = jsonInt(json, "status", -1);
        System.out.println("  " + monitorId + ": status=" + status
                + " (" + jsonInt(json, "percent", 0) + "%)");
        if (TERMINAL.contains(status)) return status;
        Thread.sleep(3000);
    }
    throw new RuntimeException(monitorId + " did not finish within " + timeoutSeconds + "s");
}

// Usage - a repeating automation shows a transfer only while it is running.
for (String monitorId : monitorIds(automationId)) waitFor(monitorId, 3600);
const SKIP_ROW_TYPES = new Set(['automation', 'history', 'flow']);
const TERMINAL = new Set([2, 4, 5, 9, 99]);

async function activeTransfers(automationId) {
  const result = await call('GET', '/api/transfers', undefined, { automationId });
  const records = Array.isArray(result) ? result : result?.data || [];
  return records.filter((r) => !SKIP_ROW_TYPES.has(r.type));
}

for (const row of await activeTransfers(automationId)) {
  const mid = row.monitorId || row.id;
  const detail = (await call('GET', `/api/transfers/${mid}`)) || {};
  console.log(`  ${mid}: status=${detail.status} (${detail.percent ?? 0}%)`);
}
static readonly HashSet<int> Terminal = new() { 2, 4, 5, 9, 99 };
static readonly HashSet<string> SkipRowTypes = new() { "automation", "history", "flow" };

static async Task PrintActive(string automationId)
{
    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);
        Console.WriteLine(
quot; {mid}: status={detail.GetProperty("status").GetInt32()}"
); } }

Transfer options#

Start time and recurrence#

The execution timing is controlled by a single schedules[0] object. Leave details unchanged and modify only this object.

Execution timing schedules[0]
Run now { "type": "none", "startDateType": "now", "startDate": "<current ISO>", "timezone": "Asia/Seoul" }
Run once at a specified time { "type": "none", "startDateType": "specific", "startDate": "2026-09-20T01:00:00", "timezone": "Asia/Seoul" }
Repeat on a schedule See the recurrence table below
After the previous automation finishes { "type": "none", "startDateType": "now", "triggerAutomation": { "value": "<previous automationId>" }, ... } — add flowId to the body
Triggered by an external request { "type": "none", "startDateType": "now", ... } + body transferType: "command"

To start through an external request, two preliminary calls are required. POST /api/command/generate-code → data.code, GET /api/command/generate-api-key → data.apiKey. Put those two values into code and apiKey in the automation body and create the automation, then call POST https://app.innorix.com/command/<code> with the x-api-key: <apiKey> header to start the collection.

Collection typically repeats at a fixed time. In that case, configure schedules[0] as follows.

Recurrence schedules[0]
Every hour { "type": "hour", "startDateType": "specific", "startDate": "...", "timezone": "Asia/Seoul" }
Every day at 02:00 { "type": "day", "hour": "02", "minute": "00", "ampm": "am", ... }
Every Monday { "type": "week", "dayInWeek": ["monday"], "hour": "02", "minute": "00", "ampm": "am", ... }
On the 1st of every month { "type": "month", "dayInMonth": ["1"], "hour": "02", "minute": "00", "ampm": "am", ... }
  • hour uses 1–12, and ampm uses am / pm. For type: "hour", the hour value is ignored.
  • dayInWeek and dayInMonth are arrays, so you can provide multiple values like ["monday","wednesday"] or ["1","15"].
  • 0 in dayInMonth means the last day of the month.
  • With startDateType: "now", it runs once immediately when created and then follows the recurrence schedule. With "specific", it starts from the first execution time specified in startDate.

File-name conflicts#

The most common collection issue is multiple branches sending files with the same name (daily.csv). There are three options.

Method Setting Result
Separate by device-name folder (recommended) savepath: true, optionPath: 2 D:/collected/branch-01/daily.csv
Separate by date folder savepath: true, optionPath: 1 D:/collected/260915/daily.csv
Rename within the same folder target-action: "numbering" daily.csv, daily (1).csv …

The target-action values are overwrite (overwrite), numbering (append a number to the name), and nosend (skip).

To preserve only the original folder structure without creating a subfolder, set only savepath: true and omit optionPath.

File options#

Place file-processing options inside details[].transferOptions.

Option Key Value
Extension filter send-fileoption.extension { "extension": ["pdf","mp4"], "allow": true } — use allow: false for a block list
Size filter send-fileoption.fileSize { "size": <bytes>, "over": true, "equal": true } — use over: true for a lower bound or over: false for an upper bound (only one can be specified)
Name filter send-fileoption.fileName { "name": "temp", "allow": false } — exclude files whose names contain the value
Preserve folder structure savepath true
Date subfolder savepath + optionPath true + 1
Device-name subfolder savepath + optionPath true + 2
Custom subfolder savepath + optionPath "<folder name>" + 3
Duplicate name — overwrite target-action "overwrite"
Duplicate name — append a number target-action "numbering"
Duplicate name — skip target-action "nosend"
Integrity verification checkIntegrity true
json
{
  "noSchedule": false,
  "target-action": "numbering",
  "checkIntegrity": true,
  "savepath": true,
  "optionPath": 1,
  "send-fileoption": {
    "extension": { "extension": ["pdf", "xlsx"], "allow": true },
    "fileSize": { "size": 1048576, "over": true, "equal": true },
    "fileName": { "name": "tmp", "allow": false }
  }
}

ℹ️ optionPath (date · device name · custom subfolder) applies only to automations. Because every transfer in this document is created with POST /api/automations, it still applies when using Start → Now.

Actions after transfer#

Actions that run 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, not globally to the workspace. Available events are started · completed · paused · recovered · deviceConnected · deviceDisconnected.

② Workspace-level 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, and Email → email. You can check provider-specific required settings 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 = Collect Files from Many TRANSFER_TYPE=collect N details entries
From device list SOURCE_IDS (comma-separated) details[].senderId
From path SOURCE_PATHS (1 entry or N entries) details[].sourceItem[].filePath
To device TARGET_ID all details[].receiverId values (shared)
To path TARGET_PATH all details[].targetPath values (shared)
Start START_WHEN · REPEAT_* schedules[0]
Save Path SAVE_PATH savepath · optionPath
Duplicated Name DUPLICATE_ACTION target-action
After transfer ON_* processors[] · POST /api/integrations

If SOURCE_IDS is empty, the example uses a single SOURCE_ID as a one-item list instead.

Common errors#

Symptom Cause and solution
Files overwrite one another The default target-action is overwrite. Change it to numbering or separate files with optionPath: 2.
Subfolders are not created optionPath does not work on its own. You must also send savepath: true.
Only some branches are collected The relevant agent is offline, or sourceItem[].filePath does not exist on that device.
A repeating job runs only once If you send isUpcoming: true, the server converts it into a one-time scheduled job. Keep it false.
The first run starts immediately startDateType: "now" runs once immediately when created. If you do not want that, use "specific" and provide the first execution time.
PreviousSend Files to ManyNextSync Files Across Systems

On this page

  • Getting started
  • Requirements
  • Quick start
  • Create a transfer
  • Create a transfer
  • Check progress
  • Transfer options
  • Start time and recurrence
  • File-name conflicts
  • File options
  • Actions after transfer
  • Reference
  • Builder UI ↔ .env ↔ API mapping
  • Common errors