Discover

Get Started

Developers

Industries

Build File Transfer Into Your Product.

Add INNORIX file transfer to your application with APIs and SDKs. Keep your own UI and user experience while INNORIX handles file movement behind the scenes.

Python

Create the code step by step to create, monitor, and control a 1:1 transfer by calling the API from a Python application between two already registered devices.

Device registration is assumed to be complete (the source and target devices are displayed in the device list). Always check the Swagger documentation for the exact endpoint schema.

code
1
2
pip install requests

Common Setup

First, configure a reusable session and authentication headers for all requests.

Base URL and Session Setup

Configure the common settings once with requests.Session and reuse them.

code
1
2
3
4
5
6
7
8
import time import requests BASE_URL = "https://exacoola.innorix.com" WORKSPACE_ID = "<WORKSPACE_ID>" session = requests.Session()

Issue a Token by Logging In

Log in with your account to obtain an access token. Use data.user.accessToken from the response for subsequent requests.

code
1
2
3
4
5
6
7
8
9
def login(email: str, password: str) -> str: res = session.post( f"{BASE_URL}/api/auth/login", json={"email": email, "password": password}, timeout=10, ) res.raise_for_status() return res.json()["data"]["user"]["accessToken"]

Configure Authentication Headers

Add the issued token and workspace ID to the session's default headers so you do not need to specify them repeatedly for each request.

code
1
2
3
4
5
6
7
def set_auth(access_token: str) -> None: session.headers.update({ "Authorization": f"Bearer {access_token}", "x-workspace-id": WORKSPACE_ID, "Content-Type": "application/json", })

If the token expires, obtain a new token using POST /api/auth/refresh-token (header X-Refresh-Token) or GET /api/auth/get-token, then set it again.

Create a 1:1 Transfer

This is the core flow for sending files directly from the source device to the target device.

Check the Source and Target Devices

Use GET /api/device to retrieve the device list and identify the deviceId of the device that will send the files (source) and the device that will receive them (target).

code
1
2
3
4
5
6
7
8
9
def list_devices() -> list: res = session.get( f"{BASE_URL}/api/device", params={"page": 1, "size": 20}, timeout=10, ) res.raise_for_status() return res.json()["data"]["items"]

Check Connectivity

Checking that both the source and target devices are online before starting the transfer can reduce failures.

code
1
2
3
4
5
6
7
8
def is_online(device_id: str) -> bool: res = session.get( f"{BASE_URL}/api/device/connectivity/{device_id}", timeout=10, ) res.raise_for_status() return res.json()

Browse the Source Path

Skip this step if the file path to be sent is already known in the code. If the path needs to be found dynamically, browse the path on the source device.

code
1
2
3
4
5
6
7
8
9
def browse(device_id: str, path: str, only_folder: bool = False) -> dict: res = session.post( f"{BASE_URL}/api/explorer/fileSearchV3/{device_id}", json={"path": path, "onlyFolder": only_folder}, timeout=15, ) res.raise_for_status() return res.json()

Specify the Target Path

Specify the target storage path as a plain-text absolute path based on the target device.

code
1
2
target_path = "C:/Users/innorix/Downloads/New folder (3)"

For Windows paths, it is safer to use forward slashes (/) instead of backslashes (\).

Create the Transfer

Create the transfer using the source/target deviceId, the file paths to send, and the target storage path (targetPath). Save the monitorId from the response because it is used for subsequent control and monitoring.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def create_transfer(source_id: str, target_id: str, target_path: str, source_file_paths: list) -> str: body = { "sourceId": source_id, "targetId": target_id, "targetPath": target_path, "sourceItem": [{"filePath": p} for p in source_file_paths], } res = session.post( f"{BASE_URL}/api/transfer/manualTransfer", json=body, timeout=15, ) res.raise_for_status() return res.json()["data"]["monitorId"]

Request body fields: sourceId (sending device), targetId (receiving device), targetPath (storage path on the target device), sourceItem (list of files/folders to send — specifying a folder path also transfers its contents).

Check Transfer Status

Poll the progress periodically using monitorId. Wait until the transfer is completed or fails.

code
1
2
3
4
5
6
7
8
9
10
11
12
TERMINAL = {"completed", "failed", "canceled", "cancelled"} def wait_for_completion(monitor_id: str, interval: float = 3.0) -> str: while True: res = session.get(f"{BASE_URL}/api/transfer/{monitor_id}/status", timeout=10) res.raise_for_status() status = res.json().get("data", {}).get("status") print("transfer status:", status) if status in TERMINAL: return status time.sleep(interval)

After completion, retrieve the detailed result with GET /api/transfer-history/{monitorId}/detail?idType=monitor and file-level results with GET /api/transfer-history/{monitorId}/get-files?idType=monitor.

The exact status strings (completed, etc.) may vary depending on the environment, so check the actual response logs before finalizing the termination conditions.

Transfer Control

Control an in-progress transfer using monitorId.

Pause · Resume · Cancel

All three actions use PATCH requests with no request body.

code
1
2
3
4
def control_transfer(monitor_id: str, action: str) -> None: res = session.patch(f"{BASE_URL}/api/transfer/{action}/{monitor_id}", timeout=10) res.raise_for_status()

Pass one of pause, resume, or cancel as the action.

Retry Failed Files

If some files fail during the transfer, retry only the failed files.

code
1
2
3
4
5
6
7
8
def retry_failed(monitor_id: str, files: list) -> None: res = session.post( f"{BASE_URL}/api/transfer/retry-failed-files", json={"monitorId": monitor_id, "filesRetry": files}, timeout=15, ) res.raise_for_status()

Complete Example

This is a minimal executable example that combines login, transfer creation, and status polling. For actual integration, enhance token renewal, error handling, and retries as appropriate for your environment.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import time import requests BASE_URL = "https://exacoola.innorix.com" WORKSPACE_ID = "<WORKSPACE_ID>" session = requests.Session() def login(email, password): res = session.post(f"{BASE_URL}/api/auth/login", json={"email": email, "password": password}, timeout=10) res.raise_for_status() return res.json()["data"]["user"]["accessToken"] def set_auth(access_token): session.headers.update({ "Authorization": f"Bearer {access_token}", "x-workspace-id": WORKSPACE_ID, "Content-Type": "application/json", }) def create_transfer(source_id, target_id, target_path, source_file_paths): body = { "sourceId": source_id, "targetId": target_id, "targetPath": target_path, "sourceItem": [{"filePath": p} for p in source_file_paths], } res = session.post(f"{BASE_URL}/api/transfer/manualTransfer", json=body, timeout=15) res.raise_for_status() return res.json()["data"]["monitorId"] def wait_for_completion(monitor_id, interval=3.0): terminal = {"completed", "failed", "canceled", "cancelled"} while True: res = session.get(f"{BASE_URL}/api/transfer/{monitor_id}/status", timeout=10) res.raise_for_status() status = res.json().get("data", {}).get("status") print("transfer status:", status) if status in terminal: return status time.sleep(interval) if __name__ == "__main__": token = login("<YOUR_EMAIL>", "<YOUR_PASSWORD>") set_auth(token) monitor_id = create_transfer( source_id="6901ae48ca578216fd739f78", target_id="690037c22d309a7bc494bc53", target_path="C:/Users/innorix/Downloads/New folder (3)", source_file_paths=["C:/Users/MY PC/Downloads/100 files/New folder"], ) print("monitorId:", monitor_id) final = wait_for_completion(monitor_id) print("final:", final)

Automation

For scheduled transfers that run repeatedly at a specified time, use the automation endpoint instead of the one-time manualTransfer. The field structure differs from an immediate transfer, so first check the differences below.

