본 문서는 INNORIX Public API v2.0.0 기준입니다. 모든 응답은 statusCode · message · data · isCached 형식이며, status_code(snake_case)는 하위호환을 위해 남아 있지만 statusCode 사용을 권장합니다.
API 인증#
애플리케이션이 INNORIX와 통신하려면 먼저 인증해야 합니다.
로그인과 토큰#
설명#
계정으로 로그인해 액세스 토큰(JWT) 을 발급받고, 이후 모든 요청에 Authorization: Bearer와 x-workspace-id 헤더를 함께 보냅니다. 토큰이 만료되면 POST /api/auth/token/refresh(헤더 X-Refresh-Token, 본문 없음)로 갱신합니다. 명령 자동화용 장기 키가 필요하면 POST /api/auth/api-keys(본문 없음)로 발급해 x-api-key 헤더로 사용합니다. 사용자당 활성 API 키는 1개이며, 재호출 시 동일한 키가 반환됩니다.
사용 API#
| 목적 | Method | Endpoint |
|---|---|---|
| 로그인 | POST | /api/auth/login |
| 토큰 갱신 | POST | /api/auth/token/refresh |
| API 키 발급 | POST | /api/auth/api-keys |
| 현재 사용자 조회 | GET | /api/auth/me |
Request#
POST /api/auth/login
{
"email": "<YOUR_EMAIL>",
"password": "<YOUR_PASSWORD>"
}Response#
{
"statusCode": 200,
"message": "success",
"data": {
"user": {
"email": "user@example.com",
"userName": "User Name",
"userId": "usr_abc123",
"accessToken": "<ACCESS_TOKEN>",
"refreshToken": "<REFRESH_TOKEN>"
}
},
"isCached": false
}처리 순서#
POST /api/auth/login으로 로그인 →data.user.accessToken수신- 이후 요청에
Authorization: Bearer <ACCESS_TOKEN>와x-workspace-id헤더 부착 - 토큰 만료 시
POST /api/auth/token/refresh(헤더X-Refresh-Token)로 갱신 →data.accessToken·data.refreshToken·data.expiresIn수신
구현 예제#
BASE_URL="https://app.innorix.com"
WORKSPACE_ID="<WORKSPACE_ID>"
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")const BASE_URL = "https://app.innorix.com";
const WORKSPACE_ID = "<WORKSPACE_ID>";
let accessToken = "";
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;
}
function authHeaders() {
return {
"Authorization": `Bearer ${accessToken}`,
"x-workspace-id": WORKSPACE_ID,
"Content-Type": "application/json",
};
}import time
import requests
BASE_URL = "https://app.innorix.com"
WORKSPACE_ID = "<WORKSPACE_ID>"
session = requests.Session()
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"]
def set_auth(access_token: str) -> None:
session.headers.update({
"Authorization": f"Bearer {access_token}",
"x-workspace-id": WORKSPACE_ID,
"Content-Type": "application/json",
})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 InnorixClient {
static final String BASE_URL = "https://app.innorix.com";
static final String WORKSPACE_ID = "<WORKSPACE_ID>";
final HttpClient http = HttpClient.newHttpClient();
final ObjectMapper mapper = new ObjectMapper();
String accessToken = "";
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;
}
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));
}
}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 InnorixClient
{
const string BaseUrl = "https://app.innorix.com";
const string WorkspaceId = "<WORKSPACE_ID>";
readonly HttpClient http = new() { BaseAddress = new Uri(BaseUrl) };
string accessToken = "";
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;
}
void SetAuth(string token)
{
accessToken = token;
http.DefaultRequestHeaders.Remove("Authorization");
http.DefaultRequestHeaders.Add("Authorization", quot;Bearer {token}");
http.DefaultRequestHeaders.Remove("x-workspace-id");
http.DefaultRequestHeaders.Add("x-workspace-id", WorkspaceId);
}
}시스템 연결#
파일 전송은 연결된 장비(device) 사이에서 일어납니다.
디바이스 확인과 파일 탐색#
설명#
장비 목록을 조회하거나, 이름·IP·MAC을 GET /api/devices/resolve로 정확한 deviceId로 변환합니다. 즉시 전송(POST /api/transfers/manual)은 소스·대상에 deviceId뿐 아니라 이름·IP도 그대로 받고 평문 경로를 전송하므로, 대부분의 경우 별도 탐색 없이 바로 전송할 수 있습니다. 필요할 때만 POST /api/devices/{deviceId}/files/search로 경로를 배치 탐색합니다. 이 탐색은 커서 기반 페이지네이션으로, 응답의 nextCursor를 GET /api/devices/{deviceId}/files/search?cursor=에 넘겨 hasMore가 false가 될 때까지 반복합니다. (에이전트 설치·등록은 완료된 상태를 전제로 하며, 설치된 에이전트가 서버에 연결되면 장비 목록에 나타납니다.)
사용 API#
| 목적 | Method | Endpoint |
|---|---|---|
| 디바이스 목록 | GET | /api/devices |
| 디바이스 확인(이름·IP·MAC → ID) | GET | /api/devices/resolve |
| 연결 상태 조회 | GET | /api/devices/{deviceId}/connectivity |
| 파일 검색 시작 | POST | /api/devices/{deviceId}/files/search |
| 파일 검색 다음 배치 | GET | /api/devices/{deviceId}/files/search?cursor= |
Request#
POST /api/devices/{deviceId}/files/search
{
"path": "C:/data/export",
"pageSize": 500
}Response#
GET /api/devices/resolve?name=seoul-node-01
{
"statusCode": 200,
"message": "success",
"data": {
"matchCount": 1,
"devices": [
{ "deviceId": "6901ae48ca578216fd739f78", "name": "seoul-node-01", "os": "linux", "status": 1 }
]
}
}GET /api/devices/{deviceId}/connectivity
{
"statusCode": 200,
"message": "success",
"data": { "deviceId": "6901ae48ca578216fd739f78", "state": 1, "isConnected": true }
}POST /api/devices/{deviceId}/files/search
{
"statusCode": 200,
"message": "OK",
"data": {
"searchId": "srch_9f2a3c",
"items": [
{ "name": "report.pdf", "path": "C:/data/export/report.pdf", "type": "file", "size": 20480 }
],
"count": 1,
"hasMore": false,
"nextCursor": null
}
}처리 순서#
GET /api/devices또는GET /api/devices/resolve로 소스·대상deviceId확인 (즉시 전송에는 이름·IP도 그대로 사용 가능)GET /api/devices/{deviceId}/connectivity로 두 장비isConnected여부 확인- (선택)
POST /api/devices/{deviceId}/files/search({path, pageSize})로 첫 배치 조회 →nextCursor·hasMore확인,GET ...?cursor=로 반복. 자동화의sourceItem은{deviceId}_ino_{base64(path)}토큰이 필요합니다(아래 일정 자동화 참고).
구현 예제#
# --- List devices ---
curl -s "$BASE_URL/api/devices?page=1&size=20" "${AUTH[@]}"
# --- Resolve a name / IP into an exact deviceId ---
curl -s "$BASE_URL/api/devices/resolve?name=seoul-node-01" "${AUTH[@]}"
# --- Check connectivity ---
curl -s "$BASE_URL/api/devices/<DEVICE_ID>/connectivity" "${AUTH[@]}"
# --- Browse a device path (start) ---
curl -s -X POST "$BASE_URL/api/devices/<DEVICE_ID>/files/search" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d '{"path":"C:/data/export","pageSize":500}'
# --- Browse a device path (next batch) ---
curl -s "$BASE_URL/api/devices/<DEVICE_ID>/files/search?cursor=<NEXT_CURSOR>" "${AUTH[@]}"// --- List devices ---
async function listDevices() {
const url = new URL(`${BASE_URL}/api/devices`);
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.devices;
}
// --- Resolve a name / IP into an exact deviceId ---
async function resolveDevice(name) {
const url = new URL(`${BASE_URL}/api/devices/resolve`);
url.searchParams.set("name", name);
const res = await fetch(url, { headers: authHeaders() });
if (!res.ok) throw new Error(`resolve failed: ${res.status}`);
const data = (await res.json()).data;
return (data.devices?.[0]?.deviceId) ?? data.deviceId;
}
// --- Check connectivity ---
async function isOnline(deviceId) {
const res = await fetch(`${BASE_URL}/api/devices/${deviceId}/connectivity`, {
headers: authHeaders(),
});
if (!res.ok) throw new Error(`connectivity failed: ${res.status}`);
return (await res.json()).data.isConnected;
}
// --- Browse a device path (cursor paging) ---
async function browse(deviceId, path, pageSize = 500) {
const items = [];
let res = await fetch(`${BASE_URL}/api/devices/${deviceId}/files/search`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ path, pageSize }),
});
if (!res.ok) throw new Error(`browse failed: ${res.status}`);
let page = (await res.json()).data;
items.push(...page.items);
while (page.hasMore) {
const url = new URL(`${BASE_URL}/api/devices/${deviceId}/files/search`);
url.searchParams.set("cursor", page.nextCursor);
res = await fetch(url, { headers: authHeaders() });
if (!res.ok) throw new Error(`browse failed: ${res.status}`);
page = (await res.json()).data;
items.push(...page.items);
}
return items;
}# --- List devices ---
def list_devices() -> list:
res = session.get(
f"{BASE_URL}/api/devices",
params={"page": 1, "size": 20},
timeout=10,
)
res.raise_for_status()
return res.json()["data"]["devices"]
# --- Resolve a name / IP into an exact deviceId ---
def resolve_device(name: str) -> str:
res = session.get(f"{BASE_URL}/api/devices/resolve", params={"name": name}, timeout=10)
res.raise_for_status()
data = res.json()["data"]
devices = data.get("devices") or []
return devices[0]["deviceId"] if devices else data.get("deviceId")
# --- Check connectivity ---
def is_online(device_id: str) -> bool:
res = session.get(
f"{BASE_URL}/api/devices/{device_id}/connectivity",
timeout=10,
)
res.raise_for_status()
return res.json()["data"]["isConnected"]
# --- Browse a device path (cursor paging) ---
def browse(device_id: str, path: str, page_size: int = 500) -> list:
items = []
res = session.post(
f"{BASE_URL}/api/devices/{device_id}/files/search",
json={"path": path, "pageSize": page_size},
timeout=15,
)
res.raise_for_status()
page = res.json()["data"]
items.extend(page["items"])
while page["hasMore"]:
res = session.get(
f"{BASE_URL}/api/devices/{device_id}/files/search",
params={"cursor": page["nextCursor"]},
timeout=15,
)
res.raise_for_status()
page = res.json()["data"]
items.extend(page["items"])
return items// Methods of the InnorixClient class (use with the API 인증 code)
// --- List devices ---
public JsonNode listDevices() throws Exception {
HttpRequest req = authed("/api/devices?page=1&size=20").GET().build();
HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
return mapper.readTree(res.body()).at("/data/devices");
}
// --- Resolve a name / IP into an exact deviceId ---
public String resolveDevice(String name) throws Exception {
String q = URLEncoder.encode(name, java.nio.charset.StandardCharsets.UTF_8);
HttpRequest req = authed("/api/devices/resolve?name=" + q).GET().build();
HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
JsonNode data = mapper.readTree(res.body()).at("/data");
JsonNode devices = data.get("devices");
return (devices != null && devices.size() > 0)
? devices.get(0).get("deviceId").asText()
: data.get("deviceId").asText();
}
// --- Check connectivity ---
public boolean isOnline(String deviceId) throws Exception {
HttpRequest req = authed("/api/devices/" + deviceId + "/connectivity").GET().build();
HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
return mapper.readTree(res.body()).at("/data/isConnected").asBoolean();
}
// --- Browse a device path (cursor paging) ---
public List<JsonNode> browse(String deviceId, String path, int pageSize) throws Exception {
List<JsonNode> items = new ArrayList<>();
String payload = mapper.writeValueAsString(Map.of("path", path, "pageSize", pageSize));
HttpRequest req = authed("/api/devices/" + deviceId + "/files/search")
.POST(HttpRequest.BodyPublishers.ofString(payload)).build();
JsonNode page = mapper.readTree(http.send(req, HttpResponse.BodyHandlers.ofString()).body()).at("/data");
page.get("items").forEach(items::add);
while (page.get("hasMore").asBoolean()) {
HttpRequest next = authed("/api/devices/" + deviceId + "/files/search?cursor="
+ URLEncoder.encode(page.get("nextCursor").asText(), java.nio.charset.StandardCharsets.UTF_8))
.GET().build();
page = mapper.readTree(http.send(next, HttpResponse.BodyHandlers.ofString()).body()).at("/data");
page.get("items").forEach(items::add);
}
return items;
}// Methods of the InnorixClient class (use with the API 인증 code)
// --- List devices ---
async Task<JsonElement> ListDevicesAsync()
{
var json = await http.GetFromJsonAsync<JsonElement>("/api/devices?page=1&size=20");
return json.GetProperty("data").GetProperty("devices");
}
// --- Resolve a name / IP into an exact deviceId ---
async Task<string> ResolveDeviceAsync(string name)
{
var json = await http.GetFromJsonAsync<JsonElement>(quot;/api/devices/resolve?name={Uri.EscapeDataString(name)}");
var data = json.GetProperty("data");
if (data.TryGetProperty("devices", out var devices) && devices.GetArrayLength() > 0)
return devices[0].GetProperty("deviceId").GetString()!;
return data.GetProperty("deviceId").GetString()!;
}
// --- Check connectivity ---
async Task<bool> IsOnlineAsync(string deviceId)
{
var json = await http.GetFromJsonAsync<JsonElement>(quot;/api/devices/{deviceId}/connectivity");
return json.GetProperty("data").GetProperty("isConnected").GetBoolean();
}
// --- Browse a device path (cursor paging) ---
async Task<List<JsonElement>> BrowseAsync(string deviceId, string path, int pageSize = 500)
{
var items = new List<JsonElement>();
var res = await http.PostAsJsonAsync(quot;/api/devices/{deviceId}/files/search", new { path, pageSize });
res.EnsureSuccessStatusCode();
var page = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("data");
items.AddRange(page.GetProperty("items").EnumerateArray());
while (page.GetProperty("hasMore").GetBoolean())
{
var cursor = Uri.EscapeDataString(page.GetProperty("nextCursor").GetString()!);
var json = await http.GetFromJsonAsync<JsonElement>(quot;/api/devices/{deviceId}/files/search?cursor={cursor}");
page = json.GetProperty("data");
items.AddRange(page.GetProperty("items").EnumerateArray());
}
return items;
}파일 · 폴더 전송#
소스 장비의 파일을 대상 장비로 보내고, 진행 중 제어·결과 확인을 수행합니다.
즉시 전송과 제어#
설명#
소스·대상 장비(sourceDevice/targetDevice — deviceId·이름·IP 모두 허용)와 평문 경로로 전송을 생성하면 monitorId가 반환됩니다(응답 data.monitorId). 폴더 루트는 sourcePaths로 넘기면 에이전트가 walk하며, 알려진 파일 목록은 sourceItem에 { path, isDir: false, fileSize }로 지정하면 더 빠릅니다. 상태는 GET /api/transfers/{monitorId}로 폴링(정수 status·percent·isTerminal)하고, pause·resume·cancel은 202로 비동기 접수됩니다. 실패 파일은 retry로 재시도하고, 파일 단위 결과는 GET /api/transfers/{monitorId}/files?idType=monitor로 조회합니다.
사용 API#
| 목적 | Method | Endpoint |
|---|---|---|
| 전송 생성 | POST | /api/transfers/manual |
| 상태 조회 | GET | /api/transfers/{monitorId} |
| 전송 제어 | POST | /api/transfers/{monitorId}/pause · resume · cancel |
| 실패 재시도 | POST | /api/transfers/{monitorId}/retry |
| 전송 파일 조회 | GET | /api/transfers/{monitorId}/files |
Request#
POST /api/transfers/manual
{
"sourceDevice": "seoul-node-01",
"targetDevice": "hanoi-node-02",
"targetPath": "/data/incoming",
"sourcePaths": ["/data/export"],
"sendAllFolder": false,
"transferOptions": { "target-action": "numbering" }
}알려진 파일 목록을 정밀 지정하려면
sourcePaths대신sourceItem을 사용합니다:"sourceItem": [{ "path": "/data/export/report.pdf", "isDir": false, "fileSize": 20480 }].target-action은numbering(자동 이름변경) ·overwrite(덮어쓰기) ·nosend(동일 파일 시 미전송) 중 하나입니다.
Response#
{
"statusCode": 201,
"message": "Created",
"data": {
"monitorId": "D5273-6820-6280-0345",
"transferId": "tr_5566",
"status": 1,
"statusName": "StartTransfer",
"isTerminal": false
}
}전송 상태 코드:
0대기 ·1시작 ·2완료 ·3일시중지 ·4오류 ·5취소 ·6전송중 ·9부분완료 ·99실패. 종료(완료 판정) 상태 ={2, 4, 5, 9, 99}.
처리 순서#
POST /api/transfers/manual호출 →data.monitorId수신GET /api/transfers/{monitorId}로status·percent·isTerminal폴링- 필요 시
POST /api/transfers/{monitorId}/pause·resume·cancel로 제어(202비동기 접수), 실패는.../retry({ filesRetry })로 재시도
구현 예제#
# --- Create a transfer ---
curl -s -X POST "$BASE_URL/api/transfers/manual" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d '{
"sourceDevice": "seoul-node-01",
"targetDevice": "hanoi-node-02",
"targetPath": "/data/incoming",
"sourcePaths": ["/data/export"],
"sendAllFolder": false,
"transferOptions": { "target-action": "numbering" }
}'
# --- Poll transfer status ---
curl -s "$BASE_URL/api/transfers/<MONITOR_ID>" "${AUTH[@]}"
# --- Control a transfer (pause / resume / cancel) ---
curl -s -X POST "$BASE_URL/api/transfers/<MONITOR_ID>/pause" "${AUTH[@]}"
curl -s -X POST "$BASE_URL/api/transfers/<MONITOR_ID>/resume" "${AUTH[@]}"
curl -s -X POST "$BASE_URL/api/transfers/<MONITOR_ID>/cancel" "${AUTH[@]}"
# --- Retry failed files ---
curl -s -X POST "$BASE_URL/api/transfers/<MONITOR_ID>/retry" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d '{ "filesRetry": [ { "filePath": "/data/export/file.txt", "isFolder": false } ] }'const TERMINAL_TRANSFER_STATUSES = new Set([2, 4, 5, 9, 99]);
// --- Create a transfer ---
async function createTransfer(sourceDevice, targetDevice, targetPath, sourcePaths, sendAllFolder = false) {
const res = await fetch(`${BASE_URL}/api/transfers/manual`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ sourceDevice, targetDevice, targetPath, sourcePaths, sendAllFolder }),
});
if (!res.ok) throw new Error(`transfer failed: ${res.status}`);
return (await res.json()).data.monitorId;
}
// --- Poll transfer status ---
async function waitForCompletion(monitorId, intervalMs = 2000) {
for (;;) {
const res = await fetch(`${BASE_URL}/api/transfers/${monitorId}`, { headers: authHeaders() });
if (!res.ok) throw new Error(`status failed: ${res.status}`);
const detail = (await res.json()).data;
const isTerminal = detail.isTerminal ?? TERMINAL_TRANSFER_STATUSES.has(detail.status);
console.log("transfer status:", detail.status, detail.statusName, `${detail.percent || 0}%`);
if (isTerminal) {
if (detail.status !== 2) throw new Error(detail.errorCode || "Transfer failed");
return detail.status;
}
await new Promise((r) => setTimeout(r, intervalMs));
}
}
// --- Control a transfer (pause / resume / cancel) — 202 accepted (async) ---
async function controlTransfer(monitorId, action) {
const res = await fetch(`${BASE_URL}/api/transfers/${monitorId}/${action}`, {
method: "POST",
headers: authHeaders(),
});
if (!res.ok) throw new Error(`control failed: ${res.status}`);
}
// --- Retry failed files ---
async function retryFailed(monitorId, files) {
const res = await fetch(`${BASE_URL}/api/transfers/${monitorId}/retry`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ filesRetry: files }),
});
if (!res.ok) throw new Error(`retry failed: ${res.status}`);
}TERMINAL_TRANSFER_STATUSES = {2, 4, 5, 9, 99}
# --- Create a transfer ---
def create_transfer(source_device: str, target_device: str, target_path: str,
source_paths: list, send_all_folder: bool = False) -> str:
res = session.post(
f"{BASE_URL}/api/transfers/manual",
json={
"sourceDevice": source_device,
"targetDevice": target_device,
"targetPath": target_path,
"sourcePaths": source_paths,
"sendAllFolder": send_all_folder,
},
timeout=15,
)
res.raise_for_status()
return res.json()["data"]["monitorId"]
# --- Poll transfer status ---
def wait_for_completion(monitor_id: str, interval: float = 2.0) -> int:
while True:
res = session.get(f"{BASE_URL}/api/transfers/{monitor_id}", timeout=10)
res.raise_for_status()
detail = res.json()["data"]
is_terminal = detail.get("isTerminal", detail.get("status") in TERMINAL_TRANSFER_STATUSES)
print("transfer status:", detail.get("status"), detail.get("statusName"), f"{detail.get('percent', 0)}%")
if is_terminal:
if detail.get("status") != 2:
raise RuntimeError(detail.get("errorCode") or "Transfer failed")
return detail.get("status")
time.sleep(interval)
# --- Control a transfer (pause / resume / cancel) — 202 accepted (async) ---
def control_transfer(monitor_id: str, action: str) -> None:
res = session.post(f"{BASE_URL}/api/transfers/{monitor_id}/{action}", timeout=10)
res.raise_for_status()
# --- Retry failed files ---
def retry_failed(monitor_id: str, files: list) -> None:
res = session.post(
f"{BASE_URL}/api/transfers/{monitor_id}/retry",
json={"filesRetry": files},
timeout=15,
)
res.raise_for_status()// Methods of the InnorixClient class (use with the API 인증 code)
static final Set<Integer> TERMINAL_TRANSFER_STATUSES = Set.of(2, 4, 5, 9, 99);
// --- Create a transfer ---
public String createTransfer(String sourceDevice, String targetDevice, String targetPath,
List<String> sourcePaths, boolean sendAllFolder) throws Exception {
Map<String, Object> body = new LinkedHashMap<>();
body.put("sourceDevice", sourceDevice);
body.put("targetDevice", targetDevice);
body.put("targetPath", targetPath);
body.put("sourcePaths", sourcePaths);
body.put("sendAllFolder", sendAllFolder);
HttpRequest req = authed("/api/transfers/manual")
.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();
}
// --- Poll transfer status ---
public int waitForCompletion(String monitorId, long intervalMs) throws Exception {
while (true) {
HttpRequest req = authed("/api/transfers/" + monitorId).GET().build();
JsonNode detail = mapper.readTree(http.send(req, HttpResponse.BodyHandlers.ofString()).body()).at("/data");
int status = detail.get("status").asInt();
boolean isTerminal = detail.has("isTerminal")
? detail.get("isTerminal").asBoolean()
: TERMINAL_TRANSFER_STATUSES.contains(status);
System.out.println("transfer status: " + status + " " + detail.path("statusName").asText());
if (isTerminal) {
if (status != 2) throw new RuntimeException(detail.path("errorCode").asText("Transfer failed"));
return status;
}
Thread.sleep(intervalMs);
}
}
// --- Control a transfer (pause / resume / cancel) — 202 accepted (async) ---
public void controlTransfer(String monitorId, String action) throws Exception {
HttpRequest req = authed("/api/transfers/" + monitorId + "/" + action)
.POST(HttpRequest.BodyPublishers.noBody()).build();
http.send(req, HttpResponse.BodyHandlers.ofString());
}
// --- Retry failed files ---
public void retryFailed(String monitorId, List<Map<String, Object>> files) throws Exception {
Map<String, Object> body = Map.of("filesRetry", files);
HttpRequest req = authed("/api/transfers/" + monitorId + "/retry")
.POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body))).build();
http.send(req, HttpResponse.BodyHandlers.ofString());
}// Methods of the InnorixClient class (use with the API 인증 code)
static readonly HashSet<int> TerminalTransferStatuses = new() { 2, 4, 5, 9, 99 };
// --- Create a transfer ---
async Task<string> CreateTransferAsync(string sourceDevice, string targetDevice, string targetPath,
IEnumerable<string> sourcePaths, bool sendAllFolder = false)
{
var res = await http.PostAsJsonAsync("/api/transfers/manual",
new { sourceDevice, targetDevice, targetPath, sourcePaths, sendAllFolder });
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadFromJsonAsync<JsonElement>();
return json.GetProperty("data").GetProperty("monitorId").GetString()!;
}
// --- Poll transfer status ---
async Task<int> WaitForCompletionAsync(string monitorId, int intervalMs = 2000)
{
while (true)
{
var json = await http.GetFromJsonAsync<JsonElement>(quot;/api/transfers/{monitorId}");
var detail = json.GetProperty("data");
var status = detail.GetProperty("status").GetInt32();
var isTerminal = detail.TryGetProperty("isTerminal", out var t)
? t.GetBoolean()
: TerminalTransferStatuses.Contains(status);
Console.WriteLine(quot;transfer status: {status}");
if (isTerminal)
{
if (status != 2) throw new Exception("Transfer failed");
return status;
}
await Task.Delay(intervalMs);
}
}
// --- Control a transfer (pause / resume / cancel) — 202 accepted (async) ---
async Task ControlTransferAsync(string monitorId, string action)
{
var res = await http.PostAsync(quot;/api/transfers/{monitorId}/{action}", null);
res.EnsureSuccessStatusCode();
}
// --- Retry failed files ---
async Task RetryFailedAsync(string monitorId, IEnumerable<object> files)
{
var res = await http.PostAsJsonAsync(quot;/api/transfers/{monitorId}/retry", new { filesRetry = files });
res.EnsureSuccessStatusCode();
}일정 자동화#
정해진 시간에 반복 실행되는 예약 전송을 구성합니다.
반복 자동화#
설명#
스케줄과 전송 상세(details)를 담아 자동화를 생성하면 automationId가 반환되며, 이 값으로 조회·일시정지·재개·수정·삭제합니다. 반복 스케줄은 transferType을 normal로 두고 schedules[].type을 day·week·month 등으로 지정하며, 시각은 12시간제 hour("01"–"12")·minute·ampm(am/pm)로 표현합니다. details[].sourceItem은 즉시 전송과 달리 {deviceId}_ino_{base64(path)} 토큰(hash 필드)이 필요하고, senderId/receiverId는 deviceId입니다. 실행 이력·상태는 GET /api/automations/{automationId}/executions로 확인합니다.
사용 API#
| 목적 | Method | Endpoint |
|---|---|---|
| 자동화 생성 | POST | /api/automations |
| 자동화 조회 | GET | /api/automations/{automationId} |
| 실행 이력 조회 | GET | /api/automations/{automationId}/executions |
| 자동화 일시정지 | POST | /api/automations/{automationId}/pause |
| 자동화 수정 | PATCH | /api/automations/{automationId} |
| 자동화 삭제 | DELETE | /api/automations/{automationId} |
Request#
POST /api/automations
{
"name": "Daily Settlement Transfer",
"transferType": "normal",
"timezone": "Asia/Seoul",
"schedules": [
{
"type": "day",
"startDateType": "now",
"hour": "02",
"minute": "00",
"ampm": "am",
"startDate": "2026-01-01T00:00:00.000Z",
"timezone": "Asia/Seoul"
}
],
"details": [
{
"senderId": "6901ae48ca578216fd739f78",
"receiverId": "690037c22d309a7bc494bc53",
"sourceItem": [
{ "hash": "6901ae48ca578216fd739f78_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }
],
"targetPath": "/data/incoming",
"step": 1,
"transferOptions": { "target-action": "numbering" }
}
]
}
hash토큰은<deviceId>_ino_<base64(path)>형식입니다. 예:deviceId가6901ae48ca578216fd739f78이고 경로가/data/report.pdf이면, 경로를 base64로 인코딩(L2RhdGEvcmVwb3J0LnBkZg==)해 붙입니다.type은week이면dayInWeek(요일명 배열),month이면dayInMonth(일자 배열)가 추가로 필요합니다.
Response#
{
"statusCode": 200,
"message": "success",
"data": { "automationId": "auto_301" }
}처리 순서#
POST /api/automations호출 →data.automationId수신GET /api/automations/{automationId}/executions로 실행 이력·상태 확인POST /api/automations/{automationId}/pause({ "pause": true })로 일시정지/재개,PATCH({ isUpdateSchedule, schedules })·DELETE로 수정·삭제
구현 예제#
# --- Build the `<deviceId>_ino_<base64(path)>` token ---
DEVICE_ID="6901ae48ca578216fd739f78"
TOKEN="${DEVICE_ID}_ino_$(printf '%s' '/data/report.pdf' | base64)"
# --- Create an automation (every day at 02:00 AM) ---
curl -s -X POST "$BASE_URL/api/automations" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d '{
"name": "Daily Settlement Transfer",
"transferType": "normal",
"timezone": "Asia/Seoul",
"schedules": [
{ "type": "day", "startDateType": "now", "hour": "02", "minute": "00", "ampm": "am",
"startDate": "2026-01-01T00:00:00.000Z", "timezone": "Asia/Seoul" }
],
"details": [
{
"senderId": "6901ae48ca578216fd739f78",
"receiverId": "690037c22d309a7bc494bc53",
"sourceItem": [ { "hash": "'"$TOKEN"'", "isDir": false } ],
"targetPath": "/data/incoming",
"step": 1,
"transferOptions": { "target-action": "numbering" }
}
]
}'
# --- Read / list executions ---
curl -s "$BASE_URL/api/automations/<AUTOMATION_ID>" "${AUTH[@]}"
curl -s "$BASE_URL/api/automations/<AUTOMATION_ID>/executions" "${AUTH[@]}"
# --- Pause / resume ---
curl -s -X POST "$BASE_URL/api/automations/<AUTOMATION_ID>/pause" "${AUTH[@]}" \
-H "Content-Type: application/json" -d '{"pause": true}'
# --- Update / delete ---
curl -s -X PATCH "$BASE_URL/api/automations/<AUTOMATION_ID>" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d '{ "name": "Daily Settlement Transfer", "isUpdateSchedule": true, "schedules": [] }'
curl -s -X DELETE "$BASE_URL/api/automations/<AUTOMATION_ID>" "${AUTH[@]}"// --- Build the `<deviceId>_ino_<base64(path)>` token ---
function encodePath(deviceId, rawPath) {
const normalized = String(rawPath || "").replaceAll("\\", "/");
return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}
// --- Create an automation (every day at 02:00 AM) ---
async function createAutomation({ name, senderId, receiverId, sourcePath, targetPath, timezone }) {
const body = {
name,
transferType: "normal",
timezone,
schedules: [
{ type: "day", startDateType: "now", hour: "02", minute: "00", ampm: "am",
startDate: new Date().toISOString(), timezone },
],
details: [
{
senderId,
receiverId,
sourceItem: [{ hash: encodePath(senderId, sourcePath), isDir: false }],
targetPath,
step: 1,
transferOptions: { "target-action": "numbering" },
},
],
};
const res = await fetch(`${BASE_URL}/api/automations`, {
method: "POST", headers: authHeaders(), body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`automation failed: ${res.status}`);
return (await res.json()).data.automationId;
}
// --- Read / list executions ---
async function getAutomation(id) {
const res = await fetch(`${BASE_URL}/api/automations/${id}`, { headers: authHeaders() });
if (!res.ok) throw new Error(`read failed: ${res.status}`);
return (await res.json()).data;
}
async function listExecutions(id) {
const res = await fetch(`${BASE_URL}/api/automations/${id}/executions`, { headers: authHeaders() });
if (!res.ok) throw new Error(`executions failed: ${res.status}`);
return (await res.json()).data;
}
// --- Pause / resume ---
async function pauseAutomation(id, pause = true) {
const res = await fetch(`${BASE_URL}/api/automations/${id}/pause`, {
method: "POST", headers: authHeaders(), body: JSON.stringify({ pause }),
});
if (!res.ok) throw new Error(`pause failed: ${res.status}`);
}
// --- Update / delete ---
async function updateAutomation(id, body) {
const res = await fetch(`${BASE_URL}/api/automations/${id}`, {
method: "PATCH", headers: authHeaders(), body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`update failed: ${res.status}`);
}
async function deleteAutomation(id) {
const res = await fetch(`${BASE_URL}/api/automations/${id}`, { method: "DELETE", headers: authHeaders() });
if (!res.ok) throw new Error(`delete failed: ${res.status}`);
}import base64
from datetime import datetime, timezone as _tz
# --- Build the `<deviceId>_ino_<base64(path)>` token ---
def encode_path(device_id: str, raw_path: str) -> str:
normalized = str(raw_path or "").replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def _now_iso() -> str:
return datetime.now(_tz.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
# --- Create an automation (every day at 02:00 AM) ---
def create_automation(name, sender_id, receiver_id, source_path, target_path, tz="Asia/Seoul"):
body = {
"name": name,
"transferType": "normal",
"timezone": tz,
"schedules": [
{"type": "day", "startDateType": "now", "hour": "02", "minute": "00",
"ampm": "am", "startDate": _now_iso(), "timezone": tz}
],
"details": [
{
"senderId": sender_id,
"receiverId": receiver_id,
"sourceItem": [{"hash": encode_path(sender_id, source_path), "isDir": False}],
"targetPath": target_path,
"step": 1,
"transferOptions": {"target-action": "numbering"},
}
],
}
res = session.post(f"{BASE_URL}/api/automations", json=body, timeout=15)
res.raise_for_status()
return res.json()["data"]["automationId"]
# --- Read / list executions ---
def get_automation(automation_id):
res = session.get(f"{BASE_URL}/api/automations/{automation_id}", timeout=10)
res.raise_for_status()
return res.json()["data"]
def list_executions(automation_id):
res = session.get(f"{BASE_URL}/api/automations/{automation_id}/executions", timeout=10)
res.raise_for_status()
return res.json()["data"]
# --- Pause / resume ---
def pause_automation(automation_id, pause=True):
res = session.post(f"{BASE_URL}/api/automations/{automation_id}/pause",
json={"pause": pause}, timeout=10)
res.raise_for_status()
# --- Update / delete ---
def update_automation(automation_id, body):
res = session.patch(f"{BASE_URL}/api/automations/{automation_id}", json=body, timeout=15)
res.raise_for_status()
def delete_automation(automation_id):
res = session.delete(f"{BASE_URL}/api/automations/{automation_id}", timeout=10)
res.raise_for_status()// Methods of the InnorixClient class (use with the API 인증 code)
// --- Build the `<deviceId>_ino_<base64(path)>` token ---
public String encodePath(String deviceId, String rawPath) {
String normalized = rawPath == null ? "" : rawPath.replace("\\", "/");
String token = Base64.getEncoder().encodeToString(normalized.getBytes(java.nio.charset.StandardCharsets.UTF_8));
return deviceId + "_ino_" + token;
}
// --- Create an automation (every day at 02:00 AM) ---
public String createAutomation(String name, String senderId, String receiverId,
String sourcePath, String targetPath, String tz) throws Exception {
Map<String, Object> schedule = new LinkedHashMap<>();
schedule.put("type", "day");
schedule.put("startDateType", "now");
schedule.put("hour", "02");
schedule.put("minute", "00");
schedule.put("ampm", "am");
schedule.put("startDate", java.time.Instant.now().toString());
schedule.put("timezone", tz);
Map<String, Object> item = Map.of("hash", encodePath(senderId, sourcePath), "isDir", false);
Map<String, Object> detail = new LinkedHashMap<>();
detail.put("senderId", senderId);
detail.put("receiverId", receiverId);
detail.put("sourceItem", List.of(item));
detail.put("targetPath", targetPath);
detail.put("step", 1);
detail.put("transferOptions", Map.of("target-action", "numbering"));
Map<String, Object> body = new LinkedHashMap<>();
body.put("name", name);
body.put("transferType", "normal");
body.put("timezone", tz);
body.put("schedules", List.of(schedule));
body.put("details", List.of(detail));
HttpRequest req = authed("/api/automations")
.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();
}
// --- Read / list executions ---
public JsonNode getAutomation(String id) throws Exception {
HttpRequest req = authed("/api/automations/" + id).GET().build();
return mapper.readTree(http.send(req, HttpResponse.BodyHandlers.ofString()).body()).at("/data");
}
public JsonNode listExecutions(String id) throws Exception {
HttpRequest req = authed("/api/automations/" + id + "/executions").GET().build();
return mapper.readTree(http.send(req, HttpResponse.BodyHandlers.ofString()).body()).at("/data");
}
// --- Pause / resume ---
public void pauseAutomation(String id, boolean pause) throws Exception {
String payload = mapper.writeValueAsString(Map.of("pause", pause));
HttpRequest req = authed("/api/automations/" + id + "/pause")
.POST(HttpRequest.BodyPublishers.ofString(payload)).build();
http.send(req, HttpResponse.BodyHandlers.ofString());
}
// --- Update / delete ---
public void updateAutomation(String id, Map<String, Object> body) throws Exception {
HttpRequest req = authed("/api/automations/" + id)
.method("PATCH", HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body))).build();
http.send(req, HttpResponse.BodyHandlers.ofString());
}
public void deleteAutomation(String id) throws Exception {
HttpRequest req = authed("/api/automations/" + id).DELETE().build();
http.send(req, HttpResponse.BodyHandlers.ofString());
}// Methods of the InnorixClient class (use with the API 인증 code)
// --- Build the `<deviceId>_ino_<base64(path)>` token ---
string EncodePath(string deviceId, string rawPath)
{
var normalized = (rawPath ?? "").Replace("\\", "/");
var token = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(normalized));
return quot;{deviceId}_ino_{token}";
}
// --- Create an automation (every day at 02:00 AM) ---
async Task<string> CreateAutomationAsync(string name, string senderId, string receiverId,
string sourcePath, string targetPath, string tz = "Asia/Seoul")
{
var body = new
{
name,
transferType = "normal",
timezone = tz,
schedules = new[]
{
new { type = "day", startDateType = "now", hour = "02", minute = "00",
ampm = "am", startDate = DateTime.UtcNow.ToString("o"), timezone = tz }
},
details = new[]
{
new
{
senderId,
receiverId,
sourceItem = new[] { new { hash = EncodePath(senderId, sourcePath), isDir = false } },
targetPath,
step = 1,
transferOptions = new Dictionary<string, string> { ["target-action"] = "numbering" },
}
}
};
var res = await http.PostAsJsonAsync("/api/automations", body);
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadFromJsonAsync<JsonElement>();
return json.GetProperty("data").GetProperty("automationId").GetString()!;
}
// --- Read / list executions ---
async Task<JsonElement> GetAutomationAsync(string id)
=> (await http.GetFromJsonAsync<JsonElement>(quot;/api/automations/{id}")).GetProperty("data");
async Task<JsonElement> ListExecutionsAsync(string id)
=> (await http.GetFromJsonAsync<JsonElement>(quot;/api/automations/{id}/executions")).GetProperty("data");
// --- Pause / resume ---
async Task PauseAutomationAsync(string id, bool pause = true)
{
var res = await http.PostAsJsonAsync(quot;/api/automations/{id}/pause", new { pause });
res.EnsureSuccessStatusCode();
}
// --- Update / delete ---
async Task UpdateAutomationAsync(string id, object body)
{
var res = await http.PatchAsJsonAsync(quot;/api/automations/{id}", body);
res.EnsureSuccessStatusCode();
}
async Task DeleteAutomationAsync(string id)
{
var res = await http.DeleteAsync(quot;/api/automations/{id}");
res.EnsureSuccessStatusCode();
}