INNORIX
전송 빌더전송 파인더개발자리소스고객사
무료 시작하기
INNORIX

LET FILES
MOVE THEMSELVES

INNORIX는 모든 시스템과 환경에서 파일 이동과 자동화를 제공하는 엔터프라이즈 파일 인프라 기업입니다.
5,000개 이상의 기업 및 공공기관에서 사용하고 있습니다.

시작하기

  • 필요한 전송 만들기
  • 필요한 전송 찾기

주요 전송

  • 팀 업무 폴더 동기화
  • 고객에게 대용량 파일 전송
  • 여러 시스템 파일 탐색
  • FTP·SFTP·SCP·rsync 전환
  • 앱에 파일 전송 추가
  • 웹 업로드·다운로드 적용
  • AI·데이터 워크플로 구축
  • 모든 전송 둘러보기→

개발자

  • 개발자 센터
  • 구현 예제
  • API 빠른 시작
  • 개발자 가이드
  • API 레퍼런스
  • GitHub

리소스

  • 리소스 센터
  • 제품 가이드
  • 외부 서비스 연동
  • 배포 및 관리
  • 도움말 센터

고객사

  • 정부
  • 공공부문
  • 제조
  • 엔지니어링
  • 금융
  • 유통
  • IT/통신
  • 미디어
  • 의료
  • 교육

플랜

  • 가격 및 플랜

회사

INNORIX 소개

Exabyter를 찾고 계신가요?

이제 INNORIX Platform에 통합되었습니다

기타 INNORIX 제품

Al.bert — 스마트 교통 AI

글로벌 오피스

  • 미국 뉴욕
  • 대한민국 서울
  • 베트남 호찌민
  • 오피스 위치 보기→

(C)2026 INNORIX. All rights reserved.

  • 보안
  • 상태
  • 이용약관
  • 개인정보 처리방침
  • 쿠키 정책
  1. 개발자
  2. 개발자 가이드

개발자 가이드

INNORIX 개발자 가이드로 API 인증, 시스템 연결, 파일 및 폴더 전송, 예약 자동화에 대해 다룹니다. 로그인, 토큰 및 API 키 엔드포인트에 대한 정보도 포함되어 있습니다.

배포 및 관리
Exabyter
  • 개발자 가이드
  • AI 모델 파일을 여러 엣지 장비에 배포하기
  • Azure Blob에서 사내 서버로 파일 전달하기
  • CI/CD 빌드 결과물을 여러 서버에 배포하기
  • Datadog으로 파일 전송 실패와 복구 알림 받기
  • FTP 배치 작업을 관리형 파일 흐름으로 전환하기
  • Google Cloud Storage에서 Amazon S3로 직접 전송하기
  • Grafana에서 파일 전송 상태 대시보드 만들기
  • Kubernetes에서 오브젝트 스토리지로 결과 파일 보내기
  • Git으로 관리하기 어려운 파일을 자동 전달하기
  • 수신 파일을 Microsoft Defender로 검사하고 후속 처리하기
  • rsync 작업을 관리형 파일 흐름으로 전환하기

본 문서는 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

json
{
  "email": "<YOUR_EMAIL>",
  "password": "<YOUR_PASSWORD>"
}

Response#

json
{
  "statusCode": 200,
  "message": "success",
  "data": {
    "user": {
      "email": "user@example.com",
      "userName": "User Name",
      "userId": "usr_abc123",
      "accessToken": "<ACCESS_TOKEN>",
      "refreshToken": "<REFRESH_TOKEN>"
    }
  },
  "isCached": false
}

처리 순서#

  1. POST /api/auth/login으로 로그인 → data.user.accessToken 수신
  2. 이후 요청에 Authorization: Bearer <ACCESS_TOKEN>와 x-workspace-id 헤더 부착
  3. 토큰 만료 시 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

json
{
  "path": "C:/data/export",
  "pageSize": 500
}

Response#

GET /api/devices/resolve?name=seoul-node-01

json
{
  "statusCode": 200,
  "message": "success",
  "data": {
    "matchCount": 1,
    "devices": [
      { "deviceId": "6901ae48ca578216fd739f78", "name": "seoul-node-01", "os": "linux", "status": 1 }
    ]
  }
}

GET /api/devices/{deviceId}/connectivity

json
{
  "statusCode": 200,
  "message": "success",
  "data": { "deviceId": "6901ae48ca578216fd739f78", "state": 1, "isConnected": true }
}

POST /api/devices/{deviceId}/files/search

json
{
  "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
  }
}

처리 순서#

  1. GET /api/devices 또는 GET /api/devices/resolve로 소스·대상 deviceId 확인 (즉시 전송에는 이름·IP도 그대로 사용 가능)
  2. GET /api/devices/{deviceId}/connectivity로 두 장비 isConnected 여부 확인
  3. (선택) 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

json
{
  "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#

json
{
  "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}.

처리 순서#

  1. POST /api/transfers/manual 호출 → data.monitorId 수신
  2. GET /api/transfers/{monitorId}로 status·percent·isTerminal 폴링
  3. 필요 시 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

json
{
  "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#

json
{
  "statusCode": 200,
  "message": "success",
  "data": { "automationId": "auto_301" }
}

처리 순서#

  1. POST /api/automations 호출 → data.automationId 수신
  2. GET /api/automations/{automationId}/executions로 실행 이력·상태 확인
  3. 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(); }
다음AI 모델 파일을 여러 엣지 장비에 배포하기

이 페이지에서

  • API 인증
  • 로그인과 토큰
  • 시스템 연결
  • 디바이스 확인과 파일 탐색
  • 파일 · 폴더 전송
  • 즉시 전송과 제어
  • 일정 자동화
  • 반복 자동화