INNORIX

INNORIX PLATFORM

INNORIX Platform시스템 전체의 파일 전송을 하나의 플랫폼에서 운영합니다.
  • 파일 전송 자동화
  • 서버 간 파일 전송
  • 객체 스토리지 간 전송
  • 파일 배포 및 수집

WEB

Exabyter웹에서 대용량 파일을 빠르고 안정적으로 업로드하고 다운로드합니다.
  • 웹 대용량 파일 업로드 & 다운로드
  • 개발자 가이드

DEVELOPERS

앱 임베디드 파일 전송기존 애플리케이션에 파일 전송 기능을 통합합니다.
  • 앱 임베디드 파일 전송

도구

전송 빌더원하는 전송을 직접 구성합니다.전송 파인더업무에 맞는 전송을 찾습니다.

성능과 규모

  • 고속 파일 전송
  • 대용량 파일 전송
  • 대량 파일 전송

현대화

  • FTP, SFTP 마이그레이션

새로운 전송 환경

  • AI Data Delivery
  • Dynamic Endpoint Transfer

글로벌 대규모 전송

Hyperlane수백 TB에서 PB까지 국가와 대륙을 넘어 전송합니다.
개발자리소스고객사
무료 시작하기
INNORIX

LET FILES
MOVE THEMSELVES

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

파일 전송

INNORIX PLATFORM

INNORIX Platform
  • 파일 전송 자동화
  • 서버 간 파일 전송
  • 객체 스토리지 간 전송
  • 파일 배포 및 수집

PERFORMANCE & SCALE

  • 고속 파일 전송
  • 대용량 파일 전송
  • 대량 파일 전송

MODERNIZATION

  • FTP, SFTP 마이그레이션

WEB

Exabyter
  • 웹 대용량 파일 업로드 & 다운로드
  • 개발자 가이드

DEVELOPERS

  • 앱 임베디드 파일 전송

NEW WORKLOADS

  • AI Data Delivery
  • Dynamic Endpoint Transfer

HYPERLANE

  • 서비스 소개
  • 서비스 문의

개발자

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

도구

  • 전송 빌더
  • 전송 파인더

리소스

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

고객사

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

플랜

  • 가격 및 플랜

회사

INNORIX 소개

비전 AI 제품

Al.bert — 스마트 교통 AI

글로벌 오피스

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

(C)2026 INNORIX. All rights reserved.

  • 보안
  • 상태
  • 이용약관
  • 개인정보 처리방침
  • 쿠키 정책
  1. 개발자
  2. 구현 예제
  3. 한 곳으로 파일 보내기

한 곳으로 파일 보내기

INNORIX Transfer Builder를 사용해 1대1 파일 전송을 구성합니다. 하나의 소스와 하나의 대상을 선택하고 전송 설정을 생성합니다.

배포 및 관리
  • 한 곳으로 파일 보내기
  • 여러 곳으로 파일 보내기
  • 여러 곳에서 파일 수집
  • 시스템 간 파일 동기화
  • 웹 업로드, 다운로드

한 곳으로 파일 보내기는 보내는 디바이스 한 대(Source)에서 받는 디바이스 한 대(Target)로 파일 또는 폴더를 전송하는, 가장 기본이 되는 형태입니다.

API 요청은 POST /api/automations 한 번입니다. details 에 보내는 쪽과 받는 쪽 한 쌍을 넣고, schedules 로 실행 시점을 정합니다.

전송 빌더에서 Get API Code 를 누르면 같은 내용이 언어별 실행 가능한 예제(combo_builder.*)와 .env 로 묶여 내려받아집니다. 이 문서는 그 예제에서 핵심만 떼어낸 것입니다.

시작하기#

