Discover

Get Started

Developers

Industries

Build File Transfer Into Your Product.

애플리케이션에 API와 SDK로 INNORIX 파일 전송 기능을 추가하세요.
기존 UI와 사용자 경험은 그대로 유지하면서, 파일 전송은 INNORIX가 백그라운드에서 처리합니다.

Python

이미 등록된 두 장비 사이에서, Python 애플리케이션이 API를 호출해 1:1 전송을 생성·모니터링·제어하는 코드를 단계별로 만듭니다.

장비 등록은 끝난 상태(소스·대상 두 장비가 장비 목록에 표시됨)를 전제로 합니다. 엔드포인트의 정확한 스키마는 항상 Swagger 문서를 기준으로 확인하세요.

bash
1
pip install requests

공통 준비

모든 요청에서 재사용할 세션과 인증 헤더를 먼저 구성합니다.

Base URL과 세션 설정

requests.Session으로 공통 설정을 한 번만 지정해 재사용합니다.

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

로그인으로 토큰 발급

계정으로 로그인해 액세스 토큰을 받습니다. 응답의 data.user.accessToken을 이후 요청에 사용합니다.

python
1
2
3
4
5
6
7
8
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"]

인증 헤더 구성

발급받은 토큰과 워크스페이스 ID를 세션 기본 헤더에 넣으면, 이후 호출마다 헤더를 반복 지정할 필요가 없습니다.

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

토큰이 만료되면 POST /api/auth/refresh-token(헤더 X-Refresh-Token) 또는 GET /api/auth/get-token으로 새 토큰을 받아 다시 설정하세요.

1:1 전송 만들기

소스 장비의 파일을 대상 장비로 곧바로 보내는 핵심 흐름입니다.

소스·대상 장비 확인

GET /api/device로 장비 목록을 조회해 보낼 장비(source)와 받을 장비(target)의 deviceId를 확인합니다.

python
1
2
3
4
5
6
7
8
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"]

연결 상태 확인

전송 전에 소스·대상 두 장비가 모두 온라인인지 확인하면 실패를 줄일 수 있습니다.

python
1
2
3
4
5
6
7
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()

소스 경로 탐색

보낼 파일 경로를 코드에서 이미 안다면 건너뜁니다. 동적으로 찾아야 하면 소스 장비의 경로를 탐색합니다.

python
1
2
3
4
5
6
7
8
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()

대상 경로 지정

대상 저장 경로는 대상 장비 기준의 절대 경로를 평문 문자열로 그대로 지정합니다.

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

Windows 경로는 역슬래시(\) 대신 슬래시(/)로 통일하는 것이 안전합니다.

전송 생성

소스/대상 deviceId, 보낼 파일 경로, 대상 저장 경로(targetPath)로 전송을 만듭니다. 응답의 monitorId는 이후 제어·모니터링에 쓰이므로 저장합니다.

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"]

요청 본문 필드: sourceId(보내는 장비), targetId(받는 장비), targetPath(대상 장비의 저장 경로), sourceItem(보낼 파일/폴더 목록 — 폴더 경로를 넣으면 하위 내용이 함께 전송).

전송 상태 확인

monitorId로 진행 상태를 주기적으로 폴링합니다. 완료/실패 상태가 될 때까지 대기합니다.

python
1
2
3
4
5
6
7
8
9
10
11
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)

완료 후 상세 결과는 GET /api/transfer-history/{monitorId}/detail?idType=monitor, 파일 단위 결과는 GET /api/transfer-history/{monitorId}/get-files?idType=monitor로 조회합니다.

상태 문자열(completed 등)의 정확한 값은 환경에 따라 다를 수 있으니, 실제 응답을 로그로 확인한 뒤 종료 조건을 확정하세요.

전송 제어

진행 중인 전송을 monitorId로 제어합니다.

일시중지 · 재개 · 취소

세 동작 모두 PATCH 요청이며 본문이 없습니다.

python
1
2
3
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()

action에는 pause(일시중지), resume(재개), cancel(취소) 중 하나를 전달합니다.

실패 파일 재시도

전송이 일부 실패했을 때 실패한 파일만 다시 시도합니다.

python
1
2
3
4
5
6
7
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()

전체 예제

로그인부터 전송 생성·상태 폴링까지 묶은 최소 실행 예제입니다. 실제 통합 시 토큰 갱신·에러 처리·재시도를 상황에 맞게 보강하세요.