ItemImmediate transfer (manualTransfer)Scheduled transfer (automation)
Source/target fieldssourceId / targetIdsenderId / receiverId
sourceItem[{"filePath": "..."}] (object array)["...", "..."] (string array)
ExecutionOnce immediatelyRepeated according to schedule

Configure the Schedule

Define the execution cycle. Use hour and minute for the execution time, timezone for the reference time zone, and startDate and endDate for the validity period.

code
1
2
3
4
5
6
7
8
9
10
def build_automation(hour, minute, timezone, start_date, end_date, schedule_type="day"): return { "type": schedule_type, "hour": hour, "minute": minute, "timezone": timezone, "startDate": start_date, "endDate": end_date, }

type is day (every day), hour and minute specify the execution time (for example, 09, 30), timezone specifies the reference time zone (for example, Asia/Seoul), and startDate and endDate use ISO 8601 UTC format (for example, 2026-01-01T00:00:00.000Z).

Create an Automation

Create an automation containing the schedule and transfer details (details). Use the automationId from the response for subsequent updates, deletion, and control.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def create_automation(name, schedule, sender_id, receiver_id, target_path, source_items, file_count, folder_count=0, size_count=0): body = { "name": name, "schedule": schedule, "details": [ { "sourceItem": source_items, "targetPath": target_path, "senderId": sender_id, "receiverId": receiver_id, "step": 1, "fileCount": file_count, "folderCount": folder_count, "sizeCount": size_count, } ], } res = session.post(f"{BASE_URL}/api/automation", json=body, timeout=15) res.raise_for_status() return res.json()["data"]["automationId"]

Example:

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
schedule = build_automation("02", "00", "Asia/Seoul", "2026-01-01T00:00:00.000Z", "2026-12-31T00:00:00.000Z") automation_id = create_automation( name="Nightly backup", schedule=schedule, sender_id="6901ae48ca578216fd739f78", receiver_id="690037c22d309a7bc494bc53", target_path="D:/Backup/Daily_Reports", source_items=["D:/Projects/data/exported_users.csv", "D:/Projects/data/sales_report.pdf"], file_count=2, ) print("automationId:", automation_id)

Automatically Generate an Automation Name

If you do not want to specify a name manually, the server can generate one.

code
1
2
3
4
5
def generate_automation_name(): res = session.get(f"{BASE_URL}/api/automation/generateAutomationName", timeout=10) res.raise_for_status() return res.json()

Pause · Resume Automation

Use the pause value to stop or restart the automation.

code
1
2
3
4
5
def pause_automation(pause=True): res = session.post(f"{BASE_URL}/api/automation/pause", json={"pause": pause}, timeout=10) res.raise_for_status()

Update · Delete Automation

Change the schedule or transfer details, or remove the automation.

code
1
2
3
4
5
6
7
8
9
def update_automation(automation_id, body): res = session.patch(f"{BASE_URL}/api/automation/{automation_id}", json=body, timeout=15) res.raise_for_status() def delete_automation(automation_id): res = session.delete(f"{BASE_URL}/api/automation/{automation_id}", timeout=10) res.raise_for_status()

Retrieve the automation's progress and status with GET /api/automation/{automationId}/details.

Common Issues

SymptomCause · Solution
401 UnauthorizedToken expired or header missing. Refresh the token with refresh-token and call set_auth() again.
Transfer does not startSource/target device is offline. Check whether both devices are online with connectivity.
File is saved in the wrong locationCheck that targetPath is an absolute path based on the target device.
Source file cannot be foundsourceItem path is not an absolute path based on the source device. Check the actual path with fileSearchV3.
Only some files failRetry only the failed files with retry-failed-files.

Development Resources and Examples

Code examples and documentation required for transfer integration are available on INNORIX GitHub, and the complete endpoint specification is available in the Swagger documentation.


Node.js

Create the code step by step to create, monitor, and control a 1:1 transfer by calling the API from a Node.js application between two already registered devices.

Device registration is assumed to be complete (the source and target devices are displayed in the device list). Always check the Swagger documentation for the exact endpoint schema.

Node.js 18 or later can use the built-in fetch directly (no additional installation required).

Common Setup

First, configure the constants and authentication headers to be reused for all requests.

Base URL and Constants

Declare the Base URL, workspace ID, and token variable that will be populated after login.

code
1
2
3
4
5
const BASE_URL = "https://exacoola.innorix.com"; const WORKSPACE_ID = "<WORKSPACE_ID>"; let accessToken = "";

Issue a Token by Logging In

Log in with your account to obtain an access token. Use data.user.accessToken from the response for subsequent requests.