준비물#

  1. API Key — 제품 좌측 하단 프로필 메뉴 → Developer 에서 발급합니다. 같은 화면에 Workspace ID 도 함께 표시됩니다. API 로 발급하려면 POST /api/auth/api-keys (Bearer 액세스 토큰, 바디 없음) → data.apiKey.
  2. deviceId 두 개 — 제품의 Devices 에서 디바이스를 선택하면 우측 상단에 표시되는 ID 입니다. Windows, macOS, Ubuntu, RHEL, Rocky, Debian 등 에이전트가 설치된 장비와 Amazon S3, Azure Blob, Google Cloud Storage 같은 오브젝트 스토리지를 모두 지정할 수 있습니다.
  3. 경로 — 보낼 쪽 경로(sourceItem[].filePath)와 받을 쪽 경로(targetPath). 둘 다 슬래시(/)로 구분한 절대 경로를 사용합니다. targetPath 는 비어 있거나 / 이면 안 됩니다.

인증은 다음 두 가지 중 하나를 사용합니다.

http
x-api-key: <API Key>                  # long-lived key (recommended)
Authorization: Bearer <accessToken>   # short-lived token from login

워크스페이스를 명시해야 하는 경우에만 헤더를 하나 더 추가합니다. 이 헤더는 인증 수단이 아니라 대상 워크스페이스 지정용입니다.

http
x-workspace-id: <Workspace ID>        # optional

기본 주소는 https://app.innorix.com 입니다.

빠른 시작#

빌더에서 Get API Code 로 받은 번들을 그대로 실행하는 순서입니다.

  1. 전송 빌더에서 옵션을 고르고 Get API Code → 언어 선택 → zip 다운로드
  2. 압축을 풀고 .env 를 열어 INNORIX_API_KEY 와 SOURCE_ID, TARGET_ID, 경로(SOURCE_PATH, TARGET_PATH)를 채웁니다
  3. 아래 명령으로 실행합니다
  4. 출력된 automationId 로 전송 상태를 조회합니다
언어 요구 사항 실행
Python Python 3.8+ pip install requests → python combo_builder.py
Node.js Node.js 18+ (의존성 없음) node combo_builder.js
Java JDK 11+ (의존성 없음) java ComboBuilder.java 또는 javac ComboBuilder.java && java ComboBuilder
C# .NET 8+ dotnet run

