Transfer files between systems to applications

Immediate transmission

Immediate transmission is a method that sends files and folders directly from a source device to a target device with a single API call, without scheduling or automation.

outline

What is Immediate Transmission?

Transfers specific files/folders from the source device to a designated path on the target device in a single operation. When a transfer is initiated, a monitorId is issued, which is used to check the progress status and perform controls such as pausing, resuming, canceling, and retrying.

Common Preparation

Base URL

https://app.innorix.com

Authentication Header — The following header is required for all requests.

HeaderDescription
Authorization: Bearer {accessToken}Access token (JWT) issued upon login
x-workspace-id: {workspaceId}Target workspace ID
Content-Type: application/jsonRequest body format

Access tokens are issued via POST /api/auth/login(email, password) using the data.user.accessToken from the response. Upon expiration, they are renewed via POST /api/auth/token/refresh(X-Refresh-Token header), and API keys can be issued via POST /api/auth/api-keys for long-term integration.

Device Concept — Both the source and target of the transmission are devices with the agent installed. Retrieve the list using GET /api/devices to obtain the deviceId. Each device has attributes such as os (Windows, Linux, Mac) and online status (status).

Specify destination (sourceItem) — Specify the items to send as an array of sourceItem.

FieldTypeDescription
hashstringItem identifier — {deviceId}_ino_{base64(UTF-8 path)}
isDirbooleanFolder status

Use sendAllFolder to send the entire folder.

Device Lookup — GET /api/devices/resolve — Immediately retrieves the deviceId using the name, IP, and MAC (at least one of name, ip, or mac).

json
{
  "status_code": 200,
  "message": "OK",
  "data": {
    "matchCount": 1,
    "devices": [
{
        "deviceId": "dev_01H8...",
        "name": "OfficePC",
        "ipAddress": "192.168.0.9",
        "osType": "windows",
        "state": 1, "stateName": "CONNECTED", "stateLabel": "Connected",
        "isConnected": true
}
]
}
}

If multiple device names match, it responds with 409 + data.candidates[], so it uses unambiguous names.

Folder Lookup (Non-streaming) — GET /api/devices/{deviceId}/files — Retrieves direct folder entries as JSON (use files/search SSE for recursive searches). The response provides both the path and fileToken, which can be used directly as an item identifier for transfer and file operations.

json
{
  "status_code": 200,
  "message": "OK",
  "data": {
    "path": "/data", "total": 128, "page": 1, "size": 50, "lastPage": 3,
    "items": [
{
"name": "Report.pdf",
"path": "/data/report.pdf",
        "fileToken": "L2RhdGEv...",
        "isDir": false, "size": 20480,
        "modifiedAt": "2026-08-01T09:12:00Z"
}
]
}
}

Inclusion of Enumerated Values — Enumerated fields in single-item and detailed responses provide a constant name and label (state/stateName/stateLabel) along with an integer value. Derived flags, such as connection status (isConnected), are also provided.

Key Endpoint

PurposeMethodEndpoint
LoginPOST/api/auth/login
Renew TokenPOST/api/auth/token/refresh
Device ListGET/api/devices
Device Lookup (Name·IP·MAC)GET/api/devices/resolve
Path Capacity PreviewGET/api/devices/{deviceId}/path-stats
Source file searchPOST/api/devices/{deviceId}/files/search
Folder Lookup (Non-streaming)GET/api/devices/{deviceId}/files
Path PrevalidationPOST/api/transfers/validate-path
Immediate transfer creationPOST/api/transfer/manual
View transferred filesGET/api/transfers/{monitorId}/files
Transfer ControlPOST/api/transfers/{monitorId}/pause · resume · cancel · retry

Basic Flow

  1. Log in to obtain an access token
  2. Check sourceId and targetId using GET /api/devices
  3. (Optional) Configure sourceItem with file search
  4. (Optional) Validate path with validate-path
  5. Create and send via POST /api/transfers/manual → Receive monitorId
  6. Monitor and control progress using monitorId

1:1 Transmission

explanation

This is the most basic pattern for immediately transferring a specified file from a single source device to a single target device. You specify the source, target, and item to send using sourceId, targetId, and sourceItem (item hash), and check the status until completion using monitorId.

API Usage

PurposeMethodEndpoint
LoginPOST/api/auth/login
Create TransferPOST/api/transfers/manual
Check statusGET/api/transfers/{monitorId}

Request

POST /api/transfers/manual

json
{
  "sourceId": "device-source-01",
  "targetId": "device-target-01",
  "targetPath": "/data/incoming",
  "sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }],
  "sendAllFolder": false
}
  • sourceId and targetId are the deviceId obtained from GET /api/devices.
  • The hash of sourceItem is in the format {deviceId}_ino_{base64(UTF-8 path)}.
  • sendAllFolder is a boolean.

For sourceId/targetId, specify the deviceId obtained with GET /api/devices. To look up by name and IP, use GET /api/devices/resolve.

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

Processing Order

  1. Obtain an access token with POST /api/auth/login (data.user.accessToken)
  2. Call POST /api/transfers/manualsourceId, targetId, targetPath, sourceItem → Receive data.monitorId
  3. Polle GET /api/transfers/{monitorId} and check status2=Completed, 4·5·9·99=Ended with failure

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function encodePath(deviceId, path) {
// Normalize the path separator and encode it in Base64 based on UTF-8.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
return detail;
}
        await new Promise((resolve) => setTimeout(resolve, 2000));
}
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// The item to send is specified by the hash of sourceItem.
    const sourceItem = [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }];
    const transfer = await api("POST", "/api/transfers/manual", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem,
        sendAllFolder: false,
});
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

File Collection

explanation

This is a pattern that aggregates folders from multiple sources (branches) into a single target path. It searches the target folder for each branch to generate a transfer and aggregates the completion of all transfers together.

API Usage

PurposeMethodEndpoint
LoginPOST/api/auth/login
Device Lookup (Name)GET/api/devices/resolve
Folder Lookup (Non-streaming)GET/api/devices/{deviceId}/files
Create TransferPOST/api/transfers/manual
Check statusGET/api/transfers/{monitorId}

Request

POST /api/transfers/manual (repeat for each source)

json
{
  "sourceId": "dev_01H8BRANCH...",
  "targetId": "dev_01H8HQ...",
  "targetPath": "/collect/logs",
  "sourceItem": [{ "hash": "dev_01H8BRANCH..._ino_L3Zhci9sb2cvYXBwLWxvZw==", "isDir": true }],
  "sendAllFolder": true,
  "transferOptions": { "target-action": "numbering" }
}
  • Change only the sourceId for each location, and keep the targetId and targetPath fixed.
  • target-action: numbering in transferOptions adds numbers in case of name conflicts when gathering in the same path.
  • Since it is a collection of the entire folder, sendAllFolder is true.

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