code
1
2
3
4
5
6
7
8
9
10
11
12
async function login(email, password) { const res = await fetch(`${BASE_URL}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); if (!res.ok) throw new Error(`login failed: ${res.status}`); const json = await res.json(); accessToken = json.data.user.accessToken; return accessToken; }

Configure Authentication Headers

Create a function containing the token and workspace ID and reuse it for subsequent requests.

code
1
2
3
4
5
6
7
8
function authHeaders() { return { "Authorization": `Bearer ${accessToken}`, "x-workspace-id": WORKSPACE_ID, "Content-Type": "application/json", }; }

If the token expires, obtain a new token using POST /api/auth/refresh-token (header X-Refresh-Token) or GET /api/auth/get-token, then set it again.

Create a 1:1 Transfer

This is the core flow for sending files directly from the source device to the target device.

Check the Source and Target Devices

Use GET /api/device to retrieve the device list and identify the deviceId of the device that will send the files (source) and the device that will receive them (target).

code
1
2
3
4
5
6
7
8
9
async function listDevices() { const url = new URL(`${BASE_URL}/api/device`); url.searchParams.set("page", "1"); url.searchParams.set("size", "20"); const res = await fetch(url, { headers: authHeaders() }); if (!res.ok) throw new Error(`device list failed: ${res.status}`); return (await res.json()).data.items; }

Check Connectivity

Checking that both the source and target devices are online before starting the transfer can reduce failures.

code
1
2
3
4
5
6
7
8
async function isOnline(deviceId) { const res = await fetch(`${BASE_URL}/api/device/connectivity/${deviceId}`, { headers: authHeaders(), }); if (!res.ok) throw new Error(`connectivity failed: ${res.status}`); return await res.json(); }

Browse the Source Path

Skip this step if the file path to be sent is already known in the code. If the path needs to be found dynamically, browse the path on the source device.

code
1
2
3
4
5
6
7
8
9
10
async function browse(deviceId, path, onlyFolder = false) { const res = await fetch(`${BASE_URL}/api/explorer/fileSearchV3/${deviceId}`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ path, onlyFolder }), }); if (!res.ok) throw new Error(`browse failed: ${res.status}`); return await res.json(); }

Specify the Target Path

Specify the target storage path as a plain-text absolute path based on the target device.

code
1
2
const targetPath = "C:/Users/innorix/Downloads/New folder (3)";

For Windows paths, it is safer to use forward slashes (/) instead of backslashes (\).

Create the Transfer

Create the transfer using the source/target deviceId, the file paths to send, and the target storage path (targetPath). Save the monitorId from the response because it is used for subsequent control and monitoring.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function createTransfer(sourceId, targetId, targetPath, sourceFilePaths) { const body = { sourceId, targetId, targetPath, sourceItem: sourceFilePaths.map((filePath) => ({ filePath })), }; const res = await fetch(`${BASE_URL}/api/transfer/manualTransfer`, { method: "POST", headers: authHeaders(), body: JSON.stringify(body), }); if (!res.ok) throw new Error(`transfer failed: ${res.status}`); return (await res.json()).data.monitorId; }

Request body fields: sourceId (sending device), targetId (receiving device), targetPath (storage path on the target device), sourceItem (list of files/folders to send — specifying a folder path also transfers its contents).

Check Transfer Status

Poll the progress periodically using monitorId. Wait until the transfer is completed or fails.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const TERMINAL = new Set(["completed", "failed", "canceled", "cancelled"]); async function waitForCompletion(monitorId, intervalMs = 3000) { while (true) { const res = await fetch(`${BASE_URL}/api/transfer/${monitorId}/status`, { headers: authHeaders(), }); if (!res.ok) throw new Error(`status failed: ${res.status}`); const status = (await res.json())?.data?.status; console.log("transfer status:", status); if (TERMINAL.has(status)) return status; await new Promise((r) => setTimeout(r, intervalMs)); } }

After completion, retrieve the detailed result with GET /api/transfer-history/{monitorId}/detail?idType=monitor and file-level results with GET /api/transfer-history/{monitorId}/get-files?idType=monitor.

The exact status strings (completed, etc.) may vary depending on the environment, so check the actual response logs before finalizing the termination conditions.

Transfer Control

Control an in-progress transfer using monitorId.

Pause · Resume · Cancel

All three actions use PATCH requests with no request body.

code
1
2
3
4
5
6
7
8
async function controlTransfer(monitorId, action) { const res = await fetch(`${BASE_URL}/api/transfer/${action}/${monitorId}`, { method: "PATCH", headers: authHeaders(), }); if (!res.ok) throw new Error(`control failed: ${res.status}`); }

Pass one of pause, resume, or cancel as the action.

Retry Failed Files

If some files fail during the transfer, retry only the failed files.

code
1
2
3
4
5
6
7
8
9
async function retryFailed(monitorId, files) { const res = await fetch(`${BASE_URL}/api/transfer/retry-failed-files`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ monitorId, filesRetry: files }), }); if (!res.ok) throw new Error(`retry failed: ${res.status}`); }

Complete Example

This is a minimal executable example that combines login, transfer creation, and status polling. For actual integration, enhance token renewal, error handling, and retries as appropriate for your environment.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const BASE_URL = "https://exacoola.innorix.com"; const WORKSPACE_ID = "<WORKSPACE_ID>"; let accessToken = ""; function authHeaders() { return { "Authorization": `Bearer ${accessToken}`, "x-workspace-id": WORKSPACE_ID, "Content-Type": "application/json", }; } async function login(email, password) { const res = await fetch(`${BASE_URL}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); if (!res.ok) throw new Error(`login failed: ${res.status}`); accessToken = (await res.json()).data.user.accessToken; return accessToken; } async function createTransfer(sourceId, targetId, targetPath, sourceFilePaths) { const body = { sourceId, targetId, targetPath, sourceItem: sourceFilePaths.map((filePath) => ({ filePath })), }; const res = await fetch(`${BASE_URL}/api/transfer/manualTransfer`, { method: "POST", headers: authHeaders(), body: JSON.stringify(body), }); if (!res.ok) throw new Error(`transfer failed: ${res.status}`); return (await res.json()).data.monitorId; } async function waitForCompletion(monitorId, intervalMs = 3000) { const terminal = new Set(["completed", "failed", "canceled", "cancelled"]); while (true) { const res = await fetch(`${BASE_URL}/api/transfer/${monitorId}/status`, { headers: authHeaders(), }); if (!res.ok) throw new Error(`status failed: ${res.status}`); const status = (await res.json())?.data?.status; console.log("transfer status:", status); if (terminal.has(status)) return status; await new Promise((r) => setTimeout(r, intervalMs)); } } (async () => { await login("<YOUR_EMAIL>", "<YOUR_PASSWORD>"); const monitorId = await createTransfer( "6901ae48ca578216fd739f78", "690037c22d309a7bc494bc53", "C:/Users/innorix/Downloads/New folder (3)", ["C:/Users/MY PC/Downloads/100 files/New folder"], ); console.log("monitorId:", monitorId); const final = await waitForCompletion(monitorId); console.log("final:", final); })();

Automation

For scheduled transfers that run repeatedly at a specified time, use the automation endpoint instead of the one-time manualTransfer. The field structure differs from an immediate transfer, so first check the differences below.

ItemImmediate transfer (manualTransfer)Scheduled transfer (automation)
Source/target fieldssourceId / targetIdsenderId / receiverId
sourceItem[{"filePath": "..."}] (object array)["...", "..."] (string array)
ExecutionOnce immediatelyRepeated according to schedule

Configure the Schedule

Define the execution cycle. Use hour and minute for the execution time, timezone for the reference time zone, and startDate and endDate for the validity period.

code
1
2
3
4
5
6
7
8
9
10
11
function buildAutomation(hour, minute, timezone, startDate, endDate, type = "day") { return { type, hour, minute, timezone, startDate, endDate, }; }

type is day (every day), hour and minute specify the execution time (for example, 09, 30), timezone specifies the reference time zone (for example, Asia/Seoul), and startDate and endDate use ISO 8601 UTC format (for example, 2026-01-01T00:00:00.000Z).

Create an Automation

Create an automation containing the schedule and transfer details (details). Use the automationId from the response for subsequent updates, deletion, and control.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
async function createAutomation({ name, schedule, senderId, receiverId, targetPath, sourceItems, fileCount, folderCount = 0, sizeCount = 0, }) { const body = { name, schedule, details: [ { sourceItem: sourceItems, targetPath, senderId, receiverId, step: 1, fileCount, folderCount, sizeCount, }, ], }; const res = await fetch(`${BASE_URL}/api/automation`, { method: "POST", headers: authHeaders(), body: JSON.stringify(body), }); if (!res.ok) throw new Error(`automation failed: ${res.status}`); return (await res.json()).data.automationId; }

Example:

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const schedule = buildAutomation("02", "00", "Asia/Seoul", "2026-01-01T00:00:00.000Z", "2026-12-31T00:00:00.000Z"); const automationId = await createAutomation({ name: "Nightly backup", schedule, senderId: "6901ae48ca578216fd739f78", receiverId: "690037c22d309a7bc494bc53", targetPath: "D:/Backup/Daily_Reports", sourceItems: [ "D:/Projects/data/exported_users.csv", "D:/Projects/data/sales_report.pdf", ], fileCount: 2, }); console.log("automationId:", automationId);

Automatically Generate an Automation Name

If you do not want to specify a name manually, the server can generate one.

code
1
2
3
4
5
6
7
8
async function generateAutomationName() { const res = await fetch(`${BASE_URL}/api/automation/generateAutomationName`, { headers: authHeaders(), }); if (!res.ok) throw new Error(`name gen failed: ${res.status}`); return await res.json(); }

Pause · Resume Automation

Use the pause value to stop or restart the automation.

code
1
2
3
4
5
6
7
8
9
async function pauseAutomation(pause = true) { const res = await fetch(`${BASE_URL}/api/automation/pause`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ pause }), }); if (!res.ok) throw new Error(`pause failed: ${res.status}`); }

Update · Delete Automation

Change the schedule or transfer details, or remove the automation.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function updateAutomation(automationId, body) { const res = await fetch(`${BASE_URL}/api/automation/${automationId}`, { method: "PATCH", headers: authHeaders(), body: JSON.stringify(body), }); if (!res.ok) throw new Error(`update failed: ${res.status}`); } async function deleteAutomation(automationId) { const res = await fetch(`${BASE_URL}/api/automation/${automationId}`, { method: "DELETE", headers: authHeaders(), }); if (!res.ok) throw new Error(`delete failed: ${res.status}`); }

