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#
- 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 loginAdd 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> # optionalThe 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:/incomingWindows 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 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.
{
"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)// 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 SendToOne {
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();
/** 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 {
// Every setting comes from an environment variable (second argument is the default).
String sourceId = env("SOURCE_ID", "");
String sourcePath = env("SOURCE_PATH", "C:/data/out");
String targetId = env("TARGET_ID", "");
String targetPath = env("TARGET_PATH", "C:/incoming");
String tz = env("SCHEDULE_TZ", "Asia/Seoul");
String nowIso = Instant.now().truncatedTo(ChronoUnit.SECONDS).toString().replace("Z", ".000Z");
String body = """
{
"name": "nightly-export",
"flowName": "nightly-export",
"transferType": "normal",
"timezone": "%s",
"details": [{
"senderId": "%s",
"receiverId": "%s",
"sourceItem": [{ "filePath": "%s", "isDir": true }],
"targetPath": "%s",
"step": 1,
"transferOptions": { "noSchedule": false, "target-action": "overwrite" }
}],
"schedules": [{
"type": "none", "startDateType": "now",
"startDate": "%s", "timezone": "%s"
}],
"step": 1,
"isUpcoming": false
}
""".formatted(tz, sourceId, targetId, sourcePath, targetPath, nowIso, tz);
String res = call("POST", "/api/automations", body);
System.out.println("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 || 'C:/data/out';
const TARGET_ID = process.env.TARGET_ID;
const TARGET_PATH = process.env.TARGET_PATH || 'C:/incoming';
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 body = {
name: 'nightly-export',
flowName: 'nightly-export',
transferType: 'normal',
timezone: TZ,
details: [{
senderId: SOURCE_ID,
receiverId: TARGET_ID,
sourceItem: [{ filePath: SOURCE_PATH, isDir: true }],
targetPath: TARGET_PATH,
step: 1,
transferOptions: { noSchedule: false, 'target-action': 'overwrite' },
}],
schedules: [{ type: 'none', startDateType: 'now', startDate: nowIso(), timezone: TZ }],
step: 1,
isUpcoming: false,
};
const { automationId } = await call('POST', '/api/automations', body);
console.log('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 SendToOne
{
// 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();
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");
// Keys that contain a hyphen, like "target-action", need a Dictionary.
var transferOptions = new Dictionary<string, object>
{
["noSchedule"] = false,
["target-action"] = "overwrite",
};
var body = new
{
name = "nightly-export",
flowName = "nightly-export",
transferType = "normal",
timezone = tz,
details = new[]
{
new
{
senderId = Env("SOURCE_ID"),
receiverId = Env("TARGET_ID"),
sourceItem = new[] { new { filePath = Env("SOURCE_PATH", "C:/data/out"), isDir = true } },
targetPath = Env("TARGET_PATH", "C:/incoming"),
step = 1,
transferOptions,
}
},
schedules = new[]
{
new { type = "none", startDateType = "now", startDate = nowIso, timezone = tz }
},
step = 1,
isUpcoming = false,
};
var data = await Call("POST", "/api/automations", body);
Console.WriteLine("automation created: " + data.GetProperty("automationId").GetString());
}
}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)// Reuses call(), jsonString() and jsonInt() from SendToOne 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
for (String monitorId : monitorIds(automationId)) {
int status = waitFor(monitorId, 3600);
System.out.println(" finished: " + (status == 2 ? "complete" : "status " + status));
}const SKIP_ROW_TYPES = new Set(['automation', 'history', 'flow']);
const TERMINAL = new Set([2, 4, 5, 9, 99]);
const 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',
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function monitorIds(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))
.map((r) => r.monitorId || r.id)
.filter(Boolean);
}
async function waitFor(monitorId, timeoutMs = 3600_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const detail = (await call('GET', `/api/transfers/${monitorId}`)) || {};
console.log(` ${STATUS[detail.status] ?? detail.status} (${detail.percent ?? 0}%)`);
if (detail.isTerminal ?? TERMINAL.has(detail.status)) return detail;
await sleep(3000);
}
throw new Error(`${monitorId} did not finish within the time limit`);
}
for (const mid of await monitorIds(automationId)) await waitFor(mid);static readonly HashSet<int> Terminal = new() { 2, 4, 5, 9, 99 };
static readonly HashSet<string> SkipRowTypes = new() { "automation", "history", "flow" };
static async Task<List<string>> MonitorIds(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;
var ids = new List<string>();
foreach (var r in records.EnumerateArray())
{
var type = r.TryGetProperty("type", out var t) ? t.GetString() : null;
if (type != null && SkipRowTypes.Contains(type)) continue;
if (r.TryGetProperty("monitorId", out var m)) ids.Add(m.GetString()!);
else if (r.TryGetProperty("id", out var i)) ids.Add(i.GetString()!);
}
return ids;
}
static async Task WaitFor(string monitorId, int timeoutSeconds = 3600)
{
var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
while (DateTime.UtcNow < deadline)
{
var detail = await Call("GET", "/api/transfers/" + monitorId);
var status = detail.GetProperty("status").GetInt32();
Console.WriteLine(quot; status={status}");
var isTerminal = detail.TryGetProperty("isTerminal", out var it)
? it.GetBoolean() : Terminal.Contains(status);
if (isTerminal) return;
await Task.Delay(3000);
}
throw new Exception(quot;{monitorId} did not finish within the time limit");
}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.apiKeyCreate 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 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.