python
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
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)

자동화

정해진 시간에 반복 실행되는 예약 전송은 1회성 manualTransfer 대신 자동화(automation) 엔드포인트로 만듭니다. 즉시 전송과 필드 구조가 다르므로 아래 차이부터 확인하세요.

항목즉시 전송 (manualTransfer)스케줄 전송 (automation)
소스·대상 필드sourceId / targetIdsenderId / receiverId
sourceItem[{"filePath": "..."}] (객체 배열)["...", "..."] (문자열 배열)
실행즉시 1회schedule에 따라 반복

스케줄 정보 구성

실행 주기를 정의합니다. hour·minute로 실행 시각을, timezone으로 기준 시간대를, startDate·endDate로 유효 기간을 지정합니다.

python
1
2
3
4
5
6
7
8
9
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, }

typeday(매일), hour·minute는 실행 시각(예: 09, 30), timezone은 기준 시간대(예: Asia/Seoul), startDate·endDate는 ISO 8601 UTC 형식(예: 2026-01-01T00:00:00.000Z)입니다.

자동화 생성

스케줄과 전송 상세(details)를 담아 자동화를 만듭니다. 응답의 automationId로 이후 수정·삭제·제어합니다.

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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"]

사용 예:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)

자동화 이름 자동 생성

이름을 직접 정하지 않으려면 서버가 이름을 생성해 줍니다.

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

자동화 일시중지 · 재개

pause 값으로 자동화를 멈추거나 다시 시작합니다.

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

자동화 수정 · 삭제

스케줄이나 전송 상세를 바꾸거나 자동화를 제거합니다.

python
1
2
3
4
5
6
7
8
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()

자동화의 진행률·상태는 GET /api/automation/{automationId}/details로 조회합니다.

자주 겪는 문제

증상원인 · 해결
401 Unauthorized토큰 만료 또는 헤더 누락. refresh-token으로 갱신 후 set_auth() 재호출.
전송이 시작되지 않음소스/대상 장비가 오프라인. connectivity로 두 장비 온라인 여부 확인.
파일이 엉뚱한 위치에 저장됨targetPath가 대상 장비 기준 절대 경로인지 확인.
소스 파일을 못 찾음sourceItem 경로가 소스 장비 기준 절대 경로가 아님. fileSearchV3로 실제 경로 확인.
일부 파일만 실패retry-failed-files로 실패 파일만 재시도.

개발 리소스 및 예제

전송 연동에 필요한 코드 예제와 문서는 INNORIX GitHub에서, 전체 엔드포인트 명세는 Swagger 문서에서 확인할 수 있습니다.


Node.js

이미 등록된 두 장비 사이에서, Node.js 애플리케이션이 API를 호출해 1:1 전송을 생성·모니터링·제어하는 코드를 단계별로 만듭니다.

장비 등록은 끝난 상태(소스·대상 두 장비가 장비 목록에 표시됨)를 전제로 합니다. 엔드포인트의 정확한 스키마는 항상 Swagger 문서를 기준으로 확인하세요.

Node.js 18 이상이면 내장 fetch를 그대로 사용합니다(별도 설치 불필요).

bash
1
node --version

공통 준비

모든 요청에서 재사용할 상수와 인증 헤더를 먼저 구성합니다.

Base URL과 상수 설정

Base URL·워크스페이스 ID와, 로그인 후 채워질 토큰 변수를 선언합니다.

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

로그인으로 토큰 발급

계정으로 로그인해 액세스 토큰을 받습니다. 응답의 data.user.accessToken을 이후 요청에 사용합니다.

javascript
1
2
3
4
5
6
7
8
9
10
11
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; }

인증 헤더 구성

토큰과 워크스페이스 ID를 담은 헤더를 함수로 만들어 이후 호출에서 재사용합니다.

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

토큰이 만료되면 POST /api/auth/refresh-token(헤더 X-Refresh-Token) 또는 GET /api/auth/get-token으로 새 토큰을 받아 다시 설정하세요.

1:1 전송 만들기

소스 장비의 파일을 대상 장비로 곧바로 보내는 핵심 흐름입니다.

소스·대상 장비 확인

GET /api/device로 장비 목록을 조회해 보낼 장비(source)와 받을 장비(target)의 deviceId를 확인합니다.