Retrieve the automation's progress and status with GET /api/automation/{automationId}/details.

Common Issues

SymptomCause · Solution
401 UnauthorizedToken expired or header missing. Refresh the token with refresh-token and reset accessToken.
Transfer does not startSource/target device is offline. Check whether both devices are online with connectivity.
File is saved in the wrong locationCheck that targetPath is an absolute path based on the target device.
Source file cannot be foundsourceItem path is not an absolute path based on the source device. Check the actual path with fileSearchV3.
Only some files failRetry only the failed files with retry-failed-files.

Development Resources and Examples

Code examples and documentation required for transfer integration are available on INNORIX GitHub, and the complete endpoint specification is available in the Swagger documentation.


Java

Create the code step by step to create, monitor, and control a 1:1 transfer by calling the API from a Java application between two already registered devices.

Device registration is assumed to be complete (the source and target devices are displayed in the device list). Always check the Swagger documentation for the exact endpoint schema.

Use the built-in HttpClient (java.net.http) in Java 11+, and use Jackson for JSON processing (Maven dependency).

code
1
2
3
4
5
6
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.0</version> </dependency>

Common Setup

First, configure a reusable client and authentication headers for all requests. The methods in each subsequent step belong to this ExacoolaClient class.

Base URL and Client Setup

Create the HttpClient and Jackson ObjectMapper once and reuse them.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.*; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; public class ExacoolaClient { static final String BASE_URL = "https://exacoola.innorix.com"; static final String WORKSPACE_ID = "<WORKSPACE_ID>"; final HttpClient http = HttpClient.newHttpClient(); final ObjectMapper mapper = new ObjectMapper(); String accessToken = ""; }

Issue a Token by Logging In

Log in with your account to obtain an access token. Use data.user.accessToken from the response for subsequent requests.

code
1
2
3
4
5
6
7
8
9
10
11
public String login(String email, String password) throws Exception { String payload = mapper.writeValueAsString(Map.of("email", email, "password", password)); HttpRequest req = HttpRequest.newBuilder(URI.create(BASE_URL + "/api/auth/login")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); accessToken = mapper.readTree(res.body()).at("/data/user/accessToken").asText(); return accessToken; }

Configure Authentication Headers

This helper creates a request builder containing the token and workspace ID. Reuse it for all subsequent calls.

code
1
2
3
4
5
6
7
8
private HttpRequest.Builder authed(String path) { return HttpRequest.newBuilder(URI.create(BASE_URL + path)) .header("Authorization", "Bearer " + accessToken) .header("x-workspace-id", WORKSPACE_ID) .header("Content-Type", "application/json") .timeout(Duration.ofSeconds(15)); }

If the token expires, obtain a new token using POST /api/auth/refresh-token (header X-Refresh-Token) or GET /api/auth/get-token, then set it again.

Create a 1:1 Transfer

This is the core flow for sending files directly from the source device to the target device.

Check the Source and Target Devices

Use GET /api/device to retrieve the device list and identify the deviceId of the device that will send the files (source) and the device that will receive them (target).

code
1
2
3
4
5
6
public JsonNode listDevices() throws Exception { HttpRequest req = authed("/api/device?page=1&size=20").GET().build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); return mapper.readTree(res.body()).at("/data/items"); }

Check Connectivity

Checking that both the source and target devices are online before starting the transfer can reduce failures.

code
1
2
3
4
5
6
public JsonNode isOnline(String deviceId) throws Exception { HttpRequest req = authed("/api/device/connectivity/" + deviceId).GET().build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); return mapper.readTree(res.body()); }

Browse the Source Path

Skip this step if the file path to be sent is already known in the code. If the path needs to be found dynamically, browse the path on the source device.

code
1
2
3
4
5
6
7
8
public JsonNode browse(String deviceId, String path, boolean onlyFolder) throws Exception { String payload = mapper.writeValueAsString(Map.of("path", path, "onlyFolder", onlyFolder)); HttpRequest req = authed("/api/explorer/fileSearchV3/" + deviceId) .POST(HttpRequest.BodyPublishers.ofString(payload)).build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); return mapper.readTree(res.body()); }

Specify the Target Path

Specify the target storage path as a plain-text absolute path based on the target device.

code
1
2
String targetPath = "C:/Users/innorix/Downloads/New folder (3)";

For Windows paths, it is safer to use forward slashes (/) instead of backslashes (\).

Create the Transfer

Create the transfer using the source/target deviceId, the file paths to send, and the target storage path (targetPath). Save the monitorId from the response because it is used for subsequent control and monitoring.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String createTransfer(String sourceId, String targetId, String targetPath, List<String> sourceFilePaths) throws Exception { List<Object> sourceItem = new ArrayList<>(); for (String p : sourceFilePaths) sourceItem.add(Map.of("filePath", p)); Map<String, Object> body = new LinkedHashMap<>(); body.put("sourceId", sourceId); body.put("targetId", targetId); body.put("targetPath", targetPath); body.put("sourceItem", sourceItem); HttpRequest req = authed("/api/transfer/manualTransfer") .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body))).build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); return mapper.readTree(res.body()).at("/data/monitorId").asText(); }

Request body fields: sourceId (sending device), targetId (receiving device), targetPath (storage path on the target device), sourceItem (list of files/folders to send — specifying a folder path also transfers its contents).

Check Transfer Status

Poll the progress periodically using monitorId. Wait until the transfer is completed or fails.

code
1
2
3
4
5
6
7
8
9
10
11
12
public String waitForCompletion(String monitorId, long intervalMs) throws Exception { Set<String> terminal = Set.of("completed", "failed", "canceled", "cancelled"); while (true) { HttpRequest req = authed("/api/transfer/" + monitorId + "/status").GET().build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); String status = mapper.readTree(res.body()).at("/data/status").asText(); System.out.println("transfer status: " + status); if (terminal.contains(status)) return status; Thread.sleep(intervalMs); } }

After completion, retrieve the detailed result with GET /api/transfer-history/{monitorId}/detail?idType=monitor and file-level results with GET /api/transfer-history/{monitorId}/get-files?idType=monitor.

The exact status strings (completed, etc.) may vary depending on the environment, so check the actual response logs before finalizing the termination conditions.

Transfer Control

Control an in-progress transfer using monitorId.

Pause · Resume · Cancel

All three actions use PATCH requests with no request body.

code
1
2
3
4
5
6
public void controlTransfer(String monitorId, String action) throws Exception { HttpRequest req = authed("/api/transfer/" + action + "/" + monitorId) .method("PATCH", HttpRequest.BodyPublishers.noBody()).build(); http.send(req, HttpResponse.BodyHandlers.ofString()); }

Pass one of pause, resume, or cancel as the action.

Retry Failed Files

If some files fail during the transfer, retry only the failed files.

code
1
2
3
4
5
6
7
8
9
10
public void retryFailed(String monitorId, List<Map<String, Object>> files) throws Exception { Map<String, Object> body = new LinkedHashMap<>(); body.put("monitorId", monitorId); body.put("filesRetry", files); HttpRequest req = authed("/api/transfer/retry-failed-files") .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body))).build(); http.send(req, HttpResponse.BodyHandlers.ofString()); }

Complete Example

