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.
| Header | Description |
|---|---|
Authorization: Bearer {accessToken} | Access token (JWT) issued upon login |
x-workspace-id: {workspaceId} | Target workspace ID |
Content-Type: application/json | Request 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.
| Field | Type | Description |
|---|---|---|
hash | string | Item identifier — {deviceId}_ino_{base64(UTF-8 path)} |
isDir | boolean | Folder 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).
{
"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.
{
"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
| Purpose | Method | Endpoint |
|---|---|---|
| Login | POST | /api/auth/login |
| Renew Token | POST | /api/auth/token/refresh |
| Device List | GET | /api/devices |
| Device Lookup (Name·IP·MAC) | GET | /api/devices/resolve |
| Path Capacity Preview | GET | /api/devices/{deviceId}/path-stats |
| Source file search | POST | /api/devices/{deviceId}/files/search |
| Folder Lookup (Non-streaming) | GET | /api/devices/{deviceId}/files |
| Path Prevalidation | POST | /api/transfers/validate-path |
| Immediate transfer creation | POST | /api/transfer/manual |
| View transferred files | GET | /api/transfers/{monitorId}/files |
| Transfer Control | POST | /api/transfers/{monitorId}/pause · resume · cancel · retry |
Basic Flow
- Log in to obtain an access token
- Check
sourceIdandtargetIdusingGET /api/devices - (Optional) Configure
sourceItemwith file search - (Optional) Validate path with
validate-path - Create and send via
POST /api/transfers/manual→ ReceivemonitorId - 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
| Purpose | Method | Endpoint |
|---|---|---|
| Login | POST | /api/auth/login |
| Create Transfer | POST | /api/transfers/manual |
| Check status | GET | /api/transfers/{monitorId} |
Request
POST /api/transfers/manual
{
"sourceId": "device-source-01",
"targetId": "device-target-01",
"targetPath": "/data/incoming",
"sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }],
"sendAllFolder": false
}
sourceIdandtargetIdare thedeviceIdobtained fromGET /api/devices.- The
hashofsourceItemis in the format{deviceId}_ino_{base64(UTF-8 path)}. sendAllFolderis 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
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
Processing Order
- Obtain an access token with
POST /api/auth/login(data.user.accessToken) - Call
POST /api/transfers/manual—sourceId,targetId,targetPath,sourceItem→ Receivedata.monitorId - Polle
GET /api/transfers/{monitorId}and checkstatus—2=Completed,4·5·9·99=Ended with failure
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Login | POST | /api/auth/login |
| Device Lookup (Name) | GET | /api/devices/resolve |
| Folder Lookup (Non-streaming) | GET | /api/devices/{deviceId}/files |
| Create Transfer | POST | /api/transfers/manual |
| Check status | GET | /api/transfers/{monitorId} |
Request
POST /api/transfers/manual (repeat for each source)
{
"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
sourceIdfor each location, and keep thetargetIdandtargetPathfixed. target-action: numberingintransferOptionsadds numbers in case of name conflicts when gathering in the same path.- Since it is a collection of the entire folder,
sendAllFolderistrue.
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
Processing Order
- Obtain an access token using
POST /api/auth/login - Repeat at each point:
- Obtain the branch
sourceIdusingGET /api/devices/resolve?name=Search for the target folder usingGET /api/devices/{deviceId}/files→ Obtain the path - Call
POST /api/transfers/manual(targetId=target,sourceItem=folder item) → CollectmonitorId
- Polle all
monitorIds viaGET /api/transfers/{monitorId}and tally completion.
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Login | POST | /api/auth/login |
| Device Lookup (Name) | GET | /api/devices/resolve |
| Folder Lookup (Non-streaming) | GET | /api/devices/{deviceId}/files |
| Create Transfer | POST | /api/transfers/manual |
| Check status | GET | /api/transfers/{monitorId} |
Request
POST /api/transfers/manual (repeat for each target)
{
"sourceId": "dev_01H8SRC...",
"targetId": "dev_01H8BRANCH01...",
"targetPath": "/deploy",
"sourceItem": [{ "hash": "dev_01H8SRC..._ino_L2RlcGxveS9kZXBsb3ltZW50LXBhY2thZ2U=", "isDir": true }],
"sendAllFolder": true,
"transferOptions": { "target-action": "overwrite" }
}
- Change only
targetIdfor each location, and keepsourceIdandsourceItemfixed. - Overwrites the target's existing files with
transferOptions'starget-action: overwrite. - Since it is a distribution of the entire folder,
sendAllFolderistrue.
Response
{
"status_code": 201,
"message": "Created",
"data": { "monitorId": "mon-abc123", "transferId": "tr-abc123" }
}
Processing Order
- Obtain an access token using
POST /api/auth/login - Obtain the source
deviceIdusingGET /api/devices/resolve?name= - Search for the deployment package folder using
GET /api/devices/{deviceId}/filesto obtain the path. - Call
POST /api/transfers/manualfor each target (targetId=Branch,sourceItem=Package Item) →monitorIdas many times as there are targets - Polle each
monitorIdviaGET /api/transfers/{monitorId}to tally successes and failures
Implementation Example
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.
| Category | Related Fields | Details |
|---|---|---|
| What(source) | sourceItem, sendAllFolder, filter | source filter |
| Where to (target) | targetId, targetPath | target option |
| Batch(Path) | pathMapping, sendAllFolder | Path Mapping |
| When overlapping | transferOptions.target-action | Collision handling |
API Usage
| Purpose | Method | Endpoint |
|---|---|---|
| Path Validation | POST | /api/transfers/validate-path |
| Create Transfer | POST | /api/transfers/manual |
Request
POST /api/transfers/validate-path
{
"sourceId": "device-src-001",
"targetId": "device-dst-002",
"targetPath": "/data/incoming",
"sourceItem": [{ "hash": "device-src-001_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }]
}
Response
{
"status_code": 200,
"message": "success",
"data": { "valid": true }
}
Processing Order
- Verify source, target, and path validity using
POST /api/transfers/validate-path - Configure the rule (
sourceItem/filter/transferOptions.pathMapping/transferOptions.target-action) and callPOST /api/transfers/manual - Follow-up processing with the response
data.monitorId
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| File Search | POST | /api/devices/{deviceId}/files/search |
| Create filter transfer | POST | /api/transfers/filtered |
filter option field:
| Field | Type | Description |
|---|---|---|
filter.includeExtensions | string[] | List of extensions to include (e.g., ["log","csv"]) |
filter.excludePatterns | string[] | glob patterns to exclude (e.g., ["*.tmp"]) |
filter.minSize | number | Minimum size (bytes) |
filter.maxSize | number | Maximum size (bytes) |
filter.modifiedAfter | string | Files modified after this time only (ISO 8601) |
filter.modifiedBefore | string | Files modified before this time only (ISO 8601) |
filter.recursive | boolean | Whether to recursively search subfolders |
Request
POST /api/transfers/filtered
{
"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
{
"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
- (Optional) Check the candidate list with
POST /api/devices/{deviceId}/files/search - Call
POST /api/transfers/filteredincludingfilter - Check progress status using response
data.monitorId
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create target folder | POST | /api/devices/{deviceId}/files/folders |
| Path Validation | POST | /api/transfers/validate-path |
| Check Capacity | GET | /api/devices/{deviceId}/capacity |
| Create Transfer | POST | /api/transfers/manual |
Request
POST /api/transfers/manual
{
"sourceId": "device-source-01",
"targetId": "device-target-01",
"targetPath": "/data/incoming/2026",
"sourceItem": [{ "hash": "device-source-01_ino_L2RhdGEvcmVwb3J0LnBkZg==", "isDir": false }]
}
Response
{
"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
- (Optional) Create target folder with
POST /api/devices/{deviceId}/files/folders - Check destination path free capacity with
GET /api/devices/{deviceId}/capacity - Call
POST /api/transfers/manual(sourceId,targetId,targetPath) - Receive response
data.monitorId
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Transfer | POST | /api/transfers/manual |
| View Batch Results | GET | /api/transfers/{monitorId}/files |
transferOptions.pathMapping option field:
| Field | Type | Description |
|---|---|---|
pathMapping.preserveStructure | boolean | Preserve source folder structure (flatten if false) |
pathMapping.removePrefix | string | Prefix path to remove from source path |
pathMapping.targetRoot | string | Target root path serving as the deployment basis |
pathMapping.fileNameTemplate | string | Filename template (e.g., {name}, date prefix) |
pathMapping.createTargetFolders | boolean | Automatically create non-existent target folders |
Request
POST /api/transfers/manual
{
"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
{
"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
- Call
POST /api/transfers/manualincludingpathMapping - Receive response
data.monitorId - Check the actual batch results with
GET /api/transfers/{monitorId}/files
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Transfer | POST | /api/transfers/manual |
| Retry failure | POST | /api/transfers/{monitorId}/retry |
transferOptions.target-action value:
| Value | Description |
|---|---|
| numbering | In case of name conflicts, number both to preserve ((1), (2)) |
skip | Skip if already present |
overwrite | Overwrite existing file |
fail | Handle file failure on crash |
Request
POST /api/transfers/manual
{
"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
{
"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
- Call
POST /api/transfers/manualincludingtransferOptions.target-action - Receive response
data.monitorId - Resend failed files via
POST /api/transfers/{monitorId}/retry
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Automation | POST | /api/automations |
| Automation List | GET | /api/automations |
| Automation Details | GET | /api/automations/{automationId}/details |
| Pause Automation | POST | /api/automations/{automationId}/pause |
Request
POST /api/automations
{
"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
{
"status_code": 201,
"message": "Created",
"data": { "automationId": "auto-abc123" }
}
Processing Order
- Call
POST /api/automations—name,schedules(array of schedule objects),details(array of transfer definitions),timezone,transferType - Receive response
data.automationId - Check details and progress with
GET /api/automations/{automationId}/details - Pause if necessary using
POST /api/automations/{automationId}/pause
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Webhook | POST | /api/webhooks |
| View Delivery History | GET | /api/webhooks/{webhookId}/deliveries |
| Retry delivery | POST | /api/webhooks/{webhookId}/deliveries/{deliveryId}/retry |
Request
POST /api/webhooks
{
"url": "https://internal.example.com/innorix",
"events": ["transfer.succeeded", "transfer.failed"],
"active": true,
"retryPolicy": { "maxAttempts": 5, "initialDelaySeconds": 30 }
}
Response
{
"status_code": 201,
"message": "Created",
"data": { "webhookId": "wh-abc123" }
}
Processing Order
- Call
POST /api/webhooks—url,events,active,retryPolicy - Receive response
data.webhookId - Subsequently, when a transmission event occurs, a notification is sent to the registered URL (signature verified with HMAC-SHA256)
- Retry delivery history with
GET /api/webhooks/{webhookId}/deliveries, and failed deliveries with.../retry.
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Automation (Flow) | POST | /api/automations |
| Flow Details | GET | /api/automations/{automationId}/details |
| View execution history | GET | /api/automations/{automationId}/executions |
Request
POST /api/automations
{
"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
{
"status_code": 201,
"message": "Created",
"data": { "automationId": "auto-flow-abc123" }
}
Processing Order
- Create flow automation with
POST /api/automations—flowName,details(array of steps, eachstep) - Receive response
data.automationId - Verify configuration with
GET /api/automations/{automationId}/details - When running automation, process in the order of
stepindetails; the history is in.../executions.
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Synchronization Job | POST | /api/sync/jobs |
| Check job status | GET | /api/sync/jobs/{jobId} |
| Pause·Resume | POST | /api/sync/jobs/{jobId}/pause · resume |
| Delete Job | DELETE | /api/sync/jobs/{jobId} |
Processing Order
- Determine synchronization direction (
mode), trigger (realTime), and range - Create a job with
POST /api/sync/jobs→jobId - Check status with
GET /api/sync/jobs/{jobId}, and operate withpauseandresume. - When completed or unnecessary, use
DELETE /api/sync/jobs/{jobId}
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Synchronization Job | POST | /api/sync/jobs |
Request
POST /api/sync/jobs
{
"source": { "deviceId": "device-src-001", "path": "/sync/source" },
"target": { "deviceId": "device-dst-002", "path": "/sync/target" },
"mode": "one-way",
"targetChangePolicy": "restore-source"
}
Response
{
"status_code": 201,
"message": "Created",
"data": { "jobId": "job-oneway-01" }
}
Processing Order
- Register a job in
POST /api/sync/jobswithmode: one-way - Automatically reflects source changes to the target (target changes are handled by
targetChangePolicy) - Check status with
GET /api/sync/jobs/{jobId}
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Synchronization Job | POST | /api/sync/jobs |
Processing Order
- Register a job in
POST /api/sync/jobswithmode: two-way - The change detection criterion is specified as
changeDetection(e.g.,sha256). - Reflect changes on either side to the other party
- Simultaneous changes on both sides are handled by
conflictPolicy(e.g.,newest-wins).
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Synchronization Job (Real-time) | POST | /api/sync/jobs |
| Event Delivery | POST | /api/sync/jobs/{jobId}/events |
Processing Order
- Register a job in
POST /api/sync/jobswithrealTime: true - Immediate reflection of file creation, modification, deletion, and renaming (
propagateDeletes·propagateRenames) - Agent events are delivered via
POST /api/sync/jobs/{jobId}/events.
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Incremental Transfer | POST | /api/transfers/manual (incremental: true) |
| Periodic Synchronization | POST | /api/automations |
| Check status | GET | /api/transfers/{monitorId} |
Processing Order
- Send only changes to
POST /api/transfers/manualwithincremental: true(for periodicization, usePOST /api/automations) - Selectively send only files changed since the last synchronization
- Check the reflection results with
GET /api/transfers/{monitorId}.
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Transfer | POST | /api/transfers/manual |
| Periodic Synchronization | POST | /api/automations |
| Check status | GET | /api/transfers/{monitorId}/files |
Processing Order
- Designate the folder/file to be managed as
sourceItem. - For one-time use, use
POST /api/transfers/manual, and for continuous management, usePOST /api/automations. - 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
| Purpose | Method | Endpoint |
|---|---|---|
| Create Transfer | POST | /api/transfers/manual |
| Automated Deployment | POST | /api/automations |
| Completion Notification | POST | /api/webhooks |
Processing Order
- Call
POST /api/transfers/manualin the pipeline after the build is complete (for multiple targets, use the file-distribution pattern) - Receive response
data.monitorId - 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
| Purpose | Method | Endpoint |
|---|---|---|
| Data Collection | POST | /api/transfers/manual |
| Periodic Collection | POST | /api/automations |
| Follow-up Trigger | POST | /api/webhooks |
Processing Order
- Specify the data location as
sourceItemand usePOST /api/transfers/manual(for collection, use the file collection pattern) - Check the status with
data.monitorIdafter completion. - Trigger follow-up processing (learning/inference) with
POST /api/webhookswebhook
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
| Purpose | Method | Endpoint |
|---|---|---|
| Transfer Status/Progress | GET | /api/transfers/{monitorId} |
| View files in progress | GET | /api/transfers/{monitorId}/files |
| Completion History Details | GET | /api/transfer-history/{monitorId} |
| Completion history file | GET | /api/transfers/{monitorId}/files?state=history |
Response
{
"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
- Check transfer status and progress with
GET /api/transfers/{monitorId}—status2=Completed,4·5·9·99=Terminated due to failure. For progress by file, useGET /api/transfers/{monitorId}/files - After completion, check the result summary with
GET /api/transfer-history/{monitorId}. - For file-level details, use
GET /api/transfers/{monitorId}/files?state=history
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Retry failure | POST | /api/transfers/{monitorId}/retry |
| Multiple Cancel | POST | /api/transfers/bulk/cancel |
| Cancel Single Transaction | POST | /api/transfers/{monitorId}/cancel |
Request
POST /api/transfers/{monitorId}/retry
{
"filesRetry": ["/data/report.pdf", "/data/image.png"]
}
Response
{
"status_code": 200,
"message": "success",
"data": { "monitorId": "mon-abc123", "retried": 2 }
}
Processing Order
- Check failed files using
GET /api/transfers/{monitorId}/files - Resend
filesRetrytoPOST /api/transfers/{monitorId}/retry - To cancel multiple transfers, use
POST /api/transfers/bulk/cancel(monitorIds)
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Resumption | POST | /api/transfers/{monitorId}/resume |
| Replay | POST | /api/transfers/{monitorId}/replay |
| Retrieve Replay Settings | GET | /api/transfers/{monitorId}/replay-data |
Response
{
"status_code": 200,
"message": "success",
"data": { "monitorId": "mon-abc123", "status": "in_progress" }
}
Processing Order
- Paused transfers are resumed from the point of interruption using
POST /api/transfers/{monitorId}/resume. - To replay a past transfer, check the settings with
GET /api/transfers/{monitorId}/replay-data. - 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
| Purpose | Method | Endpoint |
|---|---|---|
| Start verification | POST | /api/transfers/{monitorId}/verify |
| Check verification status | GET | /api/transfers/{monitorId}/verify/{verificationId} |
Response
{
"status_code": 200,
"message": "success",
"data": {
"verificationId": "vrf-abc123",
"state": "completed",
"checkedCount": 128,
"totalCount": 128,
"mismatches": []
}
}
Processing Order
- Call
POST /api/transfers/{monitorId}/verify(algorithm: sha256) with themonitorIdof the completed transfer →verificationId - Retrieve
GET /api/transfers/{monitorId}/verify/{verificationId}until thestatebecomescompleted. - If there are mismatched files in
mismatches, correct them by resending.
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| Transfer in progress | GET | /api/transfers/{monitorId}/files |
| Automation Details | GET | /api/automations/{automationId}/details |
| Device connection status | GET | /api/devices/{deviceId}/connectivity |
Processing Order
- Periodic polling for ongoing transfers using
GET /api/transfers/{monitorId}/files - Check the progress of automation using
GET /api/automations/{automationId}/details. - Check if the device is online using
GET /api/devices/{deviceId}/connectivity.
Implementation Example
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
| Purpose | Method | Endpoint |
|---|---|---|
| History Details | GET | /api/transfer-history/{monitorId} |
| Export History (CSV) | GET | /api/transfer-history/export |
Processing Order
- View individual transfer records using
GET /api/transfer-history/{monitorId}. - Filter by period, status, and keyword and export CSV using
GET /api/transfer-history/export- Query:
periodDays,status,searchKeyword,page,size,sort
- Query:
Implementation Example
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;
});