Send Files to One

Build a one-to-one file transfer with the INNORIX Transfer Builder. Configure one source, one target, and generate the transfer setup.

Send Files to One transfers files or folders from one sending device (Source) to one receiving device (Target), making it the most basic transfer configuration.

The API requires a single POST /api/automations request. Add one sender/receiver pair to details, and use schedules to set when it runs.

When you click Get API Code in the Transfer Builder, the same configuration is downloaded as runnable examples for each language (combo_builder.*) together with an .env file. This document extracts only the key parts of those examples.

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 — Select a device under Devices in the product; its ID is displayed at the top right. You can specify both agent-installed devices running Windows, macOS, Ubuntu, RHEL, Rocky, Debian, etc., and object storage such as Amazon S3, Azure Blob, Google Cloud Storage.
  3. Paths — The source path (sourceItem[].filePath) and destination path (targetPath). Both must be absolute paths separated by slashes (/). 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, and the paths (SOURCE_PATH, 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=C:/data/out
export TARGET_ID=device-target-01
export TARGET_PATH=C:/incoming

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="C:/data/out"
$env:TARGET_ID="device-target-01"
$env:TARGET_PATH="C:/incoming"

Create a transfer#

Create a transfer#

Send Files to One is created with a single POST /api/automations request. Add one sender / receiver pair to the details array, and set the execution time in schedules. The example below runs immediately.

json
{
  "name": "nightly-export",
  "flowName": "nightly-export",
  "transferType": "normal",
  "timezone": "Asia/Seoul",
  "details": [
    {
      "senderId": "<sourceDeviceId>",
      "receiverId": "<targetDeviceId>",
      "sourceItem": [{ "filePath": "C:/data/out", "isDir": true }],
      "targetPath": "C:/incoming",
      "step": 1,
      "transferOptions": { "noSchedule": false, "target-action": "overwrite" }
    }
  ],
  "schedules": [
    { "type": "none", "startDateType": "now", "startDate": "2026-09-14T02:00:00.000Z", "timezone": "Asia/Seoul" }
  ],
  "step": 1,
  "isUpcoming": false
}
  • 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. When running with Now, use the current UTC time at the time of the request (the example code below calculates the current time each time it runs).

The response's data.automationId is the identifier used for subsequent queries.

# 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).
SOURCE_ID     = os.environ["SOURCE_ID"]
SOURCE_PATH   = os.getenv("SOURCE_PATH", "C:/data/out")
TARGET_ID     = os.environ["TARGET_ID"]
TARGET_PATH   = os.getenv("TARGET_PATH", "C:/incoming")
SOURCE_IS_DIR = os.getenv("SOURCE_IS_DIR", "true").lower() != "false"
TZ            = os.getenv("SCHEDULE_TZ", "Asia/Seoul")

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": "nightly-export",
    "flowName": "nightly-export",
    "transferType": "normal",          # "sync" for sync, "command" for an external trigger
    "timezone": TZ,
    "details": [{
        "senderId": SOURCE_ID,
        "receiverId": TARGET_ID,
        "sourceItem": [{"filePath": SOURCE_PATH, "isDir": SOURCE_IS_DIR}],   # false for a single file
        "targetPath": TARGET_PATH,
        "step": 1,
        "transferOptions": {"noSchedule": False, "target-action": "overwrite"},
    }],
    "schedules": [{
        "type": "none", "startDateType": "now",
        "startDate": now_iso(), "timezone": TZ,
    }],
    "step": 1,
    "isUpcoming": False,
}

automation_id = call("POST", "/api/automations", body)["automationId"]
print("automation created:", automation_id)

Check progress#

After the automation is created, the actual transfer is tracked using a separate monitorId.

  1. GET /api/transfers?automationId=<automationId> — List of transfers in progress. The response uses cursor pagination (data.data[]), and actual transfer rows have type: "monitor". Rows with type values of automation, history, flow are summary rows, so skip them.
  2. GET /api/transfers/<monitorId> — Status and progress.

Status codes are as follows.

Code Meaning Code Meaning
-1 queued 6 transferring
0 waiting 7 skipped
1 started 8 retry
2 complete 9 partial-complete
3 paused 11 virus-scanning
4 error 12 syncing
5 cancelled 99 fail