This is a minimal executable example that combines login, transfer creation, and status polling. For actual integration, enhance token renewal, error handling, and retries as appropriate for your environment.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.*; import com.fasterxml.jackson.databind.ObjectMapper; public class ExacoolaClient { static final String BASE_URL = "https://exacoola.innorix.com"; static final String WORKSPACE_ID = "<WORKSPACE_ID>"; final HttpClient http = HttpClient.newHttpClient(); final ObjectMapper mapper = new ObjectMapper(); String accessToken = ""; HttpRequest.Builder authed(String path) { return HttpRequest.newBuilder(URI.create(BASE_URL + path)) .header("Authorization", "Bearer " + accessToken) .header("x-workspace-id", WORKSPACE_ID) .header("Content-Type", "application/json") .timeout(Duration.ofSeconds(15)); } String login(String email, String password) throws Exception { String payload = mapper.writeValueAsString(Map.of("email", email, "password", password)); HttpRequest req = HttpRequest.newBuilder(URI.create(BASE_URL + "/api/auth/login")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload)).build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); accessToken = mapper.readTree(res.body()).at("/data/user/accessToken").asText(); return accessToken; } String createTransfer(String sourceId, String targetId, String targetPath, List<String> sourceFilePaths) throws Exception { List<Object> sourceItem = new ArrayList<>(); for (String p : sourceFilePaths) sourceItem.add(Map.of("filePath", p)); Map<String, Object> body = new LinkedHashMap<>(); body.put("sourceId", sourceId); body.put("targetId", targetId); body.put("targetPath", targetPath); body.put("sourceItem", sourceItem); HttpRequest req = authed("/api/transfer/manualTransfer") .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body))).build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); return mapper.readTree(res.body()).at("/data/monitorId").asText(); } String waitForCompletion(String monitorId, long intervalMs) throws Exception { Set<String> terminal = Set.of("completed", "failed", "canceled", "cancelled"); while (true) { HttpRequest req = authed("/api/transfer/" + monitorId + "/status").GET().build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); String status = mapper.readTree(res.body()).at("/data/status").asText(); System.out.println("transfer status: " + status); if (terminal.contains(status)) return status; Thread.sleep(intervalMs); } } public static void main(String[] args) throws Exception { ExacoolaClient client = new ExacoolaClient(); client.login("<YOUR_EMAIL>", "<YOUR_PASSWORD>"); String monitorId = client.createTransfer( "6901ae48ca578216fd739f78", "690037c22d309a7bc494bc53", "C:/Users/innorix/Downloads/New folder (3)", List.of("C:/Users/MY PC/Downloads/100 files/New folder")); System.out.println("monitorId: " + monitorId); String finalStatus = client.waitForCompletion(monitorId, 3000); System.out.println("final: " + finalStatus); } }

Automation

For scheduled transfers that run repeatedly at a specified time, use the automation endpoint instead of the one-time manualTransfer. The field structure differs from an immediate transfer, so first check the differences below.

ItemImmediate transfer (manualTransfer)Scheduled transfer (automation)
Source/target fieldssourceId / targetIdsenderId / receiverId
sourceItem[{"filePath": "..."}] (object array)["...", "..."] (string array)
ExecutionOnce immediatelyRepeated according to schedule

Configure the Schedule

Define the execution cycle. Use hour and minute for the execution time, timezone for the reference time zone, and startDate and endDate for the validity period.

code
1
2
3
4
5
6
7
8
9
10
11
12
public Map<String, Object> buildAutomation(String hour, String minute, String timezone, String startDate, String endDate, String type) { Map<String, Object> s = new LinkedHashMap<>(); s.put("type", type); s.put("hour", hour); s.put("minute", minute); s.put("timezone", timezone); s.put("startDate", startDate);// "2026-01-01T00:00:00.000Z" (ISO 8601 UTC) s.put("endDate", endDate); return s; }

type is day (every day), hour and minute specify the execution time (for example, 09, 30), timezone specifies the reference time zone (for example, Asia/Seoul), and startDate and endDate use ISO 8601 UTC format (for example, 2026-01-01T00:00:00.000Z).

Create an Automation

Create an automation containing the schedule and transfer details (details). Use the automationId from the response for subsequent updates, deletion, and control.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public String createAutomation(String name, Map<String, Object> schedule, String senderId, String receiverId, String targetPath, List<String> sourceItems, int fileCount) throws Exception { Map<String, Object> detail = new LinkedHashMap<>(); detail.put("sourceItem", sourceItems); detail.put("targetPath", targetPath); detail.put("senderId", senderId); detail.put("receiverId", receiverId); detail.put("step", 1); detail.put("fileCount", fileCount); detail.put("folderCount", 0); detail.put("sizeCount", 0); Map<String, Object> body = new LinkedHashMap<>(); body.put("name", name); body.put("schedule", schedule); body.put("details", List.of(detail)); HttpRequest req = authed("/api/automation") .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body))).build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); return mapper.readTree(res.body()).at("/data/automationId").asText(); }

Example:

code
1
2
3
4
5
6
7
8
9
10
11
12
13
Map<String, Object> schedule = buildAutomation("02", "00", "Asia/Seoul", "2026-01-01T00:00:00.000Z", "2026-12-31T00:00:00.000Z", "day"); String automationId = createAutomation( "Nightly backup", schedule, "6901ae48ca578216fd739f78", "690037c22d309a7bc494bc53", "D:/Backup/Daily_Reports", List.of("D:/Projects/data/exported_users.csv", "D:/Projects/data/sales_report.pdf"), 2); System.out.println("automationId: " + automationId);

Automatically Generate an Automation Name

If you do not want to specify a name manually, the server can generate one.

code
1
2
3
4
5
6
public JsonNode generateAutomationName() throws Exception { HttpRequest req = authed("/api/automation/generateAutomationName").GET().build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); return mapper.readTree(res.body()); }

Pause · Resume Automation

Use the pause value to stop or restart the automation.

code
1
2
3
4
5
6
7
public void pauseAutomation(boolean pause) throws Exception { String payload = mapper.writeValueAsString(Map.of("pause", pause)); HttpRequest req = authed("/api/automation/pause") .POST(HttpRequest.BodyPublishers.ofString(payload)).build(); http.send(req, HttpResponse.BodyHandlers.ofString()); }

Update · Delete Automation

Change the schedule or transfer details, or remove the automation.

code
1
2
3
4
5
6
7
8
9
10
11
12
public void updateAutomation(String automationId, Map<String, Object> body) throws Exception { HttpRequest req = authed("/api/automation/" + automationId) .method("PATCH", HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body))) .build(); http.send(req, HttpResponse.BodyHandlers.ofString()); } public void deleteAutomation(String automationId) throws Exception { HttpRequest req = authed("/api/automation/" + automationId).DELETE().build(); http.send(req, HttpResponse.BodyHandlers.ofString()); }

Retrieve the automation's progress and status with GET /api/automation/{automationId}/details.

Common Issues

SymptomCause · Solution
401 UnauthorizedToken expired or header missing. Refresh the token with refresh-token and reset accessToken.
Transfer does not startSource/target device is offline. Check whether both devices are online with connectivity.
File is saved in the wrong locationCheck that targetPath is an absolute path based on the target device.
Source file cannot be foundsourceItem path is not an absolute path based on the source device. Check the actual path with fileSearchV3.
Only some files failRetry only the failed files with retry-failed-files.

Development Resources and Examples

Code examples and documentation required for transfer integration are available on INNORIX GitHub, and the complete endpoint specification is available in the Swagger documentation.


C#

Create the code step by step to create, monitor, and control a 1:1 transfer by calling the API from a C#/.NET application between two already registered devices.

Device registration is assumed to be complete (the source and target devices are displayed in the device list). Always check the Swagger documentation for the exact endpoint schema.

This example is based on .NET 8 and uses HttpClient and the built-in System.Net.Http.Json. PatchAsJsonAsync is available in .NET 7 and later.