javascript
1
2
3
4
5
6
7
8
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; }

연결 상태 확인

전송 전에 소스·대상 두 장비가 모두 온라인인지 확인하면 실패를 줄일 수 있습니다.

javascript
1
2
3
4
5
6
7
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(); }

소스 경로 탐색

보낼 파일 경로를 코드에서 이미 안다면 건너뜁니다. 동적으로 찾아야 하면 소스 장비의 경로를 탐색합니다.

javascript
1
2
3
4
5
6
7
8
9
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(); }

대상 경로 지정

대상 저장 경로는 대상 장비 기준의 절대 경로를 평문 문자열로 그대로 지정합니다.

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

Windows 경로는 역슬래시(\) 대신 슬래시(/)로 통일하는 것이 안전합니다.

전송 생성

소스/대상 deviceId, 보낼 파일 경로, 대상 저장 경로(targetPath)로 전송을 만듭니다. 응답의 monitorId는 이후 제어·모니터링에 쓰이므로 저장합니다.

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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; }

요청 본문 필드: sourceId(보내는 장비), targetId(받는 장비), targetPath(대상 장비의 저장 경로), sourceItem(보낼 파일/폴더 목록 — 폴더 경로를 넣으면 하위 내용이 함께 전송).

전송 상태 확인

monitorId로 진행 상태를 주기적으로 폴링합니다. 완료/실패 상태가 될 때까지 대기합니다.

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)); } }

완료 후 상세 결과는 GET /api/transfer-history/{monitorId}/detail?idType=monitor, 파일 단위 결과는 GET /api/transfer-history/{monitorId}/get-files?idType=monitor로 조회합니다.

상태 문자열(completed 등)의 정확한 값은 환경에 따라 다를 수 있으니, 실제 응답을 로그로 확인한 뒤 종료 조건을 확정하세요.

전송 제어

진행 중인 전송을 monitorId로 제어합니다.

일시중지 · 재개 · 취소

세 동작 모두 PATCH 요청이며 본문이 없습니다.

javascript
1
2
3
4
5
6
7
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}`); }

action에는 pause(일시중지), resume(재개), cancel(취소) 중 하나를 전달합니다.

실패 파일 재시도

전송이 일부 실패했을 때 실패한 파일만 다시 시도합니다.

javascript
1
2
3
4
5
6
7
8
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}`); }

전체 예제

로그인부터 전송 생성·상태 폴링까지 묶은 최소 실행 예제입니다. 실제 통합 시 토큰 갱신·에러 처리·재시도를 상황에 맞게 보강하세요.

javascript
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
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); })();

자동화

정해진 시간에 반복 실행되는 예약 전송은 1회성 manualTransfer 대신 자동화(automation) 엔드포인트로 만듭니다. 즉시 전송과 필드 구조가 다르므로 아래 차이부터 확인하세요.

항목즉시 전송 (manualTransfer)스케줄 전송 (automation)
소스·대상 필드sourceId / targetIdsenderId / receiverId
sourceItem[{"filePath": "..."}] (객체 배열)["...", "..."] (문자열 배열)
실행즉시 1회schedule에 따라 반복

스케줄 정보 구성

실행 주기를 정의합니다. hour·minute로 실행 시각을, timezone으로 기준 시간대를, startDate·endDate로 유효 기간을 지정합니다.

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

typeday(매일), hour·minute는 실행 시각(예: 09, 30), timezone은 기준 시간대(예: Asia/Seoul), startDate·endDate는 ISO 8601 UTC 형식(예: 2026-01-01T00:00:00.000Z)입니다.

자동화 생성

스케줄과 전송 상세(details)를 담아 자동화를 만듭니다. 응답의 automationId로 이후 수정·삭제·제어합니다.

javascript
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
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; }

사용 예:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);

자동화 이름 자동 생성

이름을 직접 정하지 않으려면 서버가 이름을 생성해 줍니다.

javascript
1
2
3
4
5
6
7
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 값으로 자동화를 멈추거나 다시 시작합니다.

javascript
1
2
3
4
5
6
7
8
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}`); }

자동화 수정 · 삭제

스케줄이나 전송 상세를 바꾸거나 자동화를 제거합니다.

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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}`); }

자동화의 진행률·상태는 GET /api/automation/{automationId}/details로 조회합니다.