ℹ️ 위 요구 사항은 번들 예제 기준입니다. 이 문서에 실린 Java 발췌 코드는 가독성을 위해 텍스트 블록(""")을 사용하므로 JDK 17+ 가 필요합니다. 번들의 ComboBuilder.java 는 JDK 11+ 에서 동작합니다.

ℹ️ 번들의 combo_builder.* 는 같은 폴더의 .env 를 직접 읽습니다(별도 라이브러리 없이). 반면 이 문서에 실린 발췌 코드는 환경 변수에서 값을 읽으므로, 그대로 복사해 실행할 때는 아래처럼 값을 내보낸 뒤 실행하세요.

macOS, Linux

bash
export INNORIX_API_KEY=your-api-key
export SOURCE_ID=device-source-01
export SOURCE_PATH=C:/data/out
export TARGET_ID=device-target-01
export TARGET_PATH=C:/incoming

Windows PowerShell (CMD 에서는 set INNORIX_API_KEY=your-api-key 형식)

powershell
$env:INNORIX_API_KEY="your-api-key"
$env:SOURCE_ID="device-source-01"
$env:SOURCE_PATH="C:/data/out"
$env:TARGET_ID="device-target-01"
$env:TARGET_PATH="C:/incoming"

전송 만들기#

전송 만들기#

한 대에서 한 대로 보내기는 POST /api/automations 한 번으로 만들어집니다. details 배열에 보내는 쪽 / 받는 쪽 한 쌍을 넣고, schedules 에 실행 시점을 넣습니다. 아래는 지금 바로 실행하는 경우입니다.

json
{
  "name": "nightly-export",
  "flowName": "nightly-export",
  "transferType": "normal",
  "timezone": "Asia/Seoul",
  "details": [
    {
      "senderId": "<sourceDeviceId>",
      "receiverId": "<targetDeviceId>",
      "sourceItem": [{ "filePath": "C:/data/out", "isDir": true }],
      "targetPath": "C:/incoming",
      "step": 1,
      "transferOptions": { "noSchedule": false, "target-action": "overwrite" }
    }
  ],
  "schedules": [
    { "type": "none", "startDateType": "now", "startDate": "2026-09-14T02:00:00.000Z", "timezone": "Asia/Seoul" }
  ],
  "step": 1,
  "isUpcoming": false
}
  • transferOptions.target-action 은 이름이 겹칠 때의 동작입니다. overwrite(덮어쓰기), numbering(이름 뒤에 번호), nosend(건너뛰기) 중 하나를 넣습니다.
  • startDate 는 예시 값입니다. Now 로 실행할 때는 요청 시점의 현재 UTC 시각을 넣으세요 (아래 예제 코드는 실행할 때마다 현재 시각을 계산합니다).

응답의 data.automationId 가 이후 조회에 쓰이는 식별자입니다.

# pip install requests
import os, time, requests

BASE = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com")
HEADERS = {"x-api-key": os.environ["INNORIX_API_KEY"], "Content-Type": "application/json"}

# Every setting comes from an environment variable (second argument is the default).
SOURCE_ID     = os.environ["SOURCE_ID"]
SOURCE_PATH   = os.getenv("SOURCE_PATH", "C:/data/out")
TARGET_ID     = os.environ["TARGET_ID"]
TARGET_PATH   = os.getenv("TARGET_PATH", "C:/incoming")
SOURCE_IS_DIR = os.getenv("SOURCE_IS_DIR", "true").lower() != "false"
TZ            = os.getenv("SCHEDULE_TZ", "Asia/Seoul")

def now_iso():
    return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())

def call(method, path, body=None, params=None):
    r = requests.request(method, BASE + path, headers=HEADERS,
                         json=body, params=params, timeout=30)
    if not r.ok:
        raise RuntimeError(f"API {r.status_code}: {r.text[:500]}")
    return (r.json() or {}).get("data")

body = {
    "name": "nightly-export",
    "flowName": "nightly-export",
    "transferType": "normal",          # "sync" for sync, "command" for an external trigger
    "timezone": TZ,
    "details": [{
        "senderId": SOURCE_ID,
        "receiverId": TARGET_ID,
        "sourceItem": [{"filePath": SOURCE_PATH, "isDir": SOURCE_IS_DIR}],   # false for a single file
        "targetPath": TARGET_PATH,
        "step": 1,
        "transferOptions": {"noSchedule": False, "target-action": "overwrite"},
    }],
    "schedules": [{
        "type": "none", "startDateType": "now",
        "startDate": now_iso(), "timezone": TZ,
    }],
    "step": 1,
    "isUpcoming": False,
}

automation_id = call("POST", "/api/automations", body)["automationId"]
print("automation created:", automation_id)
// JDK 17+ (no external dependencies)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SendToOne {

    static final String BASE = env("INNORIX_BASE_URL", "https://app.innorix.com");
    static final String API_KEY = env("INNORIX_API_KEY", "");
    static final HttpClient HTTP = HttpClient.newHttpClient();

    /** Reads an environment variable, falling back to the default when it is empty. */
    static String env(String key, String dflt) {
        String v = System.getenv(key);
        return (v == null || v.isBlank()) ? dflt : v;
    }

    /** Extracts one string field from the response. Like the downloaded example, it needs no
     *  external JSON library. Use Jackson or Gson when you need to walk deeper structures. */
    static String jsonString(String json, String key) {
        Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json);
        return m.find() ? m.group(1) : null;
    }

    /** Extracts one integer field from the response. */
    static int jsonInt(String json, String key, int dflt) {
        Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*(-?\\d+)").matcher(json);
        return m.find() ? Integer.parseInt(m.group(1)) : dflt;
    }

    static String call(String method, String path, String body) throws Exception {
        HttpRequest.BodyPublisher pub = body == null
                ? HttpRequest.BodyPublishers.noBody()
                : HttpRequest.BodyPublishers.ofString(body);
        HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
                .header("Content-Type", "application/json")
                .header("x-api-key", API_KEY)
                .method(method, pub)
                .build();
        HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() >= 400) throw new RuntimeException("API " + res.statusCode() + ": " + res.body());
        return res.body();
    }

    public static void main(String[] args) throws Exception {
        // Every setting comes from an environment variable (second argument is the default).
        String sourceId   = env("SOURCE_ID", "");
        String sourcePath = env("SOURCE_PATH", "C:/data/out");
        String targetId   = env("TARGET_ID", "");
        String targetPath = env("TARGET_PATH", "C:/incoming");
        String tz         = env("SCHEDULE_TZ", "Asia/Seoul");
        String nowIso = Instant.now().truncatedTo(ChronoUnit.SECONDS).toString().replace("Z", ".000Z");

        String body = """
                {
                  "name": "nightly-export",
                  "flowName": "nightly-export",
                  "transferType": "normal",
                  "timezone": "%s",
                  "details": [{
                    "senderId": "%s",
                    "receiverId": "%s",
                    "sourceItem": [{ "filePath": "%s", "isDir": true }],
                    "targetPath": "%s",
                    "step": 1,
                    "transferOptions": { "noSchedule": false, "target-action": "overwrite" }
                  }],
                  "schedules": [{
                    "type": "none", "startDateType": "now",
                    "startDate": "%s", "timezone": "%s"
                  }],
                  "step": 1,
                  "isUpcoming": false
                }
                """.formatted(tz, sourceId, targetId, sourcePath, targetPath, nowIso, tz);

        String res = call("POST", "/api/automations", body);
        System.out.println("automation created: " + jsonString(res, "automationId"));
    }
}
// Node.js 18+ (uses the built-in fetch)
const BASE = process.env.INNORIX_BASE_URL || 'https://app.innorix.com';
const HEADERS = {
  'x-api-key': process.env.INNORIX_API_KEY,
  'Content-Type': 'application/json',
};

// Every setting comes from an environment variable (the value after || is the default).
const TZ = process.env.SCHEDULE_TZ || 'Asia/Seoul';
const SOURCE_ID = process.env.SOURCE_ID;
const SOURCE_PATH = process.env.SOURCE_PATH || 'C:/data/out';
const TARGET_ID = process.env.TARGET_ID;
const TARGET_PATH = process.env.TARGET_PATH || 'C:/incoming';

const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, '.000Z');

async function call(method, path, body, params) {
  let url = BASE + path;
  if (params) url += '?' + new URLSearchParams(params).toString();
  const res = await fetch(url, {
    method,
    headers: HEADERS,
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const payload = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`API ${res.status}: ${JSON.stringify(payload)}`);
  return payload.data;
}

const body = {
  name: 'nightly-export',
  flowName: 'nightly-export',
  transferType: 'normal',
  timezone: TZ,
  details: [{
    senderId: SOURCE_ID,
    receiverId: TARGET_ID,
    sourceItem: [{ filePath: SOURCE_PATH, isDir: true }],
    targetPath: TARGET_PATH,
    step: 1,
    transferOptions: { noSchedule: false, 'target-action': 'overwrite' },
  }],
  schedules: [{ type: 'none', startDateType: 'now', startDate: nowIso(), timezone: TZ }],
  step: 1,
  isUpcoming: false,
};

const { automationId } = await call('POST', '/api/automations', body);
console.log('automation created:', automationId);
// .NET 8+ (standard library only)
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

static class SendToOne
{
    // Every setting comes from an environment variable (second argument is the default).
    static string Env(string key, string dflt = "") =>
        Environment.GetEnvironmentVariable(key) is { Length: > 0 } v ? v : dflt;

    static readonly string Base = Env("INNORIX_BASE_URL", "https://app.innorix.com");
    static readonly HttpClient Http = new();

    static async Task<JsonElement> Call(string method, string path, object? body = null)
    {
        var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
        req.Headers.Add("x-api-key", Env("INNORIX_API_KEY"));
        if (body != null)
            req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");

        var res = await Http.SendAsync(req);
        var text = await res.Content.ReadAsStringAsync();
        if (!res.IsSuccessStatusCode) throw new Exception(
quot;API {(int)res.StatusCode}: {text}"
); return JsonDocument.Parse(text).RootElement.GetProperty("data"); } static async Task Main() { var tz = Env("SCHEDULE_TZ", "Asia/Seoul"); var nowIso = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.000Z"); // Keys that contain a hyphen, like "target-action", need a Dictionary. var transferOptions = new Dictionary<string, object> { ["noSchedule"] = false, ["target-action"] = "overwrite", }; var body = new { name = "nightly-export", flowName = "nightly-export", transferType = "normal", timezone = tz, details = new[] { new { senderId = Env("SOURCE_ID"), receiverId = Env("TARGET_ID"), sourceItem = new[] { new { filePath = Env("SOURCE_PATH", "C:/data/out"), isDir = true } }, targetPath = Env("TARGET_PATH", "C:/incoming"), step = 1, transferOptions, } }, schedules = new[] { new { type = "none", startDateType = "now", startDate = nowIso, timezone = tz } }, step = 1, isUpcoming = false, }; var data = await Call("POST", "/api/automations", body); Console.WriteLine("automation created: " + data.GetProperty("automationId").GetString()); } }

진행 상황 확인#

자동화가 만들어지면 실제 전송은 별도의 monitorId 로 추적합니다.

  1. GET /api/transfers?automationId=<automationId> — 진행 중인 전송 목록. 응답은 커서 페이지네이션(data.data[])이고, 실제 전송 행은 type: "monitor" 입니다. type 이 automation, history, flow 인 행은 요약 행이므로 건너뜁니다.
  2. GET /api/transfers/<monitorId> — 상태와 진행률.

상태 코드는 다음과 같습니다.

코드 의미 코드 의미
-1 queued 6 transferring
0 waiting 7 skipped
1 started 8 retry
2 complete 9 partial-complete
3 paused 11 virus-scanning
4 error 12 syncing
5 cancelled 99 fail

종료 상태는 2, 4, 5, 9, 99 입니다.

SKIP_ROW_TYPES = {"automation", "history", "flow"}
TERMINAL = {2, 4, 5, 9, 99}
STATUS = {-1: "queued", 0: "waiting", 1: "started", 2: "complete", 3: "paused",
          4: "error", 5: "cancelled", 6: "transferring", 7: "skipped", 8: "retry",
          9: "partial-complete", 11: "virus-scanning", 12: "syncing", 99: "fail"}

def monitor_ids(automation_id):
    result = call("GET", "/api/transfers", params={"automationId": automation_id})
    records = result.get("data") if isinstance(result, dict) else result
    return [r.get("monitorId") or r.get("id")
            for r in (records or []) if r.get("type") not in SKIP_ROW_TYPES]

def wait_for(monitor_id, timeout=3600):
    deadline = time.time() + timeout
    while time.time() < deadline:
        detail = call("GET", f"/api/transfers/{monitor_id}") or {}
        status = detail.get("status")
        print(f"  {STATUS.get(status, status)} ({detail.get('percent', 0)}%)")
        if detail.get("isTerminal", status in TERMINAL):
            return detail
        time.sleep(3)
    raise TimeoutError(f"{monitor_id} did not finish within {timeout}s")

for mid in monitor_ids(automation_id):
    wait_for(mid)
// Reuses call(), jsonString() and jsonInt() from SendToOne above.
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

static final Set<Integer> TERMINAL = Set.of(2, 4, 5, 9, 99);

/** Collects every monitorId in the list response.
 *  Summary rows (type=automation/history/flow) carry no monitorId, so they drop out. */
static List<String> monitorIds(String automationId) throws Exception {
    String json = call("GET", "/api/transfers?automationId=" + automationId, null);
    List<String> ids = new ArrayList<>();
    Matcher m = Pattern.compile("\"monitorId\"\\s*:\\s*\"([^\"]+)\"").matcher(json);
    while (m.find()) if (!ids.contains(m.group(1))) ids.add(m.group(1));
    return ids;
}

/** Polls every 3 seconds until the transfer reaches a terminal status. */
static int waitFor(String monitorId, int timeoutSeconds) throws Exception {
    long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;
    while (System.currentTimeMillis() < deadline) {
        String json = call("GET", "/api/transfers/" + monitorId, null);
        int status = jsonInt(json, "status", -1);
        System.out.println("  " + monitorId + ": status=" + status
                + " (" + jsonInt(json, "percent", 0) + "%)");
        if (TERMINAL.contains(status)) return status;
        Thread.sleep(3000);
    }
    throw new RuntimeException(monitorId + " did not finish within " + timeoutSeconds + "s");
}

// Usage
for (String monitorId : monitorIds(automationId)) {
    int status = waitFor(monitorId, 3600);
    System.out.println("  finished: " + (status == 2 ? "complete" : "status " + status));
}
const SKIP_ROW_TYPES = new Set(['automation', 'history', 'flow']);
const TERMINAL = new Set([2, 4, 5, 9, 99]);
const STATUS = {
  '-1': 'queued', 0: 'waiting', 1: 'started', 2: 'complete', 3: 'paused',
  4: 'error', 5: 'cancelled', 6: 'transferring', 7: 'skipped', 8: 'retry',
  9: 'partial-complete', 11: 'virus-scanning', 12: 'syncing', 99: 'fail',
};

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function monitorIds(automationId) {
  const result = await call('GET', '/api/transfers', undefined, { automationId });
  const records = Array.isArray(result) ? result : result?.data || [];
  return records
    .filter((r) => !SKIP_ROW_TYPES.has(r.type))
    .map((r) => r.monitorId || r.id)
    .filter(Boolean);
}

async function waitFor(monitorId, timeoutMs = 3600_000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const detail = (await call('GET', `/api/transfers/${monitorId}`)) || {};
    console.log(`  ${STATUS[detail.status] ?? detail.status} (${detail.percent ?? 0}%)`);
    if (detail.isTerminal ?? TERMINAL.has(detail.status)) return detail;
    await sleep(3000);
  }
  throw new Error(`${monitorId} did not finish within the time limit`);
}

for (const mid of await monitorIds(automationId)) await waitFor(mid);
static readonly HashSet<int> Terminal = new() { 2, 4, 5, 9, 99 };
static readonly HashSet<string> SkipRowTypes = new() { "automation", "history", "flow" };

static async Task<List<string>> MonitorIds(string automationId)
{
    var result = await Call("GET", "/api/transfers?automationId=" + automationId);
    var records = result.ValueKind == JsonValueKind.Object && result.TryGetProperty("data", out var inner)
        ? inner : result;

    var ids = new List<string>();
    foreach (var r in records.EnumerateArray())
    {
        var type = r.TryGetProperty("type", out var t) ? t.GetString() : null;
        if (type != null && SkipRowTypes.Contains(type)) continue;
        if (r.TryGetProperty("monitorId", out var m)) ids.Add(m.GetString()!);
        else if (r.TryGetProperty("id", out var i)) ids.Add(i.GetString()!);
    }
    return ids;
}

static async Task WaitFor(string monitorId, int timeoutSeconds = 3600)
{
    var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
    while (DateTime.UtcNow < deadline)
    {
        var detail = await Call("GET", "/api/transfers/" + monitorId);
        var status = detail.GetProperty("status").GetInt32();
        Console.WriteLine(
quot; status={status}"
); var isTerminal = detail.TryGetProperty("isTerminal", out var it) ? it.GetBoolean() : Terminal.Contains(status); if (isTerminal) return; await Task.Delay(3000); } throw new Exception(
quot;{monitorId} did not finish within the time limit"
); }

전송 옵션#

시작 시점#

실행 시점은 schedules[0] 하나로 정합니다. details 는 그대로 두고 이 객체만 바꾸면 됩니다.

실행 시점 schedules[0] 비고
지금 바로 { type: "none", startDateType: "now", startDate: <현재 ISO> } 생성 즉시 실행
지정 시각에 1회 { type: "none", startDateType: "specific", startDate: "2026-09-20T01:00:00" }
매시 반복 { type: "hour", ... } 매시 정각
매일 반복 { type: "day", hour, minute, ampm }
매주 반복 { type: "week", dayInWeek: ["monday"], hour, minute, ampm }
매월 반복 { type: "month", dayInMonth: ["1"], hour, minute, ampm } 0 은 말일
이전 자동화가 끝난 뒤 { type: "none", startDateType: "now", triggerAutomation: { value: "<이전 automationId>" } } 바디에 flowId 추가
외부 요청으로 { type: "none", startDateType: "now" } + 바디 transferType: "command" 아래 참고

반복(repeat)에서는 startDateType / startDate 로 첫 실행 시각을 정합니다. startDateType: "now" 면 생성 즉시 한 번 실행하고 이후 주기대로, "specific" 이면 계산한 다음 주기부터 시작합니다. hour 는 1–12, ampm 은 am / pm, timezone 은 Asia/Seoul 같은 IANA 이름입니다.

External request 는 두 번의 사전 호출이 필요합니다.

http
POST /api/command/generate-code       → data.code
GET  /api/command/generate-api-key    → data.apiKey

두 값을 자동화 바디의 code, apiKey 로 넣어 생성하면, 다음 주소를 호출할 때마다 전송이 시작됩니다.

http
POST https://app.innorix.com/command/<code>
x-api-key: <apiKey>

파일 옵션#

파일 처리 옵션은 details[].transferOptions 안에 넣습니다.

옵션 키 값
확장자 필터 send-fileoption.extension { "extension": ["pdf","mp4"], "allow": true } — 차단 목록이면 allow: false
크기 필터 send-fileoption.fileSize { "size": <바이트>, "over": true, "equal": true } — 하한은 over: true, 상한은 over: false (한쪽만 지정 가능)
이름 필터 send-fileoption.fileName { "name": "temp", "allow": false } — 이름에 포함되면 제외
폴더 구조 유지 savepath true
날짜 하위 폴더 savepath + optionPath true + 1
디바이스명 하위 폴더 savepath + optionPath true + 2
사용자 지정 하위 폴더 savepath + optionPath "<폴더명>" + 3
중복 이름 — 덮어쓰기 target-action "overwrite"
중복 이름 — 이름 뒤에 번호 target-action "numbering"
중복 이름 — 건너뛰기 target-action "nosend"
무결성 검증 checkIntegrity true
json
{
  "noSchedule": false,
  "target-action": "numbering",
  "checkIntegrity": true,
  "savepath": true,
  "optionPath": 1,
  "send-fileoption": {
    "extension": { "extension": ["pdf", "xlsx"], "allow": true },
    "fileSize": { "size": 1048576, "over": true, "equal": true },
    "fileName": { "name": "tmp", "allow": false }
  }
}

ℹ️ optionPath(날짜, 디바이스명, 사용자 지정 하위 폴더)는 자동화에서만 적용됩니다. 이 문서의 모든 전송은 POST /api/automations 로 만들어지므로 Start → Now 여도 그대로 적용됩니다.

전송 후 동작#

전송이 끝난 뒤의 동작은 두 갈래로 나뉩니다.

① 자동화에 붙는 프로세서 — 바디의 processors[]

json
{
  "processors": [
    { "events": "Run", "type": "https", "method": "POST",
      "url": "https://api.example.com/webhook", "body": "{\"event\":\"done\"}" },
    { "category": "monitoring", "type": "grafana", "name": "builder-grafana",
      "config": { "baseUrl": "https://grafana.company.com", "apiToken": "***" },
      "notificationConfig": { "events": { "completed": true, "error": true } } }
  ]
}
  • Run API — 전송마다 호출되는 HTTP 훅입니다.
  • Monitoring(Grafana, Datadog, Prometheus 등) — 워크스페이스 전역이 아니라 이 자동화에 붙습니다. 선택 가능한 이벤트는 started, completed, paused, recovered, deviceConnected, deviceDisconnected 입니다.

② 워크스페이스 전역 연동 — POST /api/integrations

Message(Slack, Teams, Discord …), Virus scan(ClamAV, Microsoft Defender …), Email(SES, SendGrid) 은 전송이 아니라 워크스페이스에 등록됩니다.

json
{
  "name": "builder-slack",
  "type": "slack",
  "category": "notification",
  "config": { "webhookUrl": "https://hooks.slack.com/services/XXX", "channel": "#transfers" },
  "notificationConfig": { "events": { "completed": true, "error": true } }
}

category 는 Message → notification, Virus scan → security, Email → email 입니다. 제공자별 필수 설정 항목은 GET /api/integrations/rules/{type} 으로 확인할 수 있습니다. 이벤트 이름은 started, completed, paused, resumed, recovered, canceled, error, skipped 입니다.

참고#

빌더 UI ↔ .env ↔ API 매핑#

Get API Code 로 받은 번들의 .env 키와 API 필드의 대응입니다.

빌더 UI .env API
탭 = 한 대에서 한 대로 보내기 TRANSFER_TYPE=send_one details 1개
From 디바이스 SOURCE_ID details[].senderId
From 경로 SOURCE_PATH details[].sourceItem[].filePath
폴더/파일 여부 SOURCE_IS_DIR details[].sourceItem[].isDir
To 디바이스 TARGET_ID details[].receiverId
To 경로 TARGET_PATH details[].targetPath
Start START_WHEN schedules[0]
전송 이름 NAME name, flowName
File options FILTER_*, SAVE_PATH, DUPLICATE_ACTION, INTEGRITY details[].transferOptions
After transfer ON_* processors[], POST /api/integrations

자주 겪는 오류#

증상 원인과 해결
401 Unauthorized x-api-key 가 비었거나 만료됐습니다. Developer 화면에서 재발급하세요. Bearer 토큰은 수명이 짧습니다.
400 Bad Request targetPath 가 비었거나 / 인 경우가 가장 많습니다. senderId, receiverId 의 deviceId 오타도 확인하세요.
자동화는 만들어졌는데 전송이 안 보임 에이전트가 오프라인일 수 있습니다. GET /api/transfers?automationId=... 을 몇 초 간격으로 다시 조회하세요.
진행률이 멈춰 있음 받는 쪽 에이전트 연결이 끊겼을 때 나타납니다. 디바이스 상태를 먼저 확인하세요.
필터를 걸었는데 다 전송됨 send-fileoption.extension 은 { "extension": [...], "allow": ... } 중첩 구조입니다. 배열만 넣으면 무시됩니다.

요청과 응답을 그대로 보고 싶으면 내려받은 예제에서 DEBUG=true 를 켜면 됩니다.

다음여러 곳으로 파일 보내기

이 페이지에서

  • 시작하기
  • 준비물
  • 빠른 시작
  • 전송 만들기
  • 전송 만들기
  • 진행 상황 확인
  • 전송 옵션
  • 시작 시점
  • 파일 옵션
  • 전송 후 동작
  • 참고
  • 빌더 UI ↔ .env ↔ API 매핑
  • 자주 겪는 오류