Common Setup

First, configure a reusable client and authentication headers for all requests. The methods in each subsequent step belong to this ExacoolaClient class.

Base URL and Client Setup

Create the HttpClient once with BaseAddress and reuse it.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Threading.Tasks; class ExacoolaClient { const string BaseUrl = "https://exacoola.innorix.com"; const string WorkspaceId = "<WORKSPACE_ID>"; readonly HttpClient http = new() { BaseAddress = new Uri(BaseUrl) }; string accessToken = ""; }

Issue a Token by Logging In

Log in with your account to obtain an access token. Use data.user.accessToken from the response for subsequent requests.

code
1
2
3
4
5
6
7
8
9
10
11
async Task<string> LoginAsync(string email, string password) { var res = await http.PostAsJsonAsync("/api/auth/login", new { email, password }); res.EnsureSuccessStatusCode(); var json = await res.Content.ReadFromJsonAsync<JsonElement>(); accessToken = json.GetProperty("data").GetProperty("user") .GetProperty("accessToken").GetString()!; SetAuth(accessToken); return accessToken; }

Configure Authentication Headers

Add the token and workspace ID to the HttpClient default headers so you do not need to specify them repeatedly for each request.

code
1
2
3
4
5
6
7
8
9
void SetAuth(string token) { accessToken = token; http.DefaultRequestHeaders.Remove("Authorization"); http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); http.DefaultRequestHeaders.Remove("x-workspace-id"); http.DefaultRequestHeaders.Add("x-workspace-id", WorkspaceId); }

If the token expires, obtain a new token using POST /api/auth/refresh-token (header X-Refresh-Token) or GET /api/auth/get-token, then set it again.

Create a 1:1 Transfer

This is the core flow for sending files directly from the source device to the target device.

Check the Source and Target Devices

Use GET /api/device to retrieve the device list and identify the deviceId of the device that will send the files (source) and the device that will receive them (target).

code
1
2
3
4
5
6
async Task<JsonElement> ListDevicesAsync() { var json = await http.GetFromJsonAsync<JsonElement>("/api/device?page=1&size=20"); return json.GetProperty("data").GetProperty("items"); }

Check Connectivity

Checking that both the source and target devices are online before starting the transfer can reduce failures.

code
1
2
3
4
5
async Task<JsonElement> IsOnlineAsync(string deviceId) { return await http.GetFromJsonAsync<JsonElement>($"/api/device/connectivity/{deviceId}"); }

Browse the Source Path

Skip this step if the file path to be sent is already known in the code. If the path needs to be found dynamically, browse the path on the source device.

code
1
2
3
4
5
6
7
8
async Task<JsonElement> BrowseAsync(string deviceId, string path, bool onlyFolder = false) { var res = await http.PostAsJsonAsync($"/api/explorer/fileSearchV3/{deviceId}", new { path, onlyFolder }); res.EnsureSuccessStatusCode(); return await res.Content.ReadFromJsonAsync<JsonElement>(); }

Specify the Target Path

Specify the target storage path as a plain-text absolute path based on the target device.

code
1
2
string targetPath = "C:/Users/innorix/Downloads/New folder (3)";

For Windows paths, it is safer to use forward slashes (/) instead of backslashes (\).

Create the Transfer

Create the transfer using the source/target deviceId, the file paths to send, and the target storage path (targetPath). Save the monitorId from the response because it is used for subsequent control and monitoring.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async Task<string> CreateTransferAsync(string sourceId, string targetId, string targetPath, IEnumerable<string> sourceFilePaths) { var body = new { sourceId, targetId, targetPath, sourceItem = sourceFilePaths.Select(p => new { filePath = p }).ToArray(), }; var res = await http.PostAsJsonAsync("/api/transfer/manualTransfer", body); res.EnsureSuccessStatusCode(); var json = await res.Content.ReadFromJsonAsync<JsonElement>(); return json.GetProperty("data").GetProperty("monitorId").GetString()!; }

Request body fields: sourceId (sending device), targetId (receiving device), targetPath (storage path on the target device), sourceItem (list of files/folders to send — specifying a folder path also transfers its contents).

Check Transfer Status

Poll the progress periodically using monitorId. Wait until the transfer is completed or fails.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static readonly HashSet<string> Terminal = new() { "completed", "failed", "canceled", "cancelled" }; async Task<string> WaitForCompletionAsync(string monitorId, int intervalMs = 3000) { while (true) { var json = await http.GetFromJsonAsync<JsonElement>($"/api/transfer/{monitorId}/status"); var status = json.GetProperty("data").GetProperty("status").GetString(); Console.WriteLine($"transfer status: {status}"); if (status != null && Terminal.Contains(status)) return status; await Task.Delay(intervalMs); } }

After completion, retrieve the detailed result with GET /api/transfer-history/{monitorId}/detail?idType=monitor and file-level results with GET /api/transfer-history/{monitorId}/get-files?idType=monitor.

The exact status strings (completed, etc.) may vary depending on the environment, so check the actual response logs before finalizing the termination conditions.

Transfer Control

Control an in-progress transfer using monitorId.

Pause · Resume · Cancel

All three actions use PATCH requests with no request body.

code
1
2
3
4
5
6
async Task ControlTransferAsync(string monitorId, string action) { var res = await http.PatchAsync($"/api/transfer/{action}/{monitorId}", null); res.EnsureSuccessStatusCode(); }

Pass one of pause, resume, or cancel as the action.

Retry Failed Files

If some files fail during the transfer, retry only the failed files.

code
1
2
3
4
5
6
7
async Task RetryFailedAsync(string monitorId, IEnumerable<object> files) { var res = await http.PostAsJsonAsync("/api/transfer/retry-failed-files", new { monitorId, filesRetry = files }); res.EnsureSuccessStatusCode(); }

Complete Example

This is a minimal executable example that combines login, transfer creation, and status polling. For actual integration, enhance token renewal, error handling, and retries as appropriate for your environment.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Threading.Tasks; class ExacoolaClient { const string BaseUrl = "https://exacoola.innorix.com"; const string WorkspaceId = "<WORKSPACE_ID>"; readonly HttpClient http = new() { BaseAddress = new Uri(BaseUrl) }; string accessToken = ""; static readonly HashSet<string> Terminal = new() { "completed", "failed", "canceled", "cancelled" }; void SetAuth(string token) { accessToken = token; http.DefaultRequestHeaders.Remove("Authorization"); http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); http.DefaultRequestHeaders.Remove("x-workspace-id"); http.DefaultRequestHeaders.Add("x-workspace-id", WorkspaceId); } async Task<string> LoginAsync(string email, string password) { var res = await http.PostAsJsonAsync("/api/auth/login", new { email, password }); res.EnsureSuccessStatusCode(); var json = await res.Content.ReadFromJsonAsync<JsonElement>(); var token = json.GetProperty("data").GetProperty("user") .GetProperty("accessToken").GetString()!; SetAuth(token); return token; } async Task<string> CreateTransferAsync(string sourceId, string targetId, string targetPath, IEnumerable<string> sourceFilePaths) { var body = new { sourceId, targetId, targetPath, sourceItem = sourceFilePaths.Select(p => new { filePath = p }).ToArray(), }; var res = await http.PostAsJsonAsync("/api/transfer/manualTransfer", body); res.EnsureSuccessStatusCode(); var json = await res.Content.ReadFromJsonAsync<JsonElement>(); return json.GetProperty("data").GetProperty("monitorId").GetString()!; } async Task<string> WaitForCompletionAsync(string monitorId, int intervalMs = 3000) { while (true) { var json = await http.GetFromJsonAsync<JsonElement>($"/api/transfer/{monitorId}/status"); var status = json.GetProperty("data").GetProperty("status").GetString(); Console.WriteLine($"transfer status: {status}"); if (status != null && Terminal.Contains(status)) return status; await Task.Delay(intervalMs); } } static async Task Main() { var client = new ExacoolaClient(); await client.LoginAsync("<YOUR_EMAIL>", "<YOUR_PASSWORD>"); var monitorId = await client.CreateTransferAsync( "6901ae48ca578216fd739f78", "690037c22d309a7bc494bc53", "C:/Users/innorix/Downloads/New folder (3)", new[] { "C:/Users/MY PC/Downloads/100 files/New folder" }); Console.WriteLine($"monitorId: {monitorId}"); var finalStatus = await client.WaitForCompletionAsync(monitorId); Console.WriteLine($"final: {finalStatus}"); } }