자주 겪는 문제

증상원인 · 해결
401 Unauthorized토큰 만료 또는 헤더 누락. refresh-token으로 갱신 후 accessToken 재설정.
전송이 시작되지 않음소스/대상 장비가 오프라인. connectivity로 두 장비 온라인 여부 확인.
파일이 엉뚱한 위치에 저장됨targetPath가 대상 장비 기준 절대 경로인지 확인.
소스 파일을 못 찾음sourceItem 경로가 소스 장비 기준 절대 경로가 아님. fileSearchV3로 실제 경로 확인.
일부 파일만 실패retry-failed-files로 실패 파일만 재시도.

개발 리소스 및 예제

전송 연동에 필요한 코드 예제와 문서는 INNORIX GitHub에서, 전체 엔드포인트 명세는 Swagger 문서에서 확인할 수 있습니다.


Java

이미 등록된 두 장비 사이에서, Java 애플리케이션이 API를 호출해 1:1 전송을 생성·모니터링·제어하는 코드를 단계별로 만듭니다.

장비 등록은 끝난 상태(소스·대상 두 장비가 장비 목록에 표시됨)를 전제로 합니다. 엔드포인트의 정확한 스키마는 항상 Swagger 문서를 기준으로 확인하세요.

Java 11+ 의 내장 HttpClient(java.net.http)를 사용하고, JSON 처리는 Jackson을 사용합니다(Maven 의존성).

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

공통 준비

모든 요청에서 재사용할 클라이언트와 인증 헤더를 먼저 구성합니다. 이후 각 단계의 메서드는 이 ExacoolaClient 클래스에 속합니다.

Base URL과 클라이언트 설정

HttpClient와 Jackson ObjectMapper를 한 번만 만들어 재사용합니다.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 = ""; }

로그인으로 토큰 발급

계정으로 로그인해 액세스 토큰을 받습니다. 응답의 data.user.accessToken을 이후 요청에 사용합니다.

java
1
2
3
4
5
6
7
8
9
10
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; }

인증 헤더 구성

토큰과 워크스페이스 ID가 담긴 요청 빌더를 만드는 헬퍼입니다. 이후 모든 호출에서 재사용합니다.

java
1
2
3
4
5
6
7
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)); }

토큰이 만료되면 POST /api/auth/refresh-token(헤더 X-Refresh-Token) 또는 GET /api/auth/get-token으로 새 토큰을 받아 다시 설정하세요.

1:1 전송 만들기

소스 장비의 파일을 대상 장비로 곧바로 보내는 핵심 흐름입니다.

소스·대상 장비 확인

GET /api/device로 장비 목록을 조회해 보낼 장비(source)와 받을 장비(target)의 deviceId를 확인합니다.

java
1
2
3
4
5
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"); }

연결 상태 확인

전송 전에 소스·대상 두 장비가 모두 온라인인지 확인하면 실패를 줄일 수 있습니다.

java
1
2
3
4
5
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()); }

소스 경로 탐색

보낼 파일 경로를 코드에서 이미 안다면 건너뜁니다. 동적으로 찾아야 하면 소스 장비의 경로를 탐색합니다.

java
1
2
3
4
5
6
7
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()); }

대상 경로 지정

대상 저장 경로는 대상 장비 기준의 절대 경로를 평문 문자열로 그대로 지정합니다.

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

Windows 경로는 역슬래시(\) 대신 슬래시(/)로 통일하는 것이 안전합니다.

전송 생성

소스/대상 deviceId, 보낼 파일 경로, 대상 저장 경로(targetPath)로 전송을 만듭니다. 응답의 monitorId는 이후 제어·모니터링에 쓰이므로 저장합니다.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(); }

요청 본문 필드: sourceId(보내는 장비), targetId(받는 장비), targetPath(대상 장비의 저장 경로), sourceItem(보낼 파일/폴더 목록 — 폴더 경로를 넣으면 하위 내용이 함께 전송).

전송 상태 확인

monitorId로 진행 상태를 주기적으로 폴링합니다. 완료/실패 상태가 될 때까지 대기합니다.

java
1
2
3
4
5
6
7
8
9
10
11
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); } }

완료 후 상세 결과는 GET /api/transfer-history/{monitorId}/detail?idType=monitor, 파일 단위 결과는 GET /api/transfer-history/{monitorId}/get-files?idType=monitor로 조회합니다.