Processing Order

  1. Obtain an access token using POST /api/auth/login
  2. Repeat at each point:
  • Obtain the branch sourceId using GET /api/devices/resolve?name= Search for the target folder using GET /api/devices/{deviceId}/files → Obtain the path
  • Call POST /api/transfers/manual (targetId=target, sourceItem=folder item) → Collect monitorId
  1. Polle all monitorIds via GET /api/transfers/{monitorId} and tally completion.

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const BRANCH_DEVICES = (process.env.INNORIX_BRANCH_DEVICES || "branch-pc-01,branch-pc-02,branch-pc-03").split(",");
const HQ_DEVICE = process.env.INNORIX_HQ_DEVICE || "headquarters-server";
const SOURCE_ROOT = process.env.INNORIX_SOURCE_ROOT || "/var/log";
const SEARCH = process.env.INNORIX_LOG_FOLDER_SEARCH || "log";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/collect/logs";
async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}
async function resolveDevice(name, token) {
    const data = await api("GET", `/api/devices/resolve?name=${encodeURIComponent(name)}`, token);
if (data.matchCount !== 1) throw new Error(`Device must resolve uniquely: ${name}`);
return data.devices[0].deviceId;
}
function encodePath(deviceId, path) {
// Normalize path separators before UTF-8 Base64 encoding.
    const normalizedPath = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalizedPath, "utf8").toString("base64")}`;
}

async function main() {
Search the log folders of the three locations and create an independent transfer for each.
    const login = await api("POST", "/api/auth/login", null, {
        email: process.env.INNORIX_EMAIL,
        password: process.env.INNORIX_PASSWORD,
});
const token = login.user.accessToken;
    const targetId = await resolveDevice(HQ_DEVICE, token);
const monitorIds = new Map();
for (const rawName of BRANCH_DEVICES) {
const branchName = rawName.trim();
        const sourceId = await resolveDevice(branchName, token);
        const query = new URLSearchParams({ path: SOURCE_ROOT, type: "dir", search: SEARCH, size: "500" });
        const listing = await api("GET", `/api/devices/${sourceId}/files?${query}`, token);
if (!listing.items.length) throw new Error(`Log folder not found: ${branchName}`);
const folder = listing.items[0];
        const transfer = await api("POST", "/api/transfers/manual", token, {
            sourceId,
            targetId,
            targetPath: TARGET_PATH,
            sourceItem: [{ hash: encodePath(sourceId, folder.path), isDir: true }],
            sendAllFolder: true,
            transferOptions: { "target-action": "numbering" },
});
        monitorIds.set(branchName, transfer.monitorId);
}

// Aggregates the total completion status of parallel tasks until all monitorIds are finished.
const pending = new Map(monitorIds);
while (pending.size) {
await Promise.all(
            [...pending].map(async ([branchName, monitorId]) => {
                const detail = await api("GET", `/api/transfers/${monitorId}`, token);
console.log(`${branchName}: status=${detail.status} percent=${detail.percent || 0}`);
                if ([2, 4, 5, 9, 99].includes(detail.status)) {
                    if (detail.status !== 2) throw new Error(`${branchName}: ${detail.errorCode || "failed"}`);
pending.delete(branchName);
}
})
);
        if (pending.size) await new Promise((resolve) => setTimeout(resolve, 2000));
}
console.log(`Collected logs from ${monitorIds.size} branches`);
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

File Distribution

explanation

This is a pattern for distributing a single set of source files to multiple target devices. It queries the distribution folder from the source once, sends the same package to each target, and aggregates the success and failure results.

API Usage

PurposeMethodEndpoint
LoginPOST/api/auth/login
Device Lookup (Name)GET/api/devices/resolve
Folder Lookup (Non-streaming)GET/api/devices/{deviceId}/files
Create TransferPOST/api/transfers/manual
Check statusGET/api/transfers/{monitorId}

Request

POST /api/transfers/manual (repeat for each target)

json
{
  "sourceId": "dev_01H8SRC...",
  "targetId": "dev_01H8BRANCH01...",
  "targetPath": "/deploy",
  "sourceItem": [{ "hash": "dev_01H8SRC..._ino_L2RlcGxveS9kZXBsb3ltZW50LXBhY2thZ2U=", "isDir": true }],
  "sendAllFolder": true,
  "transferOptions": { "target-action": "overwrite" }
}
  • Change only targetId for each location, and keep sourceId and sourceItem fixed.
  • Overwrites the target's existing files with transferOptions's target-action: overwrite.
  • Since it is a distribution of the entire folder, sendAllFolder is true.

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

Processing Order

  1. Obtain an access token using POST /api/auth/login
  2. Obtain the source deviceId using GET /api/devices/resolve?name=
  3. Search for the deployment package folder using GET /api/devices/{deviceId}/files to obtain the path.
  4. Call POST /api/transfers/manual for each target (targetId=Branch, sourceItem=Package Item) → monitorId as many times as there are targets
  5. Polle each monitorId via GET /api/transfers/{monitorId} to tally successes and failures

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_IDS = (process.env.INNORIX_TARGET_IDS || "device-target-01,device-target-02,device-target-03").split(",");
const SOURCE_ROOT = process.env.INNORIX_SOURCE_ROOT || "/deploy";
const PACKAGE_NAME = process.env.INNORIX_PACKAGE_NAME || "deployment-package";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/deploy";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function encodePath(deviceId, path) {
// Normalize the path separator and encode it in Base64 based on UTF-8.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Search the distribution package folder at headquarters once.
    const query = new URLSearchParams({ path: SOURCE_ROOT, type: "dir", search: PACKAGE_NAME, size: "500" });
    const listing = await api("GET", `/api/devices/${SOURCE_ID}/files?${query}`, token);
const packageFolder = listing.items.find((item) => item.name === PACKAGE_NAME);
if (!packageFolder) throw new Error(`Package folder not found: ${PACKAGE_NAME}`);
    const sourceItem = [{ hash: encodePath(SOURCE_ID, packageFolder.path), isDir: true }];

// Send the same package to multiple targets separately.
    const pending = new Map(),
results = new Map();
for (const rawId of TARGET_IDS) {
const targetId = rawId.trim();
        const transfer = await api("POST", "/api/transfers/manual", token, {
            sourceId: SOURCE_ID,
            targetId,
            targetPath: TARGET_PATH,
            sourceItem,
            sendAllFolder: true,
            transferOptions: { "target-action": "overwrite" },
});
        pending.set(targetId, transfer.monitorId);
}

// Aggregates completed targets by separating them into success and failure lists.
while (pending.size) {
await Promise.all(
            [...pending].map(async ([targetId, monitorId]) => {
                const detail = await api("GET", `/api/transfers/${monitorId}`, token);
if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
                    results.set(targetId, detail.status === TRANSFER_STATUS.transferComplete ? "success" : detail.errorCode || "failed");
pending.delete(targetId);
}
})
);
        if (pending.size) await new Promise((resolve) => setTimeout(resolve, 2000));
}
    const succeeded = [...results].filter(([, value]) => value === "success").map(([name]) => name);
    const failed = Object.fromEntries([...results].filter(([, value]) => value !== "success"));
    console.log({ succeeded, failed });
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Transmission Rules

explanation

We will summarize the request components that determine what / where / how a single transmission sends. Details will be covered in the following sections.

CategoryRelated FieldsDetails
What(source)sourceItem, sendAllFolder, filtersource filter
Where to (target)targetId, targetPathtarget option
Batch(Path)pathMapping, sendAllFolderPath Mapping
When overlappingtransferOptions.target-actionCollision handling

API Usage

PurposeMethodEndpoint
Path ValidationPOST/api/transfers/validate-path
Create TransferPOST/api/transfers/manual

Request

POST /api/transfers/validate-path

json
{
  "sourceId": "device-src-001",
  "targetId": "device-dst-002",
  "targetPath": "/data/incoming",
  "sourceItem": [{ "hash": "device-src-001_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }]
}

Response

json
{
  "status_code": 200,
  "message": "success",
  "data": { "valid": true }
}

Processing Order

  1. Verify source, target, and path validity using POST /api/transfers/validate-path
  2. Configure the rule (sourceItem/filter/transferOptions.pathMapping/transferOptions.target-action) and call POST /api/transfers/manual
  3. Follow-up processing with the response data.monitorId

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function encodePath(deviceId, path) {
// Normalize the path separator and encode it in Base64 based on UTF-8.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
return detail;
}
        await new Promise((resolve) => setTimeout(resolve, 2000));
}
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;
    const sourceItem = [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }];

// 1) Before sending, first verify if the source and target paths are valid.
    const validation = await api("POST", "/api/transfers/validate-path", token, { sourceId: SOURCE_ID, targetId: TARGET_ID, sourceItem, targetPath: TARGET_PATH });
    console.log("validate-path", validation);

// 2) Combine filters, path mapping, and conflict policies in a single request and send them.
    const transfer = await api("POST", "/api/transfers/manual", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem,
        sendAllFolder: false,
transferOptions: {
"send-filetype-cus": "\\.(log|csv)$", // Filter: log·csv extensions only
savepath: true, // Path mapping: preserve source folder structure
            optionPath: "relative",
"target-action": "numbering", // Conflict policy: assign (1) after name
        },
});
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

// 3) Check until completed.
    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Source Filter

explanation

Selects items to send from the source based on criteria. In addition to directly specifying items with sourceItem, adding the filter option to the transmission request filters them on the server side based on extension, size, and modification time. When used with sendAllFolder, it recursively collects subfolders based on the filter criteria.

API Usage

PurposeMethodEndpoint
File SearchPOST/api/devices/{deviceId}/files/search
Create filter transferPOST/api/transfers/filtered

filter option field:

FieldTypeDescription
filter.includeExtensionsstring[]List of extensions to include (e.g., ["log","csv"])
filter.excludePatternsstring[]glob patterns to exclude (e.g., ["*.tmp"])
filter.minSizenumberMinimum size (bytes)
filter.maxSizenumberMaximum size (bytes)
filter.modifiedAfterstringFiles modified after this time only (ISO 8601)
filter.modifiedBeforestringFiles modified before this time only (ISO 8601)
filter.recursivebooleanWhether to recursively search subfolders

Request

POST /api/transfers/filtered

json
{
  "sourceId": "device-source-01",
  "targetId": "device-target-01",
  "targetPath": "/data/incoming",
  "sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvbG9ncw==", "isDir": true }],
  "sendAllFolder": true,
  "filter": {
    "recursive": true,
    "includeExtensions": ["log", "csv"],
    "excludePatterns": ["*.tmp"],
    "minSize": 1024,
    "modifiedAfter": "2026-08-01T00:00:00Z"
}
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

Using the response's data.monitorId, perform subsequent status checks (GET /api/transfers/{monitorId}/files) and controls (pause, resume, cancel, retry).

Processing Order

  1. (Optional) Check the candidate list with POST /api/devices/{deviceId}/files/search
  2. Call POST /api/transfers/filtered including filter
  3. Check progress status using response data.monitorId

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/var/log";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/collect/logs";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function encodePath(deviceId, path) {
// Normalize the path separator and encode it in Base64 based on UTF-8.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
return detail;
}
        await new Promise((resolve) => setTimeout(resolve, 2000));
}
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Recursively transfer only *.log and *.csv files modified since yesterday that are 1KB or larger, excluding *.tmp files, and including subfolders.
const yesterday = new Date(Date.now() - 86400000).toISOString();
    const transfer = await api("POST", "/api/transfers/filtered", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: true }],
        sendAllFolder: true,
filter: {
            recursive: true,
            includeExtensions: ["log", "csv"],
            excludePatterns: ["*.tmp"],
            minSize: 1024,
            modifiedAfter: yesterday,
        },
});
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Target Options

explanation

This is how to specify a target. The target is determined by the device (targetId) and the destination path (targetPath). If the target folder does not exist, you can create it in advance.

API Usage

PurposeMethodEndpoint
Create target folderPOST/api/devices/{deviceId}/files/folders
Path ValidationPOST/api/transfers/validate-path
Check CapacityGET/api/devices/{deviceId}/capacity
Create TransferPOST/api/transfers/manual

Request

POST /api/transfers/manual

json
{
  "sourceId": "device-source-01",
  "targetId": "device-target-01",
  "targetPath": "/data/incoming/2026",
  "sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }]
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

Using the response's data.monitorId, perform subsequent status checks (GET /api/transfers/{monitorId}/files) and controls (pause, resume, cancel, retry).

Processing Order

  1. (Optional) Create target folder with POST /api/devices/{deviceId}/files/folders
  2. Check destination path free capacity with GET /api/devices/{deviceId}/capacity
  3. Call POST /api/transfers/manual (sourceId, targetId, targetPath)
  4. Receive response data.monitorId

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming/2026/08";
const REQUIRED_BYTES = process.env.INNORIX_SOURCE_SIZE || "107374182400";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function encodePath(deviceId, path) {
// Normalize the path separator and encode it in Base64 based on UTF-8.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
return detail;
}
        await new Promise((resolve) => setTimeout(resolve, 2000));
}
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// 1) Create a date folder that does not exist in the target.
    await api("POST", `/api/devices/${TARGET_ID}/files/folders`, token, { path: TARGET_PATH });

2) Check if there is enough remaining disk space, and if not, fail the transfer beforehand.
    const capacity = await api("GET", `/api/devices/${TARGET_ID}/capacity?path=${encodeURIComponent(TARGET_PATH)}&requiredBytes=${REQUIRED_BYTES}`, token);
    if (capacity.sufficient !== true) throw new Error("Target capacity was not confirmed");

// 3) Once the capacity is confirmed, send.
    const transfer = await api("POST", "/api/transfers/manual", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }],
        sendAllFolder: false,
});
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Path Mapping

explanation

Determines where the source item is placed in the target. The destination criterion is targetPath, and the transferOptions.pathMapping option specifies preserving folder structure, removing prefixes, and filename rules. The item path is specified as the hash of sourceItem.

API Usage

PurposeMethodEndpoint
Create TransferPOST/api/transfers/manual
View Batch ResultsGET/api/transfers/{monitorId}/files

transferOptions.pathMapping option field:

FieldTypeDescription
pathMapping.preserveStructurebooleanPreserve source folder structure (flatten if false)
pathMapping.removePrefixstringPrefix path to remove from source path
pathMapping.targetRootstringTarget root path serving as the deployment basis
pathMapping.fileNameTemplatestringFilename template (e.g., {name}, date prefix)
pathMapping.createTargetFoldersbooleanAutomatically create non-existent target folders

Request

POST /api/transfers/manual

json
{
  "sourceId": "device-source-01",
  "targetId": "device-target-01",
  "targetPath": "/archive",
  "sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvMjAyNi9yZXBvcnRz", "isDir": true }],
  "sendAllFolder": true,
  "transferOptions": {
    "pathMapping": {
      "preserveStructure": true,
      "removePrefix": "/data/2026",
      "targetRoot": "/archive",
      "fileNameTemplate": "20260825_{name}",
      "createTargetFolders": true
}
}
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

Using the response's data.monitorId, perform subsequent status checks (GET /api/transfers/{monitorId}/files) and controls (pause, resume, cancel, retry).

Processing Order

  1. Call POST /api/transfers/manual including pathMapping
  2. Receive response data.monitorId
  3. Check the actual batch results with GET /api/transfers/{monitorId}/files

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/2026/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/archive";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function encodePath(deviceId, path) {
// Normalize the path separator and encode it in Base64 based on UTF-8.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
return detail;
}
        await new Promise((resolve) => setTimeout(resolve, 2000));
}
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Creates the transfer date (YYYYMMDD) to prepend to the filename.
    const today = new Date().toISOString().slice(0, 10).replaceAll("-", "");

// Maintain the source folder structure but remove the /data/2026 prefix path,
// Append the transfer date to the beginning of the filename and place it under /archive.
    const transfer = await api("POST", "/api/transfers/manual", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }],
        sendAllFolder: false,
transferOptions: {
pathMapping: {
                preserveStructure: true,
                removePrefix: "/data/2026",
                targetRoot: "/archive",
                fileNameTemplate: `${today}_{name}`,
                createTargetFolders: true,
            },
        },
});
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Collision Handling

explanation

This is the handling method when a file with the same name already exists in the target path. Specify one of the following using target-action in transferOptions: numbering, skip, overwrite, or fail. Files that fail during transfer are retrieved via a retry.

API Usage

PurposeMethodEndpoint
Create TransferPOST/api/transfers/manual
Retry failurePOST/api/transfers/{monitorId}/retry

transferOptions.target-action value:

ValueDescription
numberingIn case of name conflicts, number both to preserve ((1), (2))
skipSkip if already present
overwriteOverwrite existing file
failHandle file failure on crash

Request

POST /api/transfers/manual

json
{
  "sourceId": "device-source-01",
  "targetId": "device-target-01",
  "targetPath": "/data/incoming",
  "sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }],
  "transferOptions": { "target-action": "numbering" }
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}

Using the response's data.monitorId, perform subsequent status checks (GET /api/transfers/{monitorId}/files) and controls (pause, resume, cancel, retry).

Processing Order

  1. Call POST /api/transfers/manual including transferOptions.target-action
  2. Receive response data.monitorId
  3. Resend failed files via POST /api/transfers/{monitorId}/retry

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
// Check how the results change by changing the conflict policy.
const CONFLICT_POLICIES = ["numbering", "skip", "overwrite", "fail"];
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function encodePath(deviceId, path) {
// Normalize the path separator and encode it in Base64 based on UTF-8.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForTerminal(monitorId, token) {
The fail policy intentionally fails, so it waits only until 'termination' rather than completion.
for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) return detail;
        await new Promise((resolve) => setTimeout(resolve, 2000));
}
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

    const sourceItem = [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }];
for (const policy of CONFLICT_POLICIES) {
// Send the same file to the same location, but change only the conflict policy.
        const transfer = await api("POST", "/api/transfers/manual", token, {
            sourceId: SOURCE_ID,
            targetId: TARGET_ID,
            targetPath: TARGET_PATH,
            sourceItem,
            sendAllFolder: false,
            transferOptions: { "target-action": policy },
});
        const detail = await waitForTerminal(transfer.monitorId, token);

// Retrieves file-level results by policy
        const files = await api("GET", `/api/transfers/${transfer.monitorId}/files?state=any&size=100`, token);
        console.log(`policy=${policy} status=${detail.status}`, files.items?.map((file) => ({ name: file.name, state: file.state })));
}
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Transmission Automation

Configure it so that transmissions are executed automatically based on schedules, file events, and workflows, without requiring a human to call it every time.

Repetitive Automation

explanation

It repeats the same transmission according to the schedule. When an automation is created, an automationId is issued, and it is operated in paused and detailed view modes.

API Usage

PurposeMethodEndpoint
Create AutomationPOST/api/automations
Automation ListGET/api/automations
Automation DetailsGET/api/automations/{automationId}/details
Pause AutomationPOST/api/automations/{automationId}/pause

Request

POST /api/automations

json
{
  "name": "Daily Settlement Transfer",
  "transferType": "normal",
  "timezone": "Asia/Seoul",
  "details": [
{
      "senderId": "device-src-001",
      "receiverId": "device-hq-001",
      "targetPath": "/collect/logs",
      "sourceItem": [{ "hash": "device-src-001_ino_...", "isDir": false }],
      "step": 1,
      "transferOptions": { "noSchedule": false, "target-action": "numbering", "send-fileoption": {} }
}
  ],
  "schedules": [
    { "type": "day", "startDateType": "now", "hour": "02", "minute": "00", "ampm": "am", "startDate": "2026-08-25T00:00:00Z", "timezone": "Asia/Seoul" }
]
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "automationId": "auto-abc123" }
}

Processing Order

  1. Call POST /api/automationsname, schedules (array of schedule objects), details (array of transfer definitions), timezone, transferType
  2. Receive response data.automationId
  3. Check details and progress with GET /api/automations/{automationId}/details
  4. Pause if necessary using POST /api/automations/{automationId}/pause

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
const TIMEZONE = process.env.INNORIX_TIMEZONE || "UTC";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function encodePath(deviceId, path) {
// Normalize the path separator and encode it in Base64 based on UTF-8.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Schedule to send the previous day's settlement file to the head office server at 2 AM every day.
    const schedule = { type: "day", startDateType: "now", hour: "02", minute: "00", ampm: "am", startDate: new Date().toISOString(), timezone: TIMEZONE };

// 1) Register Schedule
    const created = await api("POST", "/api/automations", token, {
        name: "Daily Settlement Transfer",
        details: [{ sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }], targetPath: TARGET_PATH, senderId: SOURCE_ID, receiverId: TARGET_ID, step: 1, transferOptions: { noSchedule: false, "target-action": "numbering", "send-fileoption": {} } }],
        transferType: "normal",
        timezone: TIMEZONE,
        schedules: [schedule],
});
const automationId = created.automationId;
    console.log("automation created", automationId);

// 2) View Schedule
    console.log("detail", await api("GET", `/api/automations/${automationId}`, token));

// 3) Schedule Modification
    await api("PATCH", `/api/automations/${automationId}`, token, { name: "Daily Settlement Transfer", isUpdateSchedule: true, schedules: [schedule] });

// 4) View past execution history
    console.log("executions", await api("GET", `/api/automations/${automationId}/executions`, token));

// 5) Pause
    await api("POST", `/api/automations/${automationId}/pause`, token, { pause: true });

// 6) Delete (Example cleanup)
    await api("DELETE", `/api/automations/${automationId}`, token);
    console.log("automation deleted", automationId);
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Webhook

explanation

Notifies external systems of changes in transmission and automation status. Registers the integration in webhook mode or receives a completion callback via the automation's callbackURL.

API Usage

PurposeMethodEndpoint
Create WebhookPOST/api/webhooks
View Delivery HistoryGET/api/webhooks/{webhookId}/deliveries
Retry deliveryPOST/api/webhooks/{webhookId}/deliveries/{deliveryId}/retry

Request

POST /api/webhooks

json
{
  "url": "https://internal.example.com/innorix",
  "events": ["transfer.succeeded", "transfer.failed"],
  "active": true,
  "retryPolicy": { "maxAttempts": 5, "initialDelaySeconds": 30 }
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "webhookId": "wh-abc123" }
}

Processing Order

  1. Call POST /api/webhooksurl, events, active, retryPolicy
  2. Receive response data.webhookId
  3. Subsequently, when a transmission event occurs, a notification is sent to the registered URL (signature verified with HMAC-SHA256)
  4. Retry delivery history with GET /api/webhooks/{webhookId}/deliveries, and failed deliveries with .../retry.

Implementation Example

javascript
const { createHmac, timingSafeEqual } = require("node:crypto");

const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const WEBHOOK_URL = process.env.INNORIX_WEBHOOK_URL || "https://internal.example.com/innorix";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function verifySignature(secret, timestamp, rawBody, receivedSignature) {
// Verify (timestamp.body) with HMAC-SHA256 whether the notification is not forged.
    const expected = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
    const b = Buffer.from(receivedSignature || "");
    return a.length === b.length && timingSafeEqual(a, b);
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// 1) Register a webhook to receive transmission completion and failure events.
    const created = await api("POST", "/api/webhooks", token, {
        url: WEBHOOK_URL,
        events: ["transfer.succeeded", "transfer.failed"],
        active: true,
retryPolicy: { maxAttempts: 5, initialDelaySeconds: 30 }, // Re-notify if our server is down
});
const webhookId = created.webhookId;
    console.log("webhook created", webhookId);

// 2) Verify the signature of the notification received from the receiving side. (Inject the actual value into an environment variable)
if (process.env.INNORIX_WEBHOOK_SECRET) {
        const ok = verifySignature(process.env.INNORIX_WEBHOOK_SECRET, process.env.INNORIX_WEBHOOK_TIMESTAMP, process.env.INNORIX_WEBHOOK_PAYLOAD, process.env.INNORIX_WEBHOOK_SIGNATURE);
        if (!ok) throw new Error("Invalid webhook signature");
        console.log("Webhook signature verified");
}

// 3) Check the notification transmission history.
    const deliveries = await api("GET", `/api/webhooks/${webhookId}/deliveries?limit=20`, token);
    console.log("deliveries", deliveries);

// 4) Resend the failed notification.
    const failed = deliveries.items?.find((item) => item.status !== "delivered");
    if (failed) await api("POST", `/api/webhooks/${webhookId}/deliveries/${failed.deliveryId}/retry`, token);
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Workflow

explanation

Connect multiple steps to form a single automation flow. Create the automation in a flow format (isFlowDesign), and connect each step with an automation component.

API Usage

PurposeMethodEndpoint
Create Automation (Flow)POST/api/automations
Flow DetailsGET/api/automations/{automationId}/details
View execution historyGET/api/automations/{automationId}/executions

Request

POST /api/automations

json
{
  "name": "A to B to C Workflow",
  "flowName": "A-B-C Workflow",
  "transferType": "normal",
  "timezone": "Asia/Seoul",
  "details": [
    { "senderId": "device-a", "receiverId": "device-b", "targetPath": "/data/incoming", "sourceItem": [{ "hash": "device-a_ino_...", "isDir": false }], "step": 1, "transferOptions": { "target-action": "numbering" } },
    { "senderId": "device-b", "receiverId": "device-c", "targetPath": "/data/incoming", "sourceItem": [{ "hash": "device-b_ino_...", "isDir": false }], "step": 2, "transferOptions": { "target-action": "numbering" } }
]
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "automationId": "auto-flow-abc123" }
}

Processing Order

  1. Create flow automation with POST /api/automationsflowName, details(array of steps, each step)
  2. Receive response data.automationId
  3. Verify configuration with GET /api/automations/{automationId}/details
  4. When running automation, process in the order of step in details; the history is in .../executions.

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01"; // A
const MIDDLE_ID = process.env.INNORIX_MIDDLE_ID || "device-middle-01"; // B
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01"; // C
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/report.pdf";
const MIDDLE_PATH = process.env.INNORIX_MIDDLE_PATH || SOURCE_PATH;
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
const TIMEZONE = process.env.INNORIX_TIMEZONE || "UTC";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function encodePath(deviceId, path) {
// Normalize the path separator and encode it in Base64 based on UTF-8.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Define the A→B→C chain transfer on the server. B→C follows the step order
// After A→B is completed, the server automatically continues execution. (Not client polling assembly)
    const created = await api("POST", "/api/automations", token, {
        name: "A to B to C Workflow",
        flowName: "A-B-C Workflow",
details: [
            { sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: false }], targetPath: TARGET_PATH, senderId: SOURCE_ID, receiverId: MIDDLE_ID, step: 1, transferOptions: { noSchedule: false, "target-action": "numbering", "send-fileoption": {} } },
            { sourceItem: [{ hash: encodePath(MIDDLE_ID, MIDDLE_PATH), isDir: false }], targetPath: TARGET_PATH, senderId: MIDDLE_ID, receiverId: TARGET_ID, step: 2, transferOptions: { noSchedule: false, "target-action": "numbering", "send-fileoption": {} } },
        ],
        transferType: "normal",
        timezone: TIMEZONE,
        schedules: [{ type: "none", startDateType: "now", hour: "00", minute: "00", ampm: "am", startDate: new Date().toISOString(), timezone: TIMEZONE }],
});
const automationId = created.automationId;
    console.log("workflow created", automationId);

// View step definitions and overall execution status.
    console.log("steps", await api("GET", `/api/automations/${automationId}/details`, token));
    console.log("executions", await api("GET", `/api/automations/${automationId}/executions`, token));
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

File Synchronization

Continuously synchronizes the file status of two locations. Configure real-time monitoring with a hot folder and periodic synchronization with automation.

Synchronization Overview

explanation

Synchronization is configured by creating a synchronization job using POST /api/sync/jobs. A policy is created by combining the direction (unidirectional or bidirectional) with mode, the trigger (real-time or periodic) with realTime, and the scope (entire or incremental) with incremental series options.

API Usage

PurposeMethodEndpoint
Create Synchronization JobPOST/api/sync/jobs
Check job statusGET/api/sync/jobs/{jobId}
Pause·ResumePOST/api/sync/jobs/{jobId}/pause · resume
Delete JobDELETE/api/sync/jobs/{jobId}

Processing Order

  1. Determine synchronization direction (mode), trigger (realTime), and range
  2. Create a job with POST /api/sync/jobsjobId
  3. Check status with GET /api/sync/jobs/{jobId}, and operate with pause and resume.
  4. When completed or unnecessary, use DELETE /api/sync/jobs/{jobId}

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/master";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/mirror";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Lifecycle of synchronization operations: Create → Lookup → Pause → Resume → Delete.
// (Unlike immediate transmission, the operation persists and has state.)
    const created = await api("POST", "/api/sync/jobs", token, {
        source: { deviceId: SOURCE_ID, path: SOURCE_PATH },
        target: { deviceId: TARGET_ID, path: TARGET_PATH },
        mode: "one-way",
});
const jobId = created.jobId;
    console.log("sync job created", jobId);
    console.log("detail", await api("GET", `/api/sync/jobs/${jobId}`, token));
    await api("POST", `/api/sync/jobs/${jobId}/pause`, token);
    console.log("paused", jobId);
    await api("POST", `/api/sync/jobs/${jobId}/resume`, token);
    console.log("resumed", jobId);
    await api("DELETE", `/api/sync/jobs/${jobId}`, token);
    console.log("deleted", jobId);
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Unidirectional

explanation

Changes in the source are reflected only in the target (Source → Target). Changes in the target are not rolled back to the source.

API Usage

PurposeMethodEndpoint
Create Synchronization JobPOST/api/sync/jobs

Request

POST /api/sync/jobs

json
{
  "source": { "deviceId": "device-src-001", "path": "/sync/source" },
  "target": { "deviceId": "device-dst-002", "path": "/sync/target" },
  "mode": "one-way",
  "targetChangePolicy": "restore-source"
}

Response

json
{
  "status_code": 201,
  "message": "Created",
  "data": { "jobId": "job-oneway-01" }
}

Processing Order

  1. Register a job in POST /api/sync/jobs with mode: one-way
  2. Automatically reflects source changes to the target (target changes are handled by targetChangePolicy)
  3. Check status with GET /api/sync/jobs/{jobId}

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/master";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/mirror";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Mirrors the headquarters master folder to the branch PCs exactly as is (unidirectional).
// Restore files arbitrarily modified by the branch to the headquarters' standard (restore-source).
    const created = await api("POST", "/api/sync/jobs", token, {
        source: { deviceId: SOURCE_ID, path: SOURCE_PATH },
        target: { deviceId: TARGET_ID, path: TARGET_PATH },
        mode: "one-way",
        targetChangePolicy: "restore-source",
});
const jobId = created.jobId;
    console.log("sync job created", jobId);
    console.log("detail", await api("GET", `/api/sync/jobs/${jobId}`, token));
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Bidirectional

explanation

Changes in both locations are reflected to each other (Source ↔ Target). Both sides are monitored, and any changes from either side are transmitted to the other side.

API Usage

PurposeMethodEndpoint
Create Synchronization JobPOST/api/sync/jobs

Processing Order

  1. Register a job in POST /api/sync/jobs with mode: two-way
  2. The change detection criterion is specified as changeDetection (e.g., sha256).
  3. Reflect changes on either side to the other party
  4. Simultaneous changes on both sides are handled by conflictPolicy (e.g., newest-wins).

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/shared-a";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/shared-b";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Update the shared folders of both workspaces to the latest state (bidirectional).
// Changes are determined by hash (sha256), and conflicts are handled with the newest file first.
    const created = await api("POST", "/api/sync/jobs", token, {
        source: { deviceId: SOURCE_ID, path: SOURCE_PATH },
        target: { deviceId: TARGET_ID, path: TARGET_PATH },
        mode: "two-way",
        changeDetection: "sha256",
        conflictPolicy: "newest-wins",
});
const jobId = created.jobId;
    console.log("sync job created", jobId);
    console.log("detail", await api("GET", `/api/sync/jobs/${jobId}`, token));
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Real-time

explanation

It immediately detects file changes and synchronizes without delay. File events, not schedules, are the trigger.

API Usage

PurposeMethodEndpoint
Create Synchronization Job (Real-time)POST/api/sync/jobs
Event DeliveryPOST/api/sync/jobs/{jobId}/events

Processing Order

  1. Register a job in POST /api/sync/jobs with realTime: true
  2. Immediate reflection of file creation, modification, deletion, and renaming (propagateDeletes·propagateRenames)
  3. Agent events are delivered via POST /api/sync/jobs/{jobId}/events.

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/shared";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/shared";

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Real-time synchronization reflecting folder changes on the other side within seconds.
// Propagates not only creation and modification events but also deletion and renaming events as is.
    const created = await api("POST", "/api/sync/jobs", token, {
        source: { deviceId: SOURCE_ID, path: SOURCE_PATH },
        target: { deviceId: TARGET_ID, path: TARGET_PATH },
        mode: "two-way",
        realTime: true,
        propagateDeletes: true,
        propagateRenames: true,
        targetDelaySeconds: 3,
});
const jobId = created.jobId;
    console.log("sync job created", jobId);

// Example of a renamed event payload delivered by the agent.
// (Deletion is eventType:"deleted" + path, creation/modification is "created"/"modified")
    await api("POST", `/api/sync/jobs/${jobId}/events`, token, {
        eventId: "event-20260825-001",
        eventType: "renamed",
        deviceId: SOURCE_ID,
        occurredAt: new Date().toISOString(),
        path: "/shared/old-name.txt",
        newPath: "/shared/new-name.txt",
        isDirectory: false,
});
    console.log("rename event sent");
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Incremental

explanation

Instead of sending the entire dataset every time, only files that have changed since the last synchronization are sent. Configure repetitive automation based on change criteria.

API Usage

PurposeMethodEndpoint
Create Incremental TransferPOST/api/transfers/manual (incremental: true)
Periodic SynchronizationPOST/api/automations
Check statusGET/api/transfers/{monitorId}

Processing Order

  1. Send only changes to POST /api/transfers/manual with incremental: true (for periodicization, use POST /api/automations)
  2. Selectively send only files changed since the last synchronization
  3. Check the reflection results with GET /api/transfers/{monitorId}.

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const SOURCE_ID = process.env.INNORIX_SOURCE_ID || "device-source-01";
const TARGET_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const SOURCE_PATH = process.env.INNORIX_SOURCE_PATH || "/data/bigfolder";
const TARGET_PATH = process.env.INNORIX_TARGET_PATH || "/data/incoming";
const TRANSFER_STATUS = Object.freeze({ transferComplete: 2, transferError: 4, transferCancel: 5, transferPartialComplete: 9, transferFail: 99 });
const TERMINAL_STATUSES = new Set(Object.values(TRANSFER_STATUS));

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function encodePath(deviceId, path) {
// Normalize the path separator and encode it in Base64 based on UTF-8.
    const normalized = path.replaceAll("\\", "/");
    return `${deviceId}_ino_${Buffer.from(normalized, "utf8").toString("base64")}`;
}

async function waitForCompletion(monitorId, token) {
for (;;) {
        const detail = await api("GET", `/api/transfers/${monitorId}`, token);
        console.log({ monitorId, status: detail.status, percent: detail.percent || 0 });
if (detail.isTerminal ?? TERMINAL_STATUSES.has(detail.status)) {
            if (detail.status !== TRANSFER_STATUS.transferComplete) throw new Error(detail.errorCode || "Transfer failed");
return detail;
}
        await new Promise((resolve) => setTimeout(resolve, 2000));
}
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Sync a large folder daily, but send only changed files.
    const transfer = await api("POST", "/api/transfers/manual", token, {
        sourceId: SOURCE_ID,
        targetId: TARGET_ID,
        targetPath: TARGET_PATH,
        sourceItem: [{ hash: encodePath(SOURCE_ID, SOURCE_PATH), isDir: true }],
        sendAllFolder: true,
incremental: true, // Send only changes
});
    console.log("transfer created", { monitorId: transfer.monitorId, status: transfer.status });

    await waitForCompletion(transfer.monitorId, token);
    console.log("Transfer completed");
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Extended Usage

This is a use case that combines immediate transmission, automation, and synchronization to apply them to actual development scenarios.

Files outside of Git

explanation

Transfers and synchronizes files that are difficult to manage with Git, such as large binaries, media, and datasets, between devices.

API Usage

PurposeMethodEndpoint
Create TransferPOST/api/transfers/manual
Periodic SynchronizationPOST/api/automations
Check statusGET/api/transfers/{monitorId}/files

Processing Order

  1. Designate the folder/file to be managed as sourceItem.
  2. For one-time use, use POST /api/transfers/manual, and for continuous management, use POST /api/automations.
  3. Verify that the changes have been reflected using GET /api/transfers/{monitorId}/files.

Build Results

explanation

Delivers build artifacts generated by the CI/CD pipeline to the deployment target device. The pipeline triggers the delivery, and completion is notified via a webhook.

API Usage

PurposeMethodEndpoint
Create TransferPOST/api/transfers/manual
Automated DeploymentPOST/api/automations
Completion NotificationPOST/api/webhooks

Processing Order

  1. Call POST /api/transfers/manual in the pipeline after the build is complete (for multiple targets, use the file-distribution pattern)
  2. Receive response data.monitorId
  3. Receive deployment completion notification via the webhook registered with POST /api/webhooks

AI·Data

explanation

Moves large volumes of data, such as training datasets and inference results, to collection servers or processing nodes. Connects subsequent pipelines using the completion of the move as a trigger.

API Usage

PurposeMethodEndpoint
Data CollectionPOST/api/transfers/manual
Periodic CollectionPOST/api/automations
Follow-up TriggerPOST/api/webhooks

Processing Order

  1. Specify the data location as sourceItem and use POST /api/transfers/manual (for collection, use the file collection pattern)
  2. Check the status with data.monitorId after completion.
  3. Trigger follow-up processing (learning/inference) with POST /api/webhooks webhook

Results·Operations

After a transmission is created, it handles status checking, error response, interruption recovery, monitoring, and record management.

Status·Result

explanation

View the status of files in progress and the results of completed transfers.

API Usage

PurposeMethodEndpoint
Transfer Status/ProgressGET/api/transfers/{monitorId}
View files in progressGET/api/transfers/{monitorId}/files
Completion History DetailsGET/api/transfer-history/{monitorId}
Completion history fileGET/api/transfers/{monitorId}/files?state=history

Response

json
{
  "status_code": 200,
  "message": "success",
  "data": {
    "monitorId": "mon-abc123",
    "status": 2, "statusName": "COMPLETED", "statusLabel": "Completed",
    "files": [
{
        "path": "/data/report.pdf",
        "fileToken": "L2RhdGEv...",
"status": 2, "statusName": "DONE", "statusLabel": "Completed",
        "size": 20480
}
]
}
}

Processing Order

  1. Check transfer status and progress with GET /api/transfers/{monitorId}status 2=Completed, 4·5·9·99=Terminated due to failure. For progress by file, use GET /api/transfers/{monitorId}/files
  2. After completion, check the result summary with GET /api/transfer-history/{monitorId}.
  3. For file-level details, use GET /api/transfers/{monitorId}/files?state=history

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const DEVICE_ID = process.env.INNORIX_TARGET_ID || "device-target-01";
const PAGE_SIZE = 50;
const TRANSFER_STATUS = Object.freeze({ transferError: 4, transferFail: 99 });

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Retrieves only the cases where transmission to a specific device failed (error/fail) over the past 7 days.
const startDate = new Date(Date.now() - 7 * 86400000).toISOString();
    const statusFilter = [TRANSFER_STATUS.transferError, TRANSFER_STATUS.transferFail].join(",");

// Iterate through the pagination and collect the entire list of failures.
const failures = [];
for (let offset = 0; ; offset += PAGE_SIZE) {
        const page = await api("GET", `/api/transfer-history?deviceId=${DEVICE_ID}&statusFilter=${statusFilter}&startDate=${startDate}&offset=${offset}&limit=${PAGE_SIZE}`, token);
failures.push(...page.items);
if (page.items.length < PAGE_SIZE) break;
}

console.log(`Failed transfers in last 7 days: ${failures.length}`);
for (const item of failures) {
        console.log({ monitorId: item.monitorId, status: item.status, targetPath: item.targetPath, finishedAt: item.finishedAt });
}
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Error/Retry

explanation

Select and resend only the files that failed during transmission, or cancel multiple transmissions at once.

API Usage

PurposeMethodEndpoint
Retry failurePOST/api/transfers/{monitorId}/retry
Multiple CancelPOST/api/transfers/bulk/cancel
Cancel Single TransactionPOST/api/transfers/{monitorId}/cancel

Request

POST /api/transfers/{monitorId}/retry

json
{
  "filesRetry": ["/data/report.pdf", "/data/image.png"]
}

Response

json
{
  "status_code": 200,
  "message": "success",
  "data": { "monitorId": "mon-abc123", "retried": 2 }
}

Processing Order

  1. Check failed files using GET /api/transfers/{monitorId}/files
  2. Resend filesRetry to POST /api/transfers/{monitorId}/retry
  3. To cancel multiple transfers, use POST /api/transfers/bulk/cancel(monitorIds)

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
// Classification of failure causes: Differentiate network, authority, and capacity based on the first digit of the code.
const ERROR_CATEGORY = {
NETWORK: "Network",
PERMISSION: "Authority",
CAPACITY: "Capacity",
};

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

function categorize(errorCode) {
// Maps error codes to human-readable causes.
return ERROR_CATEGORY[errorCode?.split("_")[0]] || "Others";
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// 1) Find the most recent failed transmission.
    const history = await api("GET", "/api/transfer-history?statusFilter=4,99&limit=1", token);
if (!history.items.length) {
        console.log("No failed transfers found");
return;
}
const monitorId = history.items[0].monitorId;

// 2) Check the failure cause code in the transmission details.
    const detail = await api("GET", `/api/transfers/${monitorId}`, token);
    console.log({ monitorId, status: detail.status, errorCode: detail.errorCode, cause: categorize(detail.errorCode) });

// 3) View the list of failed files and the reason for each failure.
    const files = await api("GET", `/api/transfers/${monitorId}/files?state=any&size=100`, token);
for (const file of files.items) {
        console.log({ name: file.name, state: file.state, errorCode: file.errorCode, cause: categorize(file.errorCode) });
}
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Interruption Recovery

explanation

Resume paused or interrupted transmissions and rerun past transmissions with the same settings.

API Usage

PurposeMethodEndpoint
ResumptionPOST/api/transfers/{monitorId}/resume
ReplayPOST/api/transfers/{monitorId}/replay
Retrieve Replay SettingsGET/api/transfers/{monitorId}/replay-data

Response

json
{
  "status_code": 200,
  "message": "success",
  "data": { "monitorId": "mon-abc123", "status": "in_progress" }
}

Processing Order

  1. Paused transfers are resumed from the point of interruption using POST /api/transfers/{monitorId}/resume.
  2. To replay a past transfer, check the settings with GET /api/transfers/{monitorId}/replay-data.
  3. Re-execute the same settings with POST /api/transfers/{monitorId}/replay

Integrity Verification

explanation

Verify that the transmitted file is identical to the source. Verify integrity by comparing the checksums (sha256) of the source and the copy, and receive a list of mismatched files.

API Usage

PurposeMethodEndpoint
Start verificationPOST/api/transfers/{monitorId}/verify
Check verification statusGET/api/transfers/{monitorId}/verify/{verificationId}

Response

json
{
  "status_code": 200,
  "message": "success",
  "data": {
    "verificationId": "vrf-abc123",
    "state": "completed",
    "checkedCount": 128,
    "totalCount": 128,
    "mismatches": []
}
}

Processing Order

  1. Call POST /api/transfers/{monitorId}/verify(algorithm: sha256) with the monitorId of the completed transfer → verificationId
  2. Retrieve GET /api/transfers/{monitorId}/verify/{verificationId} until the state becomes completed.
  3. If there are mismatched files in mismatches, correct them by resending.

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
// Since the verification target is a transmission that has already been completed, its monitorId is received as input.
const MONITOR_ID = process.env.INNORIX_MONITOR_ID;

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

async function main() {
    if (!MONITOR_ID) throw new Error("INNORIX_MONITOR_ID is required");
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// 1) Request a checksum comparison (integrity verification) between the original and the copy. Since this is an asynchronous operation, an ID is received.
    const started = await api("POST", `/api/transfers/${MONITOR_ID}/verify`, token, { algorithm: "sha256" });
const verificationId = started.verificationId;

// 2) Check the status until verification is complete.
let result;
for (;;) {
        result = await api("GET", `/api/transfers/${MONITOR_ID}/verify/${verificationId}`, token);
        console.log({ verificationId, state: result.state, checked: result.checkedCount, total: result.totalCount });
        if (result.state === "completed") break;
        await new Promise((resolve) => setTimeout(resolve, 2000));
}

// 3) If there are mismatched files, output them as a list.
const mismatches = result.mismatches || [];
if (!mismatches.length) {
        console.log("All files verified: source and copy match");
return;
}
console.log(`Mismatched files: ${mismatches.length}`);
for (const file of mismatches) {
        console.log({ path: file.path, sourceChecksum: file.sourceChecksum, targetChecksum: file.targetChecksum });
}
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Monitoring

explanation

Continuously monitors transmission, automation, and device status.

API Usage

PurposeMethodEndpoint
Transfer in progressGET/api/transfers/{monitorId}/files
Automation DetailsGET/api/automations/{automationId}/details
Device connection statusGET/api/devices/{deviceId}/connectivity

Processing Order

  1. Periodic polling for ongoing transfers using GET /api/transfers/{monitorId}/files
  2. Check the progress of automation using GET /api/automations/{automationId}/details.
  3. Check if the device is online using GET /api/devices/{deviceId}/connectivity.

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
const POLL_COUNT = 10; // Number of polling attempts
const POLL_INTERVAL_MS = 2000; // Polling interval (increase to reduce load)

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

Periodically checks and displays the progress, speed, and estimated completion time of ongoing transmissions.
for (let tick = 1; tick <= POLL_COUNT; tick++) {
        const active = await api("GET", "/api/transfers?limit=50", token);
console.log(`--- poll ${tick}/${POLL_COUNT}: ${active.items.length} active transfers ---`);
for (const transfer of active.items) {
console.log({
                monitorId: transfer.monitorId,
                percent: transfer.percent || 0,
                speed: transfer.speed, // bytes/sec
eta: transfer.eta, // Estimated completion time
});
}
        if (tick < POLL_COUNT) await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});

Audit Log

explanation

View and export transmission history to save it as an operational record.

API Usage

PurposeMethodEndpoint
History DetailsGET/api/transfer-history/{monitorId}
Export History (CSV)GET/api/transfer-history/export

Processing Order

  1. View individual transfer records using GET /api/transfer-history/{monitorId}.
  2. Filter by period, status, and keyword and export CSV using GET /api/transfer-history/export
    • Query: periodDays, status, searchKeyword, page, size, sort

Implementation Example

javascript
const BASE_URL = process.env.INNORIX_BASE_URL || "https://app.innorix.com";
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID;
// Equipment to be audited (e.g., device-branch-01)
const DEVICE_ID = process.env.INNORIX_SOURCE_ID || "device-branch-01";
const PAGE_SIZE = 50;

async function api(method, path, token, body) {
    const response = await fetch(BASE_URL + path, {
        method,
headers: {
            "Content-Type": "application/json",
            ...(token && { Authorization: `Bearer ${token}`, "x-workspace-id": WORKSPACE_ID }),
        },
        ...(body && { body: JSON.stringify(body) }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
return payload.data;
}

async function main() {
    const login = await api("POST", "/api/auth/login", null, { email: process.env.INNORIX_EMAIL, password: process.env.INNORIX_PASSWORD });
const token = login.user.accessToken;

// Paginates and retrieves all transmission history sent by a specific device over the past 30 days.
const startDate = new Date(Date.now() - 30 * 86400000).toISOString();
const records = [];
for (let offset = 0; ; offset += PAGE_SIZE) {
        const page = await api("GET", `/api/devices/${DEVICE_ID}/transfer-history?startDate=${startDate}&offset=${offset}&limit=${PAGE_SIZE}`, token);
records.push(...page.items);
if (page.items.length < PAGE_SIZE) break;
}

// Check when and to which device a file was sent.
console.log(`Transfers by ${DEVICE_ID} in last 30 days: ${records.length}`);
for (const record of records) {
console.log({
            startedAt: record.startedAt,
            source: record.sourcePath,
            targetDevice: record.targetDevice,
            targetPath: record.targetPath,
            fileCount: record.fileCount,
            status: record.status,
});
}
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});