INNORIX
Transfer BuilderTransfer FinderDevelopersResourcesCustomers
Start Free
INNORIX

LET FILES
MOVE THEMSELVES

INNORIX provides enterprise file infrastructure for moving and automating files across every system and environment.
Trusted by more than 5,000 enterprise and government agencies.

START HERE

  • Build the Transfer You Need
  • Find the Transfer You Need

POPULAR TRANSFERS

  • Sync Team Folders
  • Send Large Files to Clients
  • Explore Files Across Systems
  • Migrate FTP, SFTP, SCP & rsync
  • Add Transfer to Any App
  • Add Web Upload & Download
  • Build AI & Data Workflows
  • Browse All Transfers→

DEVELOPERS

  • Developer Center
  • Examples
  • API Quickstart
  • Developer Guide
  • API Reference
  • GitHub

RESOURCES

  • Resource Center
  • Product Guide
  • Integrations
  • Deploy & Manage
  • Help Center

CUSTOMERS

  • Government
  • Public Sector
  • Manufacturing
  • Engineering
  • Finance
  • Distribution
  • IT/Telecom
  • Media
  • Healthcare
  • Education

PLANS

  • Pricing

COMPANY

About Us

OTHER INNORIX PRODUCT

Al.bert — Smart Traffic AI

GLOBAL OFFICES

  • New York, USA
  • Seoul, South Korea
  • Ho Chi Minh City, Vietnam
  • View Office Locations→

(C)2026 INNORIX. All rights reserved.

  • Security
  • Status
  • Terms
  • Privacy
  • Cookies
  1. Guide
  2. Add File Transfer Capabilities to Web, App, and Business Systems

Add File Transfer Capabilities to Web, App, and Business Systems

Embed INNORIX file transfer into applications with API setup, transfer requests, responses, status UI, controls, and verification.

IT EngineersDevelopers
  • Automating the End-to-End Workflow for AI Training Data and Model Files
  • Deploy AI Model Files to Multiple Edge Devices
  • Securely Importing and Exporting Files in Air-Gapped and Network-Segmented Environments
  • Simultaneously Distributing Approved Files to Multiple Locations
  • Transfer Files from Azure Blob to an On-Premises Server
  • Automatically Transferring DB Dumps, Backup, and Archive Files to Remote Storage
  • Centrally Collecting Files from Branches, Factories, and Edge Devices
  • Deploy CI/CD Build Artifacts to Multiple Servers
  • Move Files Between Different Cloud Storage Services
  • Transfer Files Between S3 Buckets in Different AWS Accounts
  • Provide Customer-Specific File Workspaces
  • Provide Customers with Large-File Download Links That Expire
  • Let Customers Upload Large Files Through a Browser
  • Send Large Files Directly to a Customer Workspace
  • Store Database Backup Files in Object Storage
  • Receive File Transfer Failure and Recovery Alerts in Datadog
  • Add File Transfer Capabilities to Web, App, and Business Systems
  • Run Validation, Conversion, and Follow-Up Tasks After File Arrival
  • Migrate Large Volumes of Files from NAS and File Servers to the Cloud
  • Convert FTP Batch Jobs to Managed File Flows
  • Transfer Directly from Google Cloud Storage to Amazon S3
  • Create a File Transfer Status Dashboard in Grafana
  • Automatically Verify File Integrity Using Hash Values
  • Automatically Transfer New Files to a Designated Destination
  • Send Result Files from Kubernetes to Object Storage
  • Automatically Transfer Large Files and Build Artifacts Outside Git
  • Automatically Deliver Files That Are Difficult to Manage with Git
  • Collect Logs and Diagnostic Files from Distributed Servers into a Central Location
  • Automatically Transfer Media Sources and Processing Results Step by Step
  • Scan Received Files with Microsoft Defender and Perform Follow-up Processing
  • Automate Multi-Step File Transfers in a Single Workflow
  • Automatically Transfer Media Sources and Processing Results Step by Step
  • Automatically Route Partner Files to Internal Business Systems
  • Exchange Files Regularly with Partners and Supply Chain Systems
  • Automating Recurring File Transfers Daily and Weekly
  • Automatically Resume File Transfers After a Network Interruption
  • Convert rsync Jobs to Managed File Flows
  • Schedule Recurring File Transfers
  • Distribute Software and Firmware to Multiple Devices and Review Results
  • Distributing Software Packages to Multiple Servers and Branches
  • Automatically Apply Team Folder Changes Across Multiple Devices
  • Automatically Reflect a Team Work Folder Across Multiple PCs
  • Find and Transfer Files Across Multiple Devices from One Place
  • Add Large File Upload and Download to Your Website

Getting Started#

Concept#

Run File Transfer Functions from an Application

File processing in an application consists of preparing files, requesting a transfer, and continuing the next business task based on the processing result.

Application integration connects file transfer functions to application requests so transfers can run when needed and processing results can be used in application business logic.

Application
     │
     │ Transfer Request
     ▼
File Transfer
     │
     ├── File Processing
     ├── Progress
     └── Result
             │
             ▼