상태 문자열(completed 등)의 정확한 값은 환경에 따라 다를 수 있으니, 실제 응답을 로그로 확인한 뒤 종료 조건을 확정하세요.

전송 제어

진행 중인 전송을 monitorId로 제어합니다.

일시중지 · 재개 · 취소

세 동작 모두 PATCH 요청이며 본문이 없습니다.

java
1
2
3
4
5
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()); }

action에는 pause(일시중지), resume(재개), cancel(취소) 중 하나를 전달합니다.

실패 파일 재시도

전송이 일부 실패했을 때 실패한 파일만 다시 시도합니다.

java
1
2
3
4
5
6
7
8
9
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()); }

전체 예제

로그인부터 전송 생성·상태 폴링까지 묶은 최소 실행 예제입니다. 실제 통합 시 토큰 갱신·에러 처리·재시도를 상황에 맞게 보강하세요.

java
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
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); } }

자동화

정해진 시간에 반복 실행되는 예약 전송은 1회성 manualTransfer 대신 자동화(automation) 엔드포인트로 만듭니다. 즉시 전송과 필드 구조가 다르므로 아래 차이부터 확인하세요.

항목즉시 전송 (manualTransfer)스케줄 전송 (automation)
소스·대상 필드sourceId / targetIdsenderId / receiverId
sourceItem[{"filePath": "..."}] (객체 배열)["...", "..."] (문자열 배열)
실행즉시 1회schedule에 따라 반복

스케줄 정보 구성

실행 주기를 정의합니다. hour·minute로 실행 시각을, timezone으로 기준 시간대를, startDate·endDate로 유효 기간을 지정합니다.

java
1
2
3
4
5
6
7
8
9
10
11
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; }

typeday(매일), hour·minute는 실행 시각(예: 09, 30), timezone은 기준 시간대(예: Asia/Seoul), startDate·endDate는 ISO 8601 UTC 형식(예: 2026-01-01T00:00:00.000Z)입니다.

자동화 생성

스케줄과 전송 상세(details)를 담아 자동화를 만듭니다. 응답의 automationId로 이후 수정·삭제·제어합니다.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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(); }

사용 예:

java
1
2
3
4
5
6
7
8
9
10
11
12
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);

자동화 이름 자동 생성

이름을 직접 정하지 않으려면 서버가 이름을 생성해 줍니다.

java
1
2
3
4
5
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 값으로 자동화를 멈추거나 다시 시작합니다.

java
1
2
3
4
5
6
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()); }

자동화 수정 · 삭제

스케줄이나 전송 상세를 바꾸거나 자동화를 제거합니다.

java
1
2
3
4
5
6
7
8
9
10
11
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()); }

자동화의 진행률·상태는 GET /api/automation/{automationId}/details로 조회합니다.

자주 겪는 문제

증상원인 · 해결
401 Unauthorized토큰 만료 또는 헤더 누락. refresh-token으로 갱신 후 accessToken 재설정.
전송이 시작되지 않음소스/대상 장비가 오프라인. connectivity로 두 장비 온라인 여부 확인.
파일이 엉뚱한 위치에 저장됨targetPath가 대상 장비 기준 절대 경로인지 확인.
소스 파일을 못 찾음sourceItem 경로가 소스 장비 기준 절대 경로가 아님. fileSearchV3로 실제 경로 확인.
일부 파일만 실패retry-failed-files로 실패 파일만 재시도.

개발 리소스 및 예제

전송 연동에 필요한 코드 예제와 문서는 INNORIX GitHub에서, 전체 엔드포인트 명세는 Swagger 문서에서 확인할 수 있습니다.


C#

이미 등록된 두 장비 사이에서, C#/.NET 애플리케이션이 API를 호출해 1:1 전송을 생성·모니터링·제어하는 코드를 단계별로 만듭니다.

장비 등록은 끝난 상태(소스·대상 두 장비가 장비 목록에 표시됨)를 전제로 합니다. 엔드포인트의 정확한 스키마는 항상 Swagger 문서를 기준으로 확인하세요.

.NET 8 기준이며 HttpClient와 내장 System.Net.Http.Json을 사용합니다. PatchAsJsonAsync는 .NET 7 이상에서 제공됩니다.