Terminal statuses are 2, 4, 5, 9, 99.

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 monitor_ids(automation_id):
    result = call("GET", "/api/transfers", params={"automationId": automation_id})
    records = result.get("data") if isinstance(result, dict) else result
    return [r.get("monitorId") or r.get("id")
            for r in (records or []) if r.get("type") not in SKIP_ROW_TYPES]

def wait_for(monitor_id, timeout=3600):
    deadline = time.time() + timeout
    while time.time() < deadline:
        detail = call("GET", f"/api/transfers/{monitor_id}") or {}
        status = detail.get("status")
        print(f"  {STATUS.get(status, status)} ({detail.get('percent', 0)}%)")
        if detail.get("isTerminal", status in TERMINAL):
            return detail
        time.sleep(3)
    raise TimeoutError(f"{monitor_id} did not finish within {timeout}s")

for mid in monitor_ids(automation_id):
    wait_for(mid)

Transfer options#

Start time#

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

Start time schedules[0] Notes
Now { type: "none", startDateType: "now", startDate: <current ISO> } Runs immediately after creation
Once at a specified time { type: "none", startDateType: "specific", startDate: "2026-09-20T01:00:00" }
Repeat hourly { type: "hour", ... } At the start of every hour
Repeat daily { type: "day", hour, minute, ampm }
Repeat weekly { type: "week", dayInWeek: ["monday"], hour, minute, ampm }
Repeat monthly { type: "month", dayInMonth: ["1"], hour, minute, ampm } 0 means the last day of the month
After the previous automation finishes { type: "none", startDateType: "now", triggerAutomation: { value: "<previous automationId>" } } Add flowId to the body
By external request { type: "none", startDateType: "now" } + body transferType: "command" See below

For repeat, startDateType / startDate determines the first execution time. With startDateType: "now", it runs once immediately after creation and then follows the recurrence. With "specific", it starts from the next calculated recurrence. hour is 1–12, ampm is am / pm, and timezone uses an IANA name like Asia/Seoul.

External request requires two preliminary calls.

http
POST /api/command/generate-code       → data.code
GET  /api/command/generate-api-key    → data.apiKey

Create the automation with these two values in the body as code, apiKey; each call to the following address then starts the transfer.

http
POST https://app.innorix.com/command/<code>
x-api-key: <apiKey>

File options#

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

Option Key Value
Extension filter send-fileoption.extension { "extension": ["pdf","mp4"], "allow": true } — use allow: false for a blocklist
Size filter send-fileoption.fileSize { "size": <bytes>, "over": true, "equal": true } — use over: true for a lower bound and over: false for an upper bound (only one can be specified)
Name filter send-fileoption.fileName { "name": "temp", "allow": false } — exclude if the name contains this 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 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. All transfers in this document are created with POST /api/automations, so it still applies when using Start → Now.

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#

This maps the .env keys in the Get API Code bundle to their corresponding API fields.

Builder UI .env API
Tab = Send Files to One TRANSFER_TYPE=send_one 1 details item
From device SOURCE_ID details[].senderId
From path SOURCE_PATH details[].sourceItem[].filePath
Folder/file flag SOURCE_IS_DIR details[].sourceItem[].isDir
To device TARGET_ID details[].receiverId
To path TARGET_PATH details[].targetPath
Start START_WHEN schedules[0]
Transfer name NAME name, flowName
File options FILTER_*, SAVE_PATH, DUPLICATE_ACTION, INTEGRITY details[].transferOptions
After transfer ON_* processors[], POST /api/integrations

Common errors#

Symptom Cause and solution
401 Unauthorized x-api-key is empty or expired. Generate a new one from the Developer screen. Bearer tokens are short-lived.
400 Bad Request Most often, targetPath is empty or /. Also check senderId, receiverId for deviceId typos.
Automation was created but no transfer appears The agent may be offline. Query GET /api/transfers?automationId=... again every few seconds.
Progress is stuck This occurs when the receiving agent loses its connection. Check the device status first.
Everything transfers despite a filter send-fileoption.extension uses the nested structure { "extension": [...], "allow": ... }. Providing only the array is ignored.

To view the raw request and response, set DEBUG=true in the downloaded example.