Send to one — Send from one device to one device

Send 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
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=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 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.

CodeMeaningCodeMeaning
-1queued6transferring
0waiting7skipped
1started8retry
2complete9partial-complete
3paused11virus-scanning
4error12syncing
5cancelled99fail

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 timeschedules[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.

OptionKeyValue
Extension filtersend-fileoption.extension{ "extension": ["pdf","mp4"], "allow": true } — use allow: false for a blocklist
Size filtersend-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 filtersend-fileoption.fileName{ "name": "temp", "allow": false } — exclude if the name contains this value
Preserve folder structuresavepathtrue
Date subfoldersavepath + optionPathtrue + 1
Device-name subfoldersavepath + optionPathtrue + 2
Custom subfoldersavepath + optionPath"<folder name>" + 3
Duplicate name — overwritetarget-action"overwrite"
Duplicate name — append numbertarget-action"numbering"
Duplicate name — skiptarget-action"nosend"
Integrity verificationcheckIntegritytrue
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 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

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

Builder UI.envAPI
Tab = Send to oneTRANSFER_TYPE=send_one1 details item
From deviceSOURCE_IDdetails[].senderId
From pathSOURCE_PATHdetails[].sourceItem[].filePath
Folder/file flagSOURCE_IS_DIRdetails[].sourceItem[].isDir
To deviceTARGET_IDdetails[].receiverId
To pathTARGET_PATHdetails[].targetPath
StartSTART_WHENschedules[0]
Transfer nameNAMEname · flowName
File optionsFILTER_* · SAVE_PATH · DUPLICATE_ACTION · INTEGRITYdetails[].transferOptions
After transferON_*processors[] · POST /api/integrations

Common errors

SymptomCause and solution
401 Unauthorizedx-api-key is empty or expired. Generate a new one from the Developer screen. Bearer tokens are short-lived.
400 Bad RequestMost often, targetPath is empty or /. Also check senderId · receiverId for deviceId typos.
Automation was created but no transfer appearsThe agent may be offline. Query GET /api/transfers?automationId=... again every few seconds.
Progress is stuckThis occurs when the receiving agent loses its connection. Check the device status first.
Everything transfers despite a filtersend-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.