공통 준비

모든 요청에서 재사용할 클라이언트와 인증 헤더를 먼저 구성합니다. 이후 각 단계의 메서드는 이 ExacoolaClient 클래스에 속합니다.

Base URL과 클라이언트 설정

HttpClientBaseAddress와 함께 한 번만 만들어 재사용합니다.

csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 = ""; }

로그인으로 토큰 발급

계정으로 로그인해 액세스 토큰을 받습니다. 응답의 data.user.accessToken을 이후 요청에 사용합니다.

csharp
1
2
3
4
5
6
7
8
9
10
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; }

인증 헤더 구성

토큰과 워크스페이스 ID를 HttpClient 기본 헤더에 넣으면, 이후 호출마다 헤더를 반복 지정할 필요가 없습니다.

csharp
1
2
3
4
5
6
7
8
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); }

토큰이 만료되면 POST /api/auth/refresh-token(헤더 X-Refresh-Token) 또는 GET /api/auth/get-token으로 새 토큰을 받아 다시 설정하세요.

1:1 전송 만들기

소스 장비의 파일을 대상 장비로 곧바로 보내는 핵심 흐름입니다.

소스·대상 장비 확인

GET /api/device로 장비 목록을 조회해 보낼 장비(source)와 받을 장비(target)의 deviceId를 확인합니다.

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

연결 상태 확인

전송 전에 소스·대상 두 장비가 모두 온라인인지 확인하면 실패를 줄일 수 있습니다.

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

소스 경로 탐색

보낼 파일 경로를 코드에서 이미 안다면 건너뜁니다. 동적으로 찾아야 하면 소스 장비의 경로를 탐색합니다.

csharp
1
2
3
4
5
6
7
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>(); }

대상 경로 지정

대상 저장 경로는 대상 장비 기준의 절대 경로를 평문 문자열로 그대로 지정합니다.

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

Windows 경로는 역슬래시(\) 대신 슬래시(/)로 통일하는 것이 안전합니다.

전송 생성

소스/대상 deviceId, 보낼 파일 경로, 대상 저장 경로(targetPath)로 전송을 만듭니다. 응답의 monitorId는 이후 제어·모니터링에 쓰이므로 저장합니다.

csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()!; }

요청 본문 필드: sourceId(보내는 장비), targetId(받는 장비), targetPath(대상 장비의 저장 경로), sourceItem(보낼 파일/폴더 목록 — 폴더 경로를 넣으면 하위 내용이 함께 전송).

전송 상태 확인

monitorId로 진행 상태를 주기적으로 폴링합니다. 완료/실패 상태가 될 때까지 대기합니다.

csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
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); } }

완료 후 상세 결과는 GET /api/transfer-history/{monitorId}/detail?idType=monitor, 파일 단위 결과는 GET /api/transfer-history/{monitorId}/get-files?idType=monitor로 조회합니다.

상태 문자열(completed 등)의 정확한 값은 환경에 따라 다를 수 있으니, 실제 응답을 로그로 확인한 뒤 종료 조건을 확정하세요.

전송 제어

진행 중인 전송을 monitorId로 제어합니다.

일시중지 · 재개 · 취소

세 동작 모두 PATCH 요청이며 본문이 없습니다.

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

action에는 pause(일시중지), resume(재개), cancel(취소) 중 하나를 전달합니다.

실패 파일 재시도

전송이 일부 실패했을 때 실패한 파일만 다시 시도합니다.

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

전체 예제

로그인부터 전송 생성·상태 폴링까지 묶은 최소 실행 예제입니다. 실제 통합 시 토큰 갱신·에러 처리·재시도를 상황에 맞게 보강하세요.

csharp
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
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}"); } }

자동화

정해진 시간에 반복 실행되는 예약 전송은 1회성 manualTransfer 대신 자동화(automation) 엔드포인트로 만듭니다. 즉시 전송과 필드 구조가 다르므로 아래 차이부터 확인하세요.

항목즉시 전송 (manualTransfer)스케줄 전송 (automation)
소스·대상 필드sourceId / targetIdsenderId / receiverId
sourceItem[{"filePath": "..."}] (객체 배열)["...", "..."] (문자열 배열)
실행즉시 1회schedule에 따라 반복

스케줄 정보 구성

