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
- 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 — 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.
- Paths — The source path (
sourceItem[].filePath) and destination path (targetPath). Both must be absolute paths separated by slashes (/).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, and the paths (SOURCE_PATH·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=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)
$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.
{
"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-actioncontrols what happens when names conflict. Use one ofoverwrite(overwrite) ·numbering(append a number to the name) ·nosend(skip).startDateis 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.
GET /api/transfers?automationId=<automationId>— List of transfers in progress. The response uses cursor pagination (data.data[]), and actual transfer rows havetype: "monitor". Rows withtypevalues ofautomation·history·floware summary rows, so skip them.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.
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.
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 |
{
"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 withPOST /api/automations, so it still applies when usingStart → Now.
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
This maps the .env keys in the Get API Code bundle to their corresponding API fields.
| Builder UI | .env | API |
|---|---|---|
| Tab = Send 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.