Application Logic

This connects file transfer and result processing to application business functions.

Integration Flow#

Connect the Flow from Transfer Request to Result Processing

When a file transfer request is generated in an application, the job runs based on the files to transfer and target information.

The application can use transfer progress and response information and continue the next business logic based on the final result.

File Transfer Request
      │
      ▼
File, Target Information Setup
      │
      ▼
Run Transfer Job
      │
      ▼
Receive Status, Response Information
      │
      ▼
Check Final Result
      │
      ▼
Process Application Logic

Connecting file transfer and result processing around a single request lets completed work flow naturally into the next business task.

Development Benefits#

Apply File Transfer Functions to the Application Business Flow

Application integration connects the execution process and result handling required for file transfer to service functions.

Category Application Integration
Transfer Execution Start file transfer based on an application request
Use Status Connect progress status and response information to the UI and business logic
Result Processing Use the final result in the next business function
Business Expansion Connect follow-up tasks such as storage, processing, and notifications after transfer completion

With this configuration, the flow from file transfer request to result processing can be used according to the application's business workflow.

IT Engineer#

Configure and Manage Application File Transfer Integration

Integration Setup#

Connect the Application to the File Transfer Environment

First, configure the file transfer environment and integration method so the application can use file transfer functions.

Configure request and response paths so application requests connect to file transfer jobs and execution status and result information can be returned.

┌─────────────────┐
│   Application   │
└────────┬────────┘
         │
         │ Request / Response
         ▼
┌─────────────────┐
│ Transfer Layer  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Transfer Device │
└─────────────────┘

Configuring the integration environment connects the application's business functions to actual file transfer jobs.

Request configuration#

Define the Files, Target, and Execution Conditions

Configure which requests in the integrated application should trigger file transfers.

A request can include the files to transfer, file paths, target location, and conditions required for execution.

Transfer Request
       │
       ├── Source
       │      └── File / Path
       │
       ├── Target
       │      └── Device / Workspace
       │
       └── Options
              │
              ▼
         Transfer Run

Configuration Item Configuration
Source File or file path to transfer
Target Device or workspace to which files are transferred
Request Request information sent by the application
Options Execution conditions applied to file processing
Flow File transfer job to run for the request

Configuring the request structure allows required file transfers to run according to application business conditions.

Response Handling#

Connect Transfer Status and Response Results to Application Logic

When a file transfer runs, status and response information is generated during startup, progress, and completion.

Connecting this information to the application UI and business logic allows the current progress to be displayed and a processing flow to be configured for each result.

Transfer Run
     │
     ├── Started
     │
     ├── Progress
     │
     └── Result
            │
       ┌────┼────┐
       ▼    ▼    ▼
    Success Retry Error
       │    │    │
       ▼    ▼    ▼
    Next   Retry Result
    Logic  Run   Handling

Transfer Information Application Use
Started Display transfer start status
Progress Display progress and processing status
Success Run the next business logic
Retry Request the job again according to the retry conditions
Error Connect the processing flow based on response information

This section manages transfer status and final response results in a single processing structure, consolidating content that was repeated in the existing status and event handling and error handling sections.

Integration Verification#

Check the Final Transfer Result in the Application

After integration is configured, run an actual file transfer request from the application and check the overall processing result.

Verify that the requested files were processed to the specified target, and validate the application result together with the file transfer execution record.

Application Request
        │
        ▼
   Transfer Run
        │
        ▼
  File Processing
        │
        ▼
  Result Response
        │
   ┌────┴────┐
   ▼         ▼
Application  Run
 Result      Record
   │         │
   └────┬────┘
        ▼
   Final Check

During integration verification, check the overall flow using the following information.

Check Item Details
Request Transfer request generated by the application
Execution File transfer job created for the request
Files Processed files, file count, and size
Target Specified device or workspace
Status Transfer progress and final result
Response Result information returned to the application

Developer#

Run transfers from business applications and connect progress, control, and results to business data

Integration Preparation#

Prepare Common API Call Code and Status Values

import os
import requests

BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com").rstrip("/")
TOKEN = os.environ["INNORIX_ACCESS_TOKEN"]
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")   # optional; falls back to the current workspace

STATUS_COMPLETE = 2
TERMINAL = {2, 4, 5, 9, 99}          # complete / error / cancelled / partial / failed
NOT_SUCCEEDED = {4, 5, 9, 99}


def api(method, path, body=None, params=None):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {TOKEN}",
    }

    if WORKSPACE_ID:
        headers["x-workspace-id"] = WORKSPACE_ID

    response = requests.request(
        method, BASE_URL + path,
        headers=headers, json=body, params=params, timeout=30,
    )

    payload = response.json() if response.content else {}

    if not response.ok:
        raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")

    return payload.get("data")


def is_terminal(detail):
    return detail.get("isTerminal", detail.get("status") in TERMINAL)
// InnorixClient.java
public static final String BASE_URL =
        env("INNORIX_BASE_URL", "https://app.innorix.com").replaceAll("/+
quot;
, ""); public static final String WORKSPACE_ID = env("INNORIX_WORKSPACE_ID", null); public static final int STATUS_COMPLETE = 2; // States the transfer no longer moves out of public static final Set<Integer> TERMINAL = Set.of(2, 4, 5, 9, 99); // Terminal states that are not a full success public static final Set<Integer> NOT_SUCCEEDED = Set.of(4, 5, 9, 99); private HttpRequest.Builder headers(HttpRequest.Builder builder) { builder.header("Content-Type", "application/json") .header("Authorization", "Bearer " + session.accessToken()); // When omitted the account's current workspace is used. if (workspaceId != null) builder.header("x-workspace-id", workspaceId); return builder; } /** Unwraps and returns data from the response. Throws ApiError on failure. */ public Object api(String method, String path, Object body, Map<String, Object> params) { Resp response = request(method, path, body, params); Object payload = null; try { payload = Json.parse(response.text()); } catch (RuntimeException ignored) { payload = null; } if (!response.ok()) { Map<String, Object> map = Json.asObj(payload); String message = Json.str(map, "message", Json.str(map, "error", "unknown error")); throw new ApiError(response.status, message, map); } return Json.get(payload, "data"); } /** Use the server flag when present, otherwise fall back to the status code. */ public static boolean isTerminal(Map<String, Object> record) { Boolean flag = Json.boolOrNull(record, "isTerminal"); if (flag != null) return flag; Integer status = Json.intOrNull(record, "status"); return status != null && TERMINAL.contains(status); }
// innorix-client.js
const BASE_URL = (process.env.INNORIX_BASE_URL
  || "https://app.innorix.com").replace(/\/+$/, "");
const TOKEN = process.env.INNORIX_ACCESS_TOKEN;
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID || null;

export const STATUS_COMPLETE = 2;
export const TERMINAL = new Set([2, 4, 5, 9, 99]);       // complete / error / cancelled / partial / failed
export const NOT_SUCCEEDED = new Set([4, 5, 9, 99]);

export async function api(method, path, body = null, params = null) {
  const url = new URL(BASE_URL + path);

  if (params) {
    for (const [key, value] of Object.entries(params)) {
      if (value === undefined || value === null) continue;
      url.searchParams.set(key, String(value));
    }
  }

  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${TOKEN}`,
  };

  // When omitted the account's current workspace is used.
  if (WORKSPACE_ID) headers["x-workspace-id"] = WORKSPACE_ID;

  const response = await fetch(url, {
    method,
    headers,
    body: body === null ? undefined : JSON.stringify(body),
    signal: AbortSignal.timeout(30_000),
  });

  const payload = await response.json().catch(() => ({}));

  if (!response.ok) {
    throw new Error(payload.message || `HTTP ${response.status}`);
  }

  return payload.data;
}

export function isTerminal(detail) {
  return detail.isTerminal !== undefined
    ? detail.isTerminal
    : TERMINAL.has(detail.status);
}
// InnorixClient.cs
public static readonly string BaseUrl =
    Env("INNORIX_BASE_URL", "https://app.innorix.com").TrimEnd('/');
public static readonly string WorkspaceIdFromEnv = Env("INNORIX_WORKSPACE_ID", null);

public const int StatusComplete = 2;
/// <summary>States the transfer no longer moves out of</summary>
public static readonly HashSet<int> Terminal = new HashSet<int> { 2, 4, 5, 9, 99 };
/// <summary>Terminal states that are not a full success</summary>
public static readonly HashSet<int> NotSucceeded = new HashSet<int> { 4, 5, 9, 99 };

// Applied on every request
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + Session.AccessToken);
// When omitted the account's current workspace is used.
if (WorkspaceId != null) request.Headers.TryAddWithoutValidation("x-workspace-id", WorkspaceId);

public async Task<JsonNode> ApiAsync(string method, string path, JsonNode body = null,
    IDictionary<string, object> parameters = null)
{
    Resp response = await RequestAsync(method, path, body, parameters).ConfigureAwait(false);
    JsonNode payload = null;

    try
    {
        payload = J.Parse(response.Text());
    }
    catch (Exception)
    {
        payload = null;
    }

    if (!response.Ok)
    {
        JsonObject map = J.AsObj(payload);
        string message = J.Str(map, "message", J.Str(map, "error", "unknown error"));
        throw new ApiError(response.Status, message, map);
    }

    return J.Get(payload, "data");
}

/// <summary>Use the server flag when present, otherwise fall back to the status code.</summary>
public static bool IsTerminal(JsonObject record)
{
    bool? flag = J.BoolOrNull(record, "isTerminal");
    if (flag != null) return flag.Value;

    int? status = J.IntOrNull(record, "status");
    return status != null && Terminal.Contains(status.Value);
}

Transfer status is determined using the values below. There are five terminal states, and the value representing success is Complete (2).

Status Value Meaning Terminal
2 Complete Yes
4 Error Yes
5 Cancelled Yes
9 Partially Complete Yes
99 Failed Yes
1 , 6 , 12 , 13 Starting, Transferring, Synchronizing, Receiving No

Determine terminal status and success separately. Partially Complete (9) and Cancelled (5) are also terminal states, so treating a transfer as successful based only on isTerminal can record a failure as a success.

Preflight Validation#

Validate Paths Before Transfer

A transfer can be created successfully even when a path is invalid. The failure appears at execution time, after the business data has already been recorded as in progress.

def validate_paths(source_id, target_id, source_paths, target_path):
    # sourceItems reads filePath, not path
    return api("POST", "/api/transfers/validate-path", {
        "sourceId": source_id,
        "targetId": target_id,
        "sourceItems": [{"filePath": p} for p in source_paths],
        "targetPath": target_path,
    }) or {}


result = validate_paths("device-a", "device-b",
                        ["/data/report.pdf"], "/archive")

if result.get("invalidSourcePaths"):
    raise ValueError(f"missing source paths: {result['invalidSourcePaths']}")

if result.get("validTargetPath") is False:
    raise ValueError("target path not found")
Map<String, Object> validatePaths(String sourceId, String targetId,
                                  List<String> sourcePaths, String targetPath) {
    List<Object> items = new ArrayList<>();
    // sourceItems reads filePath, not path
    for (String p : sourcePaths) items.add(Json.newObj("filePath", p));

    return client.apiObj("POST", "/api/transfers/validate-path", Json.newObj(
            "sourceId", sourceId,
            "targetId", targetId,
            "sourceItems", items,
            "targetPath", targetPath));
}

Map<String, Object> result = validatePaths("device-a", "device-b",
        List.of("/data/report.pdf"), "/archive");

if (!Json.arrOf(result, "invalidSourcePaths").isEmpty()) {
    throw new IllegalArgumentException("missing source paths: "
            + Json.arrOf(result, "invalidSourcePaths"));
}

if (Boolean.FALSE.equals(Json.boolOrNull(result, "validTargetPath"))) {
    throw new IllegalArgumentException("target path not found");
}
async function validatePaths(sourceId, targetId, sourcePaths, targetPath) {
  return (await client.api("POST", "/api/transfers/validate-path", {
    sourceId,
    targetId,
    // sourceItems reads filePath, not path
    sourceItems: sourcePaths.map((p) => ({ filePath: p })),
    targetPath,
  })) || {};
}

const result = await validatePaths("device-a", "device-b",
  ["/data/report.pdf"], "/archive");

if (result.invalidSourcePaths?.length) {
  throw new Error(`missing source paths: ${result.invalidSourcePaths}`);
}

if (result.validTargetPath === false) {
  throw new Error("target path not found");
}
async Task<JsonObject> ValidatePathsAsync(string sourceId, string targetId,
    IEnumerable<string> sourcePaths, string targetPath)
{
    var items = new JsonArray();
    // sourceItems reads filePath, not path
    foreach (string p in sourcePaths) items.Add(new JsonObject { ["filePath"] = p });

    return await client.ApiObjAsync("POST", "/api/transfers/validate-path",
        new JsonObject
        {
            ["sourceId"] = sourceId,
            ["targetId"] = targetId,
            ["sourceItems"] = items,
            ["targetPath"] = targetPath,
        });
}

JsonObject result = await ValidatePathsAsync("device-a", "device-b",
    new[] { "/data/report.pdf" }, "/archive");

if (J.ArrOf(result, "invalidSourcePaths").Count > 0)
{
    throw new ArgumentException(
        
quot;missing source paths: {J.ArrOf(result, "invalidSourcePaths")}"
); } if (J.BoolOrNull(result, "validTargetPath") == false) { throw new ArgumentException("target path not found"); }
Response Item details
validSourcePaths Validated source path
invalidSourcePaths Source path that cannot be found
validTargetPath Whether the target path is valid

Send each path in filePath within sourceItems. If users enter paths directly in the UI, apply this validation when the input is saved.

Transfer Creation#

Request a Transfer by Specifying Devices, Paths, and Processing Rules

When sending a file list, explicitly set isDir: false in sourceItem. sourcePaths treats every path as a folder, so if files are supplied, the server attempts to scan each file as a folder, which can slow the operation or cause a timeout.

def create_transfer(source_id, target_id, source_paths, target_path,
                    action="numbering"):
    transfer = api("POST", "/api/transfers/manual", {
        "sourceDevice": source_id,
        "targetDevice": target_id,
        "targetPath": target_path,
        "sourceItem": [{"path": p, "isDir": False} for p in source_paths],
        "sendAllFolder": False,
        "transferOptions": {"target-action": action},
    })

    return transfer["monitorId"]
String createTransfer(String sourceId, String targetId,
                      List<String> sourcePaths, String targetPath, String action) {
    List<Object> items = new ArrayList<>();
    for (String p : sourcePaths) items.add(Json.newObj("path", p, "isDir", false));

    Map<String, Object> transfer = client.apiObj("POST", "/api/transfers/manual", Json.newObj(
            "sourceDevice", sourceId,
            "targetDevice", targetId,
            "targetPath", targetPath,
            "sourceItem", items,
            "sendAllFolder", false,
            "transferOptions", Json.newObj("target-action", action)));

    return Json.str(transfer, "monitorId");
}
async function createTransfer(sourceId, targetId, sourcePaths, targetPath,
  action = "numbering") {
  const transfer = await client.api("POST", "/api/transfers/manual", {
    sourceDevice: sourceId,
    targetDevice: targetId,
    targetPath,
    sourceItem: sourcePaths.map((p) => ({ path: p, isDir: false })),
    sendAllFolder: false,
    transferOptions: { "target-action": action },
  });

  return transfer.monitorId;
}
async Task<string> CreateTransferAsync(string sourceId, string targetId,
    IEnumerable<string> sourcePaths, string targetPath, string action = "numbering")
{
    var items = new JsonArray();
    foreach (string p in sourcePaths)
        items.Add(new JsonObject { ["path"] = p, ["isDir"] = false });

    JsonObject transfer = await client.ApiObjAsync("POST", "/api/transfers/manual",
        new JsonObject
        {
            ["sourceDevice"] = sourceId,
            ["targetDevice"] = targetId,
            ["targetPath"] = targetPath,
            ["sourceItem"] = items,
            ["sendAllFolder"] = false,
            ["transferOptions"] = new JsonObject { ["target-action"] = action },
        });

    return J.Str(transfer, "monitorId");
}

When sending at the folder level, use sourcePaths and sendAllFolder: True.

api("POST", "/api/transfers/manual", {
    "sourceDevice": source_id,
    "targetDevice": target_id,
    "targetPath": target_path,
    "sourcePaths": ["/data/reports"],
    "sendAllFolder": True,
    "transferOptions": {"target-action": "numbering"},
})
client.api("POST", "/api/transfers/manual", Json.newObj(
        "sourceDevice", sourceId,
        "targetDevice", targetId,
        "targetPath", targetPath,
        "sourcePaths", List.of("/data/reports"),
        "sendAllFolder", true,
        "transferOptions", Json.newObj("target-action", "numbering")));
await client.api("POST", "/api/transfers/manual", {
  sourceDevice: sourceId,
  targetDevice: targetId,
  targetPath,
  sourcePaths: ["/data/reports"],
  sendAllFolder: true,
  transferOptions: { "target-action": "numbering" },
});
await client.ApiAsync("POST", "/api/transfers/manual", new JsonObject
{
    ["sourceDevice"] = sourceId,
    ["targetDevice"] = targetId,
    ["targetPath"] = targetPath,
    ["sourcePaths"] = new JsonArray { "/data/reports" },
    ["sendAllFolder"] = true,
    ["transferOptions"] = new JsonObject { ["target-action"] = "numbering" },
});

Choose how to handle files with the same name at the target according to the nature of the business task.

Value Behavior Suitable Use Case
numbering Preserve by adding a number Work that retains submissions by iteration
overwrite Overwrite Work that maintains only the latest state
nosend Skip without sending if already present Work that does not resend the same file

If settlement data uses overwrite, the previous iteration is lost, so do not simply use the default; specify the option according to the business requirement.

If files are skipped by nosend, the transfer can end in a terminal state other than success. If completion is judged only by status == 2, normal behavior will be counted as a failure, so code using this policy should handle terminal status and success separately.

Business Data Integration#

Store monitorId in Business Data for Tracking

The monitorId returned when a transfer is created is used for subsequent queries, control, and retries. If this value is not stored in business data, the transfer cannot be tracked later.

def start_order_transfer(order_id, source_id, target_id, paths, target_path):
    validate_paths(source_id, target_id, paths, target_path)

    monitor_id = create_transfer(source_id, target_id, paths, target_path)

    db.execute(
        "UPDATE orders SET monitor_id = %s, transfer_state = %s WHERE id = %s",
        (monitor_id, "transferring", order_id),
    )

    return monitor_id
String startOrderTransfer(String orderId, String sourceId, String targetId,
                          List<String> paths, String targetPath) {
    validatePaths(sourceId, targetId, paths, targetPath);

    String monitorId = createTransfer(sourceId, targetId, paths, targetPath, "numbering");

    db.execute(
            "UPDATE orders SET monitor_id = ?, transfer_state = ? WHERE id = ?",
            monitorId, "transferring", orderId);

    return monitorId;
}
async function startOrderTransfer(orderId, sourceId, targetId, paths, targetPath) {
  await validatePaths(sourceId, targetId, paths, targetPath);

  const monitorId = await createTransfer(sourceId, targetId, paths, targetPath);

  await db.execute(
    "UPDATE orders SET monitor_id = $1, transfer_state = $2 WHERE id = $3",
    [monitorId, "transferring", orderId],
  );

  return monitorId;
}
async Task<string> StartOrderTransferAsync(string orderId, string sourceId,
    string targetId, IEnumerable<string> paths, string targetPath)
{
    await ValidatePathsAsync(sourceId, targetId, paths, targetPath);

    string monitorId = await CreateTransferAsync(sourceId, targetId, paths, targetPath);

    await db.ExecuteAsync(
        "UPDATE orders SET monitor_id = @m, transfer_state = @s WHERE id = @id",
        new { m = monitorId, s = "transferring", id = orderId });

    return monitorId;
}

Conversely, there are cases where business data must be retrieved using monitorId, such as when an operator finds a problem in the transfer list.

sql
CREATE INDEX idx_orders_monitor_id ON orders (monitor_id);

Status Display#

Display Progress Status and Determine Whether the Transfer Has Ended

import time


def describe(monitor_id):
    return api("GET", f"/api/transfers/{monitor_id}")


def wait(monitor_id, timeout=1800, interval=3):
    deadline = time.time() + timeout

    while time.time() < deadline:
        detail = describe(monitor_id)

        if is_terminal(detail):
            return detail

        time.sleep(interval)

    raise TimeoutError(monitor_id)


detail = describe(monitor_id)

print(detail["statusLabel"], detail["percent"], "%")
print(detail["transferSize"], "/", detail["totalSize"])
Map<String, Object> describe(String monitorId) {
    return client.apiObj("GET", "/api/transfers/" + monitorId);
}

Map<String, Object> await(String monitorId, int timeoutSeconds, int intervalSeconds) {
    long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;

    while (System.currentTimeMillis() < deadline) {
        Map<String, Object> detail = describe(monitorId);
        if (InnorixClient.isTerminal(detail)) return detail;
        Thread.sleep(intervalSeconds * 1000L);
    }

    throw new InnorixClient.ApiError(0, "timeout: " + monitorId);
}

Map<String, Object> detail = describe(monitorId);
System.out.println(Json.str(detail, "statusLabel") + " "
        + Json.str(detail, "percent") + " %");
System.out.println(Json.str(detail, "transferSize") + " / "
        + Json.str(detail, "totalSize"));
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function describe(monitorId) {
  return client.api("GET", `/api/transfers/${monitorId}`);
}

async function wait(monitorId, { timeout = 1800, interval = 3 } = {}) {
  const deadline = Date.now() + timeout * 1000;

  while (Date.now() < deadline) {
    const detail = await describe(monitorId);
    if (isTerminal(detail)) return detail;
    await sleep(interval * 1000);
  }

  throw new Error(`timeout: ${monitorId}`);
}

const detail = await describe(monitorId);
console.log(detail.statusLabel, detail.percent, "%");
console.log(detail.transferSize, "/", detail.totalSize);
async Task<JsonObject> DescribeAsync(string monitorId)
{
    return await client.ApiObjAsync("GET", "/api/transfers/" + monitorId);
}

async Task<JsonObject> WaitAsync(string monitorId, int timeoutSeconds = 1800,
    int intervalSeconds = 3)
{
    long deadline = Environment.TickCount64 + timeoutSeconds * 1000L;

    while (Environment.TickCount64 < deadline)
    {
        JsonObject detail = await DescribeAsync(monitorId);
        if (InnorixClient.IsTerminal(detail)) return detail;
        await Task.Delay(intervalSeconds * 1000);
    }

    throw new InnorixClient.ApiError(0, 
quot;timeout: {monitorId}"
); } JsonObject detail = await DescribeAsync(monitorId); Console.WriteLine(
quot;{J.Str(detail, "statusLabel")} {J.Str(detail, "percent")} %"
); Console.WriteLine(
quot;{J.Str(detail, "transferSize")} / {J.Str(detail, "totalSize")}"
);
Response Item UI use
statusLabel Status display string
percent progress
transferSize , totalSize Transfer volume
fileCount , folderCount Target size
estimateTime remaining time
sourceDeviceName , targetDeviceName source and target

A short polling interval can generate excessive requests. For UI display purposes, an interval of about 3 seconds is appropriate.

Transfer Control#

Pause, Resume, or Cancel Based on User Requests

All three actions are called without a request body. However, even when the call succeeds, the instruction must reach the device before the status changes, so an immediate UI refresh may still show the previous state.

PAUSED = 3
RUNNING_STATES = {1, 6, 12, 13}
CANCELLED = 5


def control(monitor_id, action, tries=10):
    api("POST", f"/api/transfers/{monitor_id}/{action}", {})

    expected = {
        "pause": {PAUSED},
        "resume": RUNNING_STATES,
        "cancel": {CANCELLED},
    }[action]

    for _ in range(tries):
        time.sleep(1)
        detail = describe(monitor_id)

        if detail.get("status") in expected:
            return detail

    return describe(monitor_id)
static final int PAUSED = 3;
static final Set<Integer> RUNNING_STATES = Set.of(1, 6, 12, 13);
static final int CANCELLED = 5;

Map<String, Object> control(String monitorId, String action, int tries) throws Exception {
    client.api("POST", "/api/transfers/" + monitorId + "/" + action, Json.newObj());

    Set<Integer> expected = switch (action) {
        case "pause" -> Set.of(PAUSED);
        case "resume" -> RUNNING_STATES;
        case "cancel" -> Set.of(CANCELLED);
        default -> Set.of();
    };

    for (int i = 0; i < tries; i++) {
        Thread.sleep(1000);
        Map<String, Object> detail = describe(monitorId);
        Integer status = Json.intOrNull(detail, "status");
        if (status != null && expected.contains(status)) return detail;
    }

    return describe(monitorId);
}
const PAUSED = 3;
const RUNNING_STATES = new Set([1, 6, 12, 13]);
const CANCELLED = 5;

async function control(monitorId, action, tries = 10) {
  await client.api("POST", `/api/transfers/${monitorId}/${action}`, {});

  const expected = {
    pause: new Set([PAUSED]),
    resume: RUNNING_STATES,
    cancel: new Set([CANCELLED]),
  }[action];

  for (let i = 0; i < tries; i += 1) {
    await sleep(1000);
    const detail = await describe(monitorId);
    if (expected.has(detail.status)) return detail;
  }

  return describe(monitorId);
}
const int Paused = 3;
static readonly HashSet<int> RunningStates = new() { 1, 6, 12, 13 };
const int Cancelled = 5;

async Task<JsonObject> ControlAsync(string monitorId, string action, int tries = 10)
{
    await client.ApiAsync("POST", 
quot;/api/transfers/{monitorId}/{action}"
, new JsonObject()); HashSet<int> expected = action switch { "pause" => new HashSet<int> { Paused }, "resume" => RunningStates, "cancel" => new HashSet<int> { Cancelled }, _ => new HashSet<int>(), }; for (int i = 0; i < tries; i++) { await Task.Delay(1000); JsonObject detail = await DescribeAsync(monitorId); int? status = J.IntOrNull(detail, "status"); if (status != null && expected.Contains(status.Value)) return detail; } return await DescribeAsync(monitorId); }

In the UI, it is natural to disable the button immediately, show that processing is in progress, and update the status after the change is confirmed.

When multiple transfers need to be stopped at once, use bulk cancellation.

result = api("POST", "/api/transfers/bulk-cancel", {"monitorIds": monitor_ids})

print(result.get("cancelled"), result.get("failed"))
Map<String, Object> result = client.apiObj("POST", "/api/transfers/bulk-cancel",
        Json.newObj("monitorIds", monitorIds));

System.out.println(Json.arrOf(result, "cancelled") + " " + Json.arrOf(result, "failed"));
const result = await client.api("POST", "/api/transfers/bulk-cancel",
  { monitorIds });

console.log(result.cancelled, result.failed);
JsonObject result = await client.ApiObjAsync("POST", "/api/transfers/bulk-cancel",
    new JsonObject { ["monitorIds"] = new JsonArray(monitorIds.Select(m => (JsonNode)m).ToArray()) });

Console.WriteLine(
quot;{J.ArrOf(result, "cancelled")} {J.ArrOf(result, "failed")}"
);

A transfer that has already ended has nothing to cancel, so cancellation fails and is included in the response's failed field. This is a normal response, not an error.

Result Finalization#

Finalize the Transfer Result in Business Data and Retry Failed Files

def finalize(order_id, monitor_id):
    detail = describe(monitor_id)

    if not is_terminal(detail):
        return None

    status = detail["status"]
    succeeded = status == STATUS_COMPLETE

    db.execute(
        "UPDATE orders SET transfer_state = %s, transfer_status = %s WHERE id = %s",
        ("done" if succeeded else "failed", status, order_id),
    )

    return succeeded
boolean finalize(String orderId, String monitorId) {
    Map<String, Object> detail = describe(monitorId);

    if (!InnorixClient.isTerminal(detail)) return false;

    int status = Json.intOr(detail, "status", -1);
    boolean succeeded = status == InnorixClient.STATUS_COMPLETE;

    db.execute(
            "UPDATE orders SET transfer_state = ?, transfer_status = ? WHERE id = ?",
            succeeded ? "done" : "failed", status, orderId);

    return succeeded;
}
async function finalize(orderId, monitorId) {
  const detail = await describe(monitorId);

  if (!isTerminal(detail)) return null;

  const succeeded = detail.status === STATUS_COMPLETE;

  await db.execute(
    "UPDATE orders SET transfer_state = $1, transfer_status = $2 WHERE id = $3",
    [succeeded ? "done" : "failed", detail.status, orderId],
  );

  return succeeded;
}
async Task<bool?> FinalizeAsync(string orderId, string monitorId)
{
    JsonObject detail = await DescribeAsync(monitorId);

    if (!InnorixClient.IsTerminal(detail)) return null;

    int status = J.Int(detail, "status", -1);
    bool succeeded = status == InnorixClient.StatusComplete;

    await db.ExecuteAsync(
        "UPDATE orders SET transfer_state = @s, transfer_status = @st WHERE id = @id",
        new { s = succeeded ? "done" : "failed", st = status, id = orderId });

    return succeeded;
}

Storing the status value as well makes it possible to distinguish failure types later. Cancelled (5) and Failed (99) require different follow-up actions.

If only some files fail, retry only those files.

def failed_files(monitor_id):
    result = api("GET", f"/api/transfers/{monitor_id}/files", params={
        "state": "any", "size": 500,
    }) or {}

    return [r for r in (result.get("children") or [])
            if r.get("status") in NOT_SUCCEEDED]


def retry_failed(monitor_id):
    rows = failed_files(monitor_id)

    if not rows:
        return 0

    api("POST", f"/api/transfers/{monitor_id}/retry", {
        "filesRetry": [
            {"filePath": r["sourceFilePath"], "isDir": bool(r.get("isFolder"))}
            for r in rows
        ]
    })

    return len(rows)
List<Map<String, Object>> failedFiles(String monitorId) {
    Map<String, Object> result = client.apiObj("GET",
            "/api/transfers/" + monitorId + "/files", null,
            Json.newObj("state", "any", "size", 500));

    List<Map<String, Object>> failed = new ArrayList<>();
    for (Object node : Json.arrOf(result, "children")) {
        Map<String, Object> row = Json.asObj(node);
        Integer status = Json.intOrNull(row, "status");
        if (status != null && InnorixClient.NOT_SUCCEEDED.contains(status)) failed.add(row);
    }
    return failed;
}

int retryFailed(String monitorId) {
    List<Map<String, Object>> rows = failedFiles(monitorId);
    if (rows.isEmpty()) return 0;

    List<Object> filesRetry = new ArrayList<>();
    for (Map<String, Object> row : rows) {
        filesRetry.add(Json.newObj(
                "filePath", Json.str(row, "sourceFilePath"),
                "isDir", Json.bool(row, "isFolder", false)));
    }

    client.api("POST", "/api/transfers/" + monitorId + "/retry",
            Json.newObj("filesRetry", filesRetry));

    return rows.size();
}
async function failedFiles(monitorId) {
  const result = (await client.api("GET", `/api/transfers/${monitorId}/files`,
    null, { state: "any", size: 500 })) || {};

  return (result.children || []).filter((r) => NOT_SUCCEEDED.has(r.status));
}

async function retryFailed(monitorId) {
  const rows = await failedFiles(monitorId);
  if (rows.length === 0) return 0;

  await client.api("POST", `/api/transfers/${monitorId}/retry`, {
    filesRetry: rows.map((r) => ({
      filePath: r.sourceFilePath,
      isDir: Boolean(r.isFolder),
    })),
  });

  return rows.length;
}
async Task<List<JsonObject>> FailedFilesAsync(string monitorId)
{
    JsonObject result = await client.ApiObjAsync("GET",
        
quot;/api/transfers/{monitorId}/files"
, null, new Dictionary<string, object> { ["state"] = "any", ["size"] = 500 }); return J.ArrOf(result, "children").Select(J.AsObj).Where(r => { int? status = J.IntOrNull(r, "status"); return status != null && InnorixClient.NotSucceeded.Contains(status.Value); }).ToList(); } async Task<int> RetryFailedAsync(string monitorId) { List<JsonObject> rows = await FailedFilesAsync(monitorId); if (rows.Count == 0) return 0; var filesRetry = new JsonArray(); foreach (JsonObject r in rows) { filesRetry.Add(new JsonObject { ["filePath"] = J.Str(r, "sourceFilePath"), ["isDir"] = J.Bool(r, "isFolder", false), }); } await client.ApiAsync("POST",
quot;/api/transfers/{monitorId}/retry"
, new JsonObject { ["filesRetry"] = filesRetry }); return rows.Count; }

Each item contains sourceFilePath, statusName, and errorCode, so the UI can show which file failed and why. Retry can be called only after the transfer has ended.

If the entire transfer needs to be run again, retrieve the previous execution information and replay it.

config = api("GET", f"/api/transfers/{monitor_id}/replay-data")
api("POST", f"/api/transfers/{monitor_id}/replay", {"action": "replay"})
Map<String, Object> config = client.apiObj("GET",
        "/api/transfers/" + monitorId + "/replay-data");
client.api("POST", "/api/transfers/" + monitorId + "/replay",
        Json.newObj("action", "replay"));
const config = await client.api("GET", `/api/transfers/${monitorId}/replay-data`);
await client.api("POST", `/api/transfers/${monitorId}/replay`, { action: "replay" });
JsonObject config = await client.ApiObjAsync("GET",
    
quot;/api/transfers/{monitorId}/replay-data"
); await client.ApiAsync("POST",
quot;/api/transfers/{monitorId}/replay"
, new JsonObject { ["action"] = "replay" });
Check Item Details
Request Validated source and target
Execution Generated monitorId
Status Progress and terminal status
Result Success status and status value
Files Failed files and error codes
Follow-up Retry or replay result
PreviousReceive File Transfer Failure and Recovery Alerts in DatadogNextRun Validation, Conversion, and Follow-Up Tasks After File Arrival

On this page

  • Getting Started
  • Concept
  • Integration Flow
  • Development Benefits
  • IT Engineer
  • Integration Setup
  • Request configuration
  • Response Handling
  • Integration Verification
  • Developer
  • Integration Preparation
  • Preflight Validation
  • Transfer Creation
  • Business Data Integration
  • Status Display
  • Transfer Control
  • Result Finalization