실행 주기를 정의합니다. hour·minute로 실행 시각을, timezone으로 기준 시간대를, startDate·endDate로 유효 기간을 지정합니다.

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

typeday(매일), hour·minute는 실행 시각(예: 09, 30), timezone은 기준 시간대(예: Asia/Seoul), startDate·endDate는 ISO 8601 UTC 형식(예: 2026-01-01T00:00:00.000Z)입니다.

자동화 생성

스케줄과 전송 상세(details)를 담아 자동화를 만듭니다. 응답의 automationId로 이후 수정·삭제·제어합니다.

csharp
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
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()!; }

사용 예:

csharp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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}");

자동화 이름 자동 생성

이름을 직접 정하지 않으려면 서버가 이름을 생성해 줍니다.

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

자동화 일시중지 · 재개

pause 값으로 자동화를 멈추거나 다시 시작합니다.

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

자동화 수정 · 삭제

스케줄이나 전송 상세를 바꾸거나 자동화를 제거합니다.

csharp
1
2
3
4
5
6
7
8
9
10
11
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(); }

자동화의 진행률·상태는 GET /api/automation/{automationId}/details로 조회합니다.

자주 겪는 문제

증상원인 · 해결
401 Unauthorized토큰 만료 또는 헤더 누락. refresh-token으로 갱신 후 SetAuth() 재호출.
전송이 시작되지 않음소스/대상 장비가 오프라인. connectivity로 두 장비 온라인 여부 확인.
파일이 엉뚱한 위치에 저장됨targetPath가 대상 장비 기준 절대 경로인지 확인.
소스 파일을 못 찾음sourceItem 경로가 소스 장비 기준 절대 경로가 아님. fileSearchV3로 실제 경로 확인.
일부 파일만 실패retry-failed-files로 실패 파일만 재시도.

개발 리소스 및 예제

전송 연동에 필요한 코드 예제와 문서는 INNORIX GitHub에서, 전체 엔드포인트 명세는 Swagger 문서에서 확인할 수 있습니다.


curl

이미 등록된 두 장비 사이에서, curl로 API를 호출해 1:1 전송을 생성·모니터링·제어하는 방법을 단계별로 정리합니다.

장비 등록은 끝난 상태(소스·대상 두 장비가 장비 목록에 표시됨)를 전제로 합니다. 엔드포인트의 정확한 스키마는 항상 Swagger 문서를 기준으로 확인하세요.

curl은 대부분의 환경에 기본 제공됩니다. 전체 예제 스크립트는 JSON 파싱에 jq가 필요합니다(sudo apt install jq 또는 brew install jq).

공통 준비

이후 모든 요청에서 재사용할 변수와 인증 헤더를 먼저 구성합니다. (아래 예시는 하나의 셸 세션에서 실행한다고 가정합니다.)

Base URL과 변수 설정

Base URL과 워크스페이스 ID를 셸 변수로 지정합니다.

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

로그인으로 토큰 발급

계정으로 로그인해 액세스 토큰을 받습니다. 응답의 data.user.accessToken을 변수에 저장해 이후 요청에 사용합니다.

bash
1
2
3
4
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[@]}"로 재사용할 수 있습니다.

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

토큰이 만료되면 POST /api/auth/refresh-token(헤더 X-Refresh-Token) 또는 GET /api/auth/get-token으로 새 토큰을 받아 다시 설정하세요.

1:1 전송 만들기

소스 장비의 파일을 대상 장비로 곧바로 보내는 핵심 흐름입니다.

소스·대상 장비 확인

GET /api/device로 장비 목록을 조회해 보낼 장비(source)와 받을 장비(target)의 deviceId를 확인합니다.

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

연결 상태 확인

전송 전에 소스·대상 두 장비가 모두 온라인인지 확인하면 실패를 줄일 수 있습니다.

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

소스 경로 탐색

보낼 파일 경로를 이미 안다면 건너뜁니다. 동적으로 찾아야 하면 소스 장비의 경로를 탐색합니다.

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

대상 경로 지정

대상 저장 경로는 대상 장비 기준의 절대 경로를 평문 문자열로 그대로 지정합니다.

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

Windows 경로는 역슬래시(\) 대신 슬래시(/)로 통일하는 것이 안전합니다.

전송 생성