Automation

For scheduled transfers that run repeatedly at a specified time, use the automation endpoint instead of the one-time manualTransfer. The field structure differs from an immediate transfer, so first check the differences below.

ItemImmediate transfer (manualTransfer)Scheduled transfer (automation)
Source/target fieldssourceId / targetIdsenderId / receiverId
sourceItem[{"filePath": "..."}] (object array)["...", "..."] (string array)
ExecutionOnce immediatelyRepeated according to schedule

Configure the Schedule

Define the execution cycle. Use hour and minute for the execution time, timezone for the reference time zone, and startDate and endDate for the validity period.

code
1
2
3
4
5
6
7
8
9
10
11
12
object BuildAutomation(string hour, string minute, string timezone, string startDate, string endDate, string type = "day") => new { type, hour, minute, timezone, startDate, endDate, };

type is day (every day), hour and minute specify the execution time (for example, 09, 30), timezone specifies the reference time zone (for example, Asia/Seoul), and startDate and endDate use ISO 8601 UTC format (for example, 2026-01-01T00:00:00.000Z).

Create an Automation

Create an automation containing the schedule and transfer details (details). Use the automationId from the response for subsequent updates, deletion, and control.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
async Task<string> CreateAutomationAsync(string name, object schedule, string senderId, string receiverId, string targetPath, IEnumerable<string> sourceItems, int fileCount, int folderCount = 0, int sizeCount = 0) { var body = new { name, schedule, details = new[] { new { sourceItem = sourceItems, targetPath, senderId, receiverId, step = 1, fileCount, folderCount, sizeCount, } } }; var res = await http.PostAsJsonAsync("/api/automation", body); res.EnsureSuccessStatusCode(); var json = await res.Content.ReadFromJsonAsync<JsonElement>(); return json.GetProperty("data").GetProperty("automationId").GetString()!; }

Example:

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var schedule = BuildAutomation("02", "00", "Asia/Seoul", "2026-01-01T00:00:00.000Z", "2026-12-31T00:00:00.000Z"); var automationId = await CreateAutomationAsync( "Nightly backup", schedule, "6901ae48ca578216fd739f78", "690037c22d309a7bc494bc53", "D:/Backup/Daily_Reports", new[] { "D:/Projects/data/exported_users.csv", "D:/Projects/data/sales_report.pdf", }, 2); Console.WriteLine($"automationId: {automationId}");

Automatically Generate an Automation Name

If you do not want to specify a name manually, the server can generate one.

code
1
2
3
4
5
async Task<JsonElement> GenerateAutomationNameAsync() { return await http.GetFromJsonAsync<JsonElement>("/api/automation/generateAutomationName"); }

Pause · Resume Automation

Use the pause value to stop or restart the automation.

code
1
2
3
4
5
6
async Task PauseAutomationAsync(bool pause = true) { var res = await http.PostAsJsonAsync("/api/automation/pause", new { pause }); res.EnsureSuccessStatusCode(); }

Update · Delete Automation

Change the schedule or transfer details, or remove the automation.

code
1
2
3
4
5
6
7
8
9
10
11
12
async Task UpdateAutomationAsync(string automationId, object body) { var res = await http.PatchAsJsonAsync($"/api/automation/{automationId}", body); res.EnsureSuccessStatusCode(); } async Task DeleteAutomationAsync(string automationId) { var res = await http.DeleteAsync($"/api/automation/{automationId}"); res.EnsureSuccessStatusCode(); }

Retrieve the automation's progress and status with GET /api/automation/{automationId}/details.

Common Issues

SymptomCause · Solution
401 UnauthorizedToken expired or header missing. Refresh the token with refresh-token and call SetAuth() again.
Transfer does not startSource/target device is offline. Check whether both devices are online with connectivity.
File is saved in the wrong locationCheck that targetPath is an absolute path based on the target device.
Source file cannot be foundsourceItem path is not an absolute path based on the source device. Check the actual path with fileSearchV3.
Only some files failRetry only the failed files with retry-failed-files.

Development Resources and Examples

Code examples and documentation required for transfer integration are available on INNORIX GitHub, and the complete endpoint specification is available in the Swagger documentation.


curl

Organize step by step how to create, monitor, and control a 1:1 transfer by calling the API with curl between two already registered devices.

Device registration is assumed to be complete (the source and target devices are displayed in the device list). Always check the Swagger documentation for the exact endpoint schema.

curl is provided by default in most environments. The complete example script requires jq for JSON parsing (sudo apt install jq or brew install jq).

Common Setup

First, configure the variables and authentication headers to reuse for all subsequent requests. (The examples below assume they are executed in a single shell session.)

Base URL and Variable Setup

Set the Base URL and workspace ID as shell variables.

code
1
2
3
BASE_URL="https://exacoola.innorix.com" WORKSPACE_ID="<WORKSPACE_ID>"

Issue a Token by Logging In

Log in with your account to obtain an access token. Store data.user.accessToken from the response in a variable for subsequent requests.

code
1
2
3
4
5
ACCESS_TOKEN=$(curl -s -X POST "$BASE_URL/api/auth/login" \ -H "Content-Type: application/json" \ -d '{"email":"<YOUR_EMAIL>","password":"<YOUR_PASSWORD>"}' \ | jq -r '.data.user.accessToken')

Configure Authentication Headers

Creating the authentication headers as an array allows you to reuse them in all subsequent requests with "${AUTH[@]}".

code
1
2
AUTH=(-H "Authorization: Bearer $ACCESS_TOKEN" -H "x-workspace-id: $WORKSPACE_ID")

If the token expires, obtain a new token using POST /api/auth/refresh-token (header X-Refresh-Token) or GET /api/auth/get-token, then set it again.

Create a 1:1 Transfer

This is the core flow for sending files directly from the source device to the target device.

Check the Source and Target Devices

Use GET /api/device to retrieve the device list and identify the deviceId of the device that will send the files (source) and the device that will receive them (target).

code
1
2
curl -s "$BASE_URL/api/device?page=1&size=20" "${AUTH[@]}"

Check Connectivity

Checking that both the source and target devices are online before starting the transfer can reduce failures.

code
1
2
curl -s "$BASE_URL/api/device/connectivity/<DEVICE_ID>" "${AUTH[@]}"

Browse the Source Path

Skip this step if the file path to be sent is already known. If the path needs to be found dynamically, browse the path on the source device.

code
1
2
3
4
curl -s -X POST "$BASE_URL/api/explorer/fileSearchV3/<DEVICE_ID>" "${AUTH[@]}" \ -H "Content-Type: application/json" \ -d '{"path":"C:/data/export","onlyFolder":false}'

Specify the Target Path

Specify the target storage path as a plain-text absolute path based on the target device.

code
1
2
TARGET_PATH="C:/Users/innorix/Downloads/New folder (3)"

For Windows paths, it is safer to use forward slashes (/) instead of backslashes (\).

Create the Transfer

Create the transfer using the source/target deviceId, the file paths to send, and the target storage path (targetPath). Save the monitorId from the response because it is used for subsequent control and monitoring.

code
1
2
3
4
5
6
7
8
9
10
11
curl -s -X POST "$BASE_URL/api/transfer/manualTransfer" "${AUTH[@]}" \ -H "Content-Type: application/json" \ -d '{ "sourceId": "6901ae48ca578216fd739f78", "targetId": "690037c22d309a7bc494bc53", "targetPath": "C:/Users/innorix/Downloads/New folder (3)", "sourceItem": [ { "filePath": "C:/Users/MY PC/Downloads/100 files/New folder" } ] }'

Request body fields: sourceId (sending device), targetId (receiving device), targetPath (storage path on the target device), sourceItem (list of files/folders to send — specifying a folder path also transfers its contents).

Check Transfer Status

Check the transfer status using monitorId. Poll periodically until it is completed or fails.

code
1
2
curl -s "$BASE_URL/api/transfer/<MONITOR_ID>/status" "${AUTH[@]}"

After completion, retrieve the detailed result with GET /api/transfer-history/{monitorId}/detail?idType=monitor and file-level results with GET /api/transfer-history/{monitorId}/get-files?idType=monitor.

The exact status strings (completed, etc.) may vary depending on the environment, so check the actual response before finalizing the termination conditions.

Transfer Control

Control an in-progress transfer using monitorId.

Pause · Resume · Cancel

All three actions use PATCH requests with no request body.

code
1
2
3
4
curl -s -X PATCH "$BASE_URL/api/transfer/pause/<MONITOR_ID>" "${AUTH[@]}" curl -s -X PATCH "$BASE_URL/api/transfer/resume/<MONITOR_ID>" "${AUTH[@]}" curl -s -X PATCH "$BASE_URL/api/transfer/cancel/<MONITOR_ID>" "${AUTH[@]}"

Pass one of pause, resume, or cancel as the action.

Retry Failed Files

If some files fail during the transfer, retry only the failed files.

code
1
2
3
4
5
6
7
8
9
curl -s -X POST "$BASE_URL/api/transfer/retry-failed-files" "${AUTH[@]}" \ -H "Content-Type: application/json" \ -d '{ "monitorId": "<MONITOR_ID>", "filesRetry": [ { "filePath": "C:/data/export/file.txt", "isFolder": false } ] }'

Complete Example

This is a minimal executable script that combines login, transfer creation, and status polling. For actual integration, enhance token renewal, error handling, and retries as appropriate for your environment. (jq is required for JSON parsing.)

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/env bash set -euo pipefail BASE_URL="https://exacoola.innorix.com" WORKSPACE_ID="<WORKSPACE_ID>" # 1) Login ACCESS_TOKEN=$(curl -s -X POST "$BASE_URL/api/auth/login" \ -H "Content-Type: application/json" \ -d '{"email":"<YOUR_EMAIL>","password":"<YOUR_PASSWORD>"}' \ | jq -r '.data.user.accessToken') AUTH=(-H "Authorization: Bearer $ACCESS_TOKEN" -H "x-workspace-id: $WORKSPACE_ID") # 2) Create transfer MONITOR_ID=$(curl -s -X POST "$BASE_URL/api/transfer/manualTransfer" "${AUTH[@]}" \ -H "Content-Type: application/json" \ -d '{ "sourceId": "6901ae48ca578216fd739f78", "targetId": "690037c22d309a7bc494bc53", "targetPath": "C:/Users/innorix/Downloads/New folder (3)", "sourceItem": [ { "filePath": "C:/Users/MY PC/Downloads/100 files/New folder" } ] }' | jq -r '.data.monitorId') echo "monitorId: $MONITOR_ID" # 3) Status polling while true; do STATUS=$(curl -s "$BASE_URL/api/transfer/$MONITOR_ID/status" "${AUTH[@]}" | jq -r '.data.status') echo "transfer status: $STATUS" case "$STATUS" in completed|failed|canceled|cancelled) break ;; esac sleep 3 done

Automation

For scheduled transfers that run repeatedly at a specified time, use the automation endpoint instead of the one-time manualTransfer. The field structure differs from an immediate transfer, so first check the differences below.

ItemImmediate transfer (manualTransfer)Scheduled transfer (automation)
Source/target fieldssourceId / targetIdsenderId / receiverId
sourceItem[{"filePath": "..."}] (object array)["...", "..."] (string array)
ExecutionOnce immediatelyRepeated according to schedule

Configure the Schedule

Define the execution cycle with the schedule object. Use hour and minute for the execution time, timezone for the reference time zone, and startDate and endDate for the validity period.

code
1
2
3
4
5
6
7
8
9
{ "type": "day", "hour": "02", "minute": "00", "timezone": "Asia/Seoul", "startDate": "2026-01-01T00:00:00.000Z", "endDate": "2026-12-31T00:00:00.000Z" }

Create an Automation

Create an automation containing the schedule and transfer details (details). Use the automationId from the response for subsequent updates, deletion, and control.

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
curl -s -X POST "$BASE_URL/api/automation" "${AUTH[@]}" \ -H "Content-Type: application/json" \ -d '{ "name": "Nightly backup", "schedule": { "type": "day", "hour": "02", "minute": "00", "timezone": "Asia/Seoul", "startDate": "2026-01-01T00:00:00.000Z", "endDate": "2026-12-31T00:00:00.000Z" }, "details": [ { "sourceItem": [ "D:/Projects/data/exported_users.csv", "D:/Projects/data/sales_report.pdf" ], "targetPath": "D:/Backup/Daily_Reports", "senderId": "6901ae48ca578216fd739f78", "receiverId": "690037c22d309a7bc494bc53", "step": 1, "fileCount": 2, "folderCount": 0, "sizeCount": 0 } ] }'

Automatically Generate an Automation Name

If you do not want to specify a name manually, the server can generate one.

code
1
2
curl -s "$BASE_URL/api/automation/generateAutomationName" "${AUTH[@]}"

Pause · Resume Automation

Use the pause value to stop or restart the automation.

code
1
2
3
4
curl -s -X POST "$BASE_URL/api/automation/pause" "${AUTH[@]}" \ -H "Content-Type: application/json" \ -d '{"pause": true}'

Update · Delete Automation

Change the schedule or transfer details, or remove the automation.

code
1
2
3
4
5
6
curl -s -X PATCH "$BASE_URL/api/automation/<AUTOMATION_ID>" "${AUTH[@]}" \ -H "Content-Type: application/json" \ -d '{ "name": "Nightly backup (updated)" }' curl -s -X DELETE "$BASE_URL/api/automation/<AUTOMATION_ID>" "${AUTH[@]}"

Retrieve the automation's progress and status with GET /api/automation/{automationId}/details.

Common Issues

SymptomCause · Solution
401 UnauthorizedToken expired or header missing. Run the login step again to refresh ACCESS_TOKEN and AUTH.
Transfer does not startSource/target device is offline. Check whether both devices are online with connectivity.
File is saved in the wrong locationCheck that targetPath is an absolute path based on the target device.
Source file cannot be foundsourceItem path is not an absolute path based on the source device. Check the actual path with fileSearchV3.
Only some files failRetry only the failed files with retry-failed-files.

Development Resources and Examples

Code examples and documentation required for transfer integration are available on INNORIX GitHub, and the complete endpoint specification is available in the Swagger documentation.