소스/대상 deviceId, 보낼 파일 경로, 대상 저장 경로(targetPath)로 전송을 만듭니다. 응답의 monitorId는 이후 제어·모니터링에 쓰이므로 저장합니다.

bash
1
2
3
4
5
6
7
8
9
10
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" } ] }'

요청 본문 필드: sourceId(보내는 장비), targetId(받는 장비), targetPath(대상 장비의 저장 경로), sourceItem(보낼 파일/폴더 목록 — 폴더 경로를 넣으면 하위 내용이 함께 전송).

전송 상태 확인

monitorId로 진행 상태를 조회합니다. 완료/실패가 될 때까지 주기적으로 폴링합니다.

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

완료 후 상세 결과는 GET /api/transfer-history/{monitorId}/detail?idType=monitor, 파일 단위 결과는 GET /api/transfer-history/{monitorId}/get-files?idType=monitor로 조회합니다.

상태 문자열(completed 등)의 정확한 값은 환경에 따라 다를 수 있으니, 실제 응답을 확인한 뒤 종료 조건을 확정하세요.

전송 제어

진행 중인 전송을 monitorId로 제어합니다.

일시중지 · 재개 · 취소

세 동작 모두 PATCH 요청이며 본문이 없습니다.

bash
1
2
3
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[@]}"

action에는 pause(일시중지), resume(재개), cancel(취소) 중 하나를 전달합니다.

실패 파일 재시도

전송이 일부 실패했을 때 실패한 파일만 다시 시도합니다.

bash
1
2
3
4
5
6
7
8
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 } ] }'

전체 예제

로그인부터 전송 생성·상태 폴링까지 묶은 최소 실행 스크립트입니다. 실제 통합 시 토큰 갱신·에러 처리·재시도를 상황에 맞게 보강하세요. (JSON 파싱에 jq가 필요합니다.)

bash
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
#!/usr/bin/env bash set -euo pipefail BASE_URL="https://exacoola.innorix.com" WORKSPACE_ID="<WORKSPACE_ID>" # 1) 로그인 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) 전송 생성 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) 상태 폴링 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

자동화

정해진 시간에 반복 실행되는 예약 전송은 1회성 manualTransfer 대신 자동화(automation) 엔드포인트로 만듭니다. 즉시 전송과 필드 구조가 다르므로 아래 차이부터 확인하세요.

항목즉시 전송 (manualTransfer)스케줄 전송 (automation)
소스·대상 필드sourceId / targetIdsenderId / receiverId
sourceItem[{"filePath": "..."}] (객체 배열)["...", "..."] (문자열 배열)
실행즉시 1회schedule에 따라 반복

스케줄 정보 구성

schedule 객체로 실행 주기를 정의합니다. hour·minute로 실행 시각을, timezone으로 기준 시간대를, startDate·endDate로 유효 기간을 지정합니다.

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

자동화 생성

스케줄과 전송 상세(details)를 담아 자동화를 만듭니다. 응답의 automationId로 이후 수정·삭제·제어합니다.

bash
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 } ] }'

자동화 이름 자동 생성

이름을 직접 정하지 않으려면 서버가 이름을 생성해 줍니다.

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

자동화 일시중지 · 재개

pause 값으로 자동화를 멈추거나 다시 시작합니다.

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

자동화 수정 · 삭제

스케줄이나 전송 상세를 바꾸거나 자동화를 제거합니다.

bash
1
2
3
4
5
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[@]}"

자동화의 진행률·상태는 GET /api/automation/{automationId}/details로 조회합니다.

자주 겪는 문제

증상원인 · 해결
401 Unauthorized토큰 만료 또는 헤더 누락. 로그인 단계를 다시 실행해 ACCESS_TOKEN·AUTH를 갱신.
전송이 시작되지 않음소스/대상 장비가 오프라인. connectivity로 두 장비 온라인 여부 확인.
파일이 엉뚱한 위치에 저장됨targetPath가 대상 장비 기준 절대 경로인지 확인.
소스 파일을 못 찾음sourceItem 경로가 소스 장비 기준 절대 경로가 아님. fileSearchV3로 실제 경로 확인.
일부 파일만 실패retry-failed-files로 실패 파일만 재시도.

개발 리소스 및 예제

전송 연동에 필요한 코드 예제와 문서는 INNORIX GitHub에서, 전체 엔드포인트 명세는 Swagger 문서에서 확인할 수 있습니다.