Getting Started#
Basic Concept#
Exchange files in both directions between your work environment and partner systems
Partner workflows often involve both files sent to partners, such as purchase orders and design materials, and files received from partners, such as inspection results and settlement data.
File exchange connects your work environment with partner systems and manages outbound and inbound files according to separate paths and conditions.
Our Work Environment
│
Purchase Orders · Design Materials │
▼
┌────────────────┐
│ File Exchange │
└────────────────┘
▲
Inspection Results · Settlement Data │
│
Partner System
By configuring the files and paths for each partner, you can manage bidirectional file flows for ordering, design, settlement, inspection, and other workflows.
Exchange Flow#
Send files, receive results, and continue to the next task
Business files created internally are sent to the partner, and result files processed by the partner are received back and used in the next step.
Prepare Business Files
│
▼
Send to Partner
│
▼
Partner Processing
│
▼
Receive Result Files
│
▼
Internal Follow-up Work
Connecting outbound and inbound transfers in a single workflow lets you manage how files move between partner systems and internal operations.
Business Benefits#
Manage partner-specific file exchange through defined workflows
Even when each partner requires different files and processes, you can separate the files and exchange paths by partner and build workflows around those requirements.
| Workflow | Sent to Partner | Received from Partner | Used For |
|---|---|---|---|
| Order | Purchase order · ordering data | Order result | Order processing |
| Settlement | Settlement request data | Settlement result | Settlement review |
| Design | Design materials | Revisions · review materials | Design review |
| Inspection | Inspection request data | Inspection result | Quality review |
Business Teams#
File Exchange#
Select and exchange the files required for each partner workflow
Business teams select the files required for current ordering, settlement, design, inspection, and other work, then run partner-specific file exchange jobs.
Select Files
│
▼
Select Partner
│
▼
Transfer Files
│
▼
Receive Result FilesReceived files can be used immediately for the next steps, including review, revision, and approval.
Exchange Status#
Review file exchange status and results by partner
When working with multiple partners, you can review the processing status of files sent to and received from each partner.
| Partner | Sent Files | Received Files | Progress |
|---|---|---|---|
| Partner A | Purchase Order | Order Result | Completed |
| Partner B | Design Materials | Revision Materials | In Progress |
| Partner C | Inspection Request | Inspection Result | Completed |

This lets business teams see which partner exchanges are currently in progress and use received results in subsequent work.
IT Engineers#
Connect Partners#
Connect partner systems to the internal file exchange environment
First, connect the partner systems used for file exchange to the internal transfer environment.
When exchanging files with multiple partners, configure each partner as a separate connection target.
File Exchange
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Partner A Partner B Partner C
↕ ↕ ↕
Order Work Design Work Settlement Work
Exchange Setup#
Configure send and receive paths and execution criteria in one flow
For each partner, configure the internal path for files to send and the partner path for files to receive.
Define exchange targets by file name, path, and type, and configure jobs to start at scheduled times or when files are prepared or arrive.
Our System Partner System
/send/orders ───────── Send ────────► /receive/orders
/receive/results ◄──── Receive ─────── /send/results| Direction | Our System | Partner System | Execution Criteria |
|---|---|---|---|
| Send | /send/orders |
/receive/orders |
Schedule · file ready |
| Receive | /receive/results |
/send/results |
Schedule · file arrival |

Combining send and receive jobs lets you manage partner file exchange as one bidirectional flow.
Exchange Flow#
Connect file exchange jobs across multiple partners by workflow
File exchange with each partner can combine send and receive operations into a single workflow.
┌───────────────┐
│ Partner File Flows │
└───────┬───────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
Partner A Partner B Partner C
│ │ │
Order Exchange Design Exchange Settlement Exchange
│ │ │
└──────────┼──────────┘
▼
Internal Follow-up WorkOperational Review#
Manage send and receive results together by partner
Use Runs and detailed execution information to review each partner's file exchange jobs and processing results.
Reviewing send and receive results by partner lets you manage both active work and completed file exchanges in one place.
Partner A
├── Send → Completed
└── Receive → Completed
Partner B
├── Send → Completed
└── Receive → In Progress
If an exchange needs attention, use execution details and processing history to review connection status, file paths, and results, then rerun the required operation.
Developers#
Send files to partner devices and retrieve partner-uploaded files through folder monitoring
Integration Setup#
Prepare shared API calls and path encoding
import os
import requests
BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com").rstrip("/")
TOKEN = os.environ["INNORIX_ACCESS_TOKEN"]
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID") # optional; falls back to the current workspace
STATUS_COMPLETE = 2
TERMINAL = {2, 4, 5, 9, 99} # complete / error / cancelled / partial / failed
NOT_SUCCEEDED = {4, 5, 9, 99}
def api(method, path, body=None, params=None):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {TOKEN}",
}
if WORKSPACE_ID:
headers["x-workspace-id"] = WORKSPACE_ID
response = requests.request(
method, BASE_URL + path,
headers=headers, json=body, params=params, timeout=30,
)
payload = response.json() if response.content else {}
if not response.ok:
raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")
return payload.get("data")
def is_terminal(detail):
return detail.get("isTerminal", detail.get("status") in TERMINAL)// InnorixClient.java
public static final String BASE_URL =
env("INNORIX_BASE_URL", "https://app.innorix.com").replaceAll("/+quot;, "");
public static final String WORKSPACE_ID = env("INNORIX_WORKSPACE_ID", null);
public static final int STATUS_COMPLETE = 2;
// States the transfer no longer moves out of
public static final Set<Integer> TERMINAL = Set.of(2, 4, 5, 9, 99);
// Terminal states that are not a full success
public static final Set<Integer> NOT_SUCCEEDED = Set.of(4, 5, 9, 99);
private HttpRequest.Builder headers(HttpRequest.Builder builder) {
builder.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + session.accessToken());
// When omitted the account's current workspace is used.
if (workspaceId != null) builder.header("x-workspace-id", workspaceId);
return builder;
}
/** Unwraps and returns data from the response. Throws ApiError on failure. */
public Object api(String method, String path, Object body, Map<String, Object> params) {
Resp response = request(method, path, body, params);
Object payload = null;
try {
payload = Json.parse(response.text());
} catch (RuntimeException ignored) {
payload = null;
}
if (!response.ok()) {
Map<String, Object> map = Json.asObj(payload);
String message = Json.str(map, "message", Json.str(map, "error", "unknown error"));
throw new ApiError(response.status, message, map);
}
return Json.get(payload, "data");
}
/** Use the server flag when present, otherwise fall back to the status code. */
public static boolean isTerminal(Map<String, Object> record) {
Boolean flag = Json.boolOrNull(record, "isTerminal");
if (flag != null) return flag;
Integer status = Json.intOrNull(record, "status");
return status != null && TERMINAL.contains(status);
}// innorix-client.js
const BASE_URL = (process.env.INNORIX_BASE_URL
|| "https://app.innorix.com").replace(/\/+$/, "");
const TOKEN = process.env.INNORIX_ACCESS_TOKEN;
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID || null;
export const STATUS_COMPLETE = 2;
export const TERMINAL = new Set([2, 4, 5, 9, 99]); // complete / error / cancelled / partial / failed
export const NOT_SUCCEEDED = new Set([4, 5, 9, 99]);
export async function api(method, path, body = null, params = null) {
const url = new URL(BASE_URL + path);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null) continue;
url.searchParams.set(key, String(value));
}
}
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${TOKEN}`,
};
// When omitted the account's current workspace is used.
if (WORKSPACE_ID) headers["x-workspace-id"] = WORKSPACE_ID;
const response = await fetch(url, {
method,
headers,
body: body === null ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload.message || `HTTP ${response.status}`);
}
return payload.data;
}
export function isTerminal(detail) {
return detail.isTerminal !== undefined
? detail.isTerminal
: TERMINAL.has(detail.status);
}// InnorixClient.cs
public static readonly string BaseUrl =
Env("INNORIX_BASE_URL", "https://app.innorix.com").TrimEnd('/');
public static readonly string WorkspaceIdFromEnv = Env("INNORIX_WORKSPACE_ID", null);
public const int StatusComplete = 2;
/// <summary>States the transfer no longer moves out of</summary>
public static readonly HashSet<int> Terminal = new HashSet<int> { 2, 4, 5, 9, 99 };
/// <summary>Terminal states that are not a full success</summary>
public static readonly HashSet<int> NotSucceeded = new HashSet<int> { 4, 5, 9, 99 };
// Applied on every request
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + Session.AccessToken);
// When omitted the account's current workspace is used.
if (WorkspaceId != null) request.Headers.TryAddWithoutValidation("x-workspace-id", WorkspaceId);
public async Task<JsonNode> ApiAsync(string method, string path, JsonNode body = null,
IDictionary<string, object> parameters = null)
{
Resp response = await RequestAsync(method, path, body, parameters).ConfigureAwait(false);
JsonNode payload = null;
try
{
payload = J.Parse(response.Text());
}
catch (Exception)
{
payload = null;
}
if (!response.Ok)
{
JsonObject map = J.AsObj(payload);
string message = J.Str(map, "message", J.Str(map, "error", "unknown error"));
throw new ApiError(response.Status, message, map);
}
return J.Get(payload, "data");
}
/// <summary>Use the server flag when present, otherwise fall back to the status code.</summary>
public static bool IsTerminal(JsonObject record)
{
bool? flag = J.BoolOrNull(record, "isTerminal");
if (flag != null) return flag.Value;
int? status = J.IntOrNull(record, "status");
return status != null && Terminal.Contains(status.Value);
}import base64
import time
def encode_path(device_id, raw_path):
normalized = str(raw_path or "").replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def now_iso():
return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())public static String encodePath(String deviceId, String rawPath) {
String normalized = (rawPath == null ? "" : rawPath).replace("\\", "/");
return deviceId + "_ino_"
+ Base64.getEncoder().encodeToString(normalized.getBytes(StandardCharsets.UTF_8));
}
public static String nowIso() {
return Instant.now().truncatedTo(ChronoUnit.SECONDS).toString().replace("Z", ".000Z");
}export function encodePath(deviceId, rawPath) {
const normalized = String(rawPath ?? "").replace(/\\/g, "/");
const token = Buffer.from(normalized, "utf8").toString("base64");
return `${deviceId}_ino_${token}`;
}
export function nowIso() {
return new Date().toISOString().replace(/\.\d{3}Z$/, ".000Z");
}public static string EncodePath(string deviceId, string rawPath)
{
string normalized = (rawPath ?? "").Replace("\\", "/");
return deviceId + "_ino_" + Convert.ToBase64String(Encoding.UTF8.GetBytes(normalized));
}
public static string NowIso()
{
return DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'.000Z'",
System.Globalization.CultureInfo.InvariantCulture);
}Use the following values to determine transfer status. There are five terminal states, and Complete (2) is the successful state.
| Status Value | Meaning | Terminal |
|---|---|---|
| 2 | Complete | Yes |
| 4 | Error | Yes |
| 5 | Cancelled | Yes |
| 9 | Partial Complete | Yes |
| 99 | Failed | Yes |
| 1 · 6 · 12 · 13 | Starting · Transferring · Synchronizing · Receiving | No |
Partner Setup#
Manage partner-specific devices and paths as data
Each partner may use different paths and workflows. Keeping this configuration in data means you do not need to change the code as partners are added.
PARTNERS = {
"partner-a": {
"device": "device-partner-a",
"send": {"local": "/send/orders", "remote": "/receive/orders"},
"receive": {"remote": "/send/results", "local": "/receive/results"},
},
"partner-b": {
"device": "device-partner-b",
"send": {"local": "/send/design", "remote": "/receive/design"},
"receive": {"remote": "/send/review", "local": "/receive/review"},
},
}
INTERNAL = "device-hq-01"record Route(String local, String remote) {}
record Partner(String device, Route send, Route receive) {}
static final Map<String, Partner> PARTNERS = Map.of(
"partner-a", new Partner("device-partner-a",
new Route("/send/orders", "/receive/orders"),
new Route("/receive/results", "/send/results")),
"partner-b", new Partner("device-partner-b",
new Route("/send/design", "/receive/design"),
new Route("/receive/review", "/send/review")));
static final String INTERNAL = "device-hq-01";export const PARTNERS = {
"partner-a": {
device: "device-partner-a",
send: { local: "/send/orders", remote: "/receive/orders" },
receive: { remote: "/send/results", local: "/receive/results" },
},
"partner-b": {
device: "device-partner-b",
send: { local: "/send/design", remote: "/receive/design" },
receive: { remote: "/send/review", local: "/receive/review" },
},
};
export const INTERNAL = "device-hq-01";public record Route(string Local, string Remote);
public record Partner(string Device, Route Send, Route Receive);
static readonly Dictionary<string, Partner> Partners = new()
{
["partner-a"] = new Partner("device-partner-a",
new Route("/send/orders", "/receive/orders"),
new Route("/receive/results", "/send/results")),
["partner-b"] = new Partner("device-partner-b",
new Route("/send/design", "/receive/design"),
new Route("/receive/review", "/send/review")),
};
const string Internal = "device-hq-01";If workspaces are separated by partner, send the workspace identifier with each request. This prevents one partner's configuration mistake from exposing another partner's files.
Send Files#
Send internal files to a partner device
When sending files, specify isDir: false in sourceItem. sourcePaths treats every path as a folder.
import os
def send_to_partner(partner_id, files):
partner = PARTNERS[partner_id]
transfer = api("POST", "/api/transfers/manual", {
"sourceDevice": INTERNAL,
"targetDevice": partner["device"],
"targetPath": partner["send"]["remote"],
"sourceItem": [{"path": p, "isDir": False} for p in files],
"sendAllFolder": False,
"transferOptions": {"target-action": "numbering"},
})
return transfer["monitorId"]
monitor_id = send_to_partner("partner-a", ["/send/orders/PO-2026-0901.xlsx"])String sendToPartner(String partnerId, List<String> files) {
Partner partner = PARTNERS.get(partnerId);
List<Object> items = new ArrayList<>();
for (String path : files) items.add(Json.newObj("path", path, "isDir", false));
Map<String, Object> transfer = client.apiObj("POST", "/api/transfers/manual", Json.newObj(
"sourceDevice", INTERNAL,
"targetDevice", partner.device(),
"targetPath", partner.send().remote(),
"sourceItem", items,
"sendAllFolder", false,
"transferOptions", Json.newObj("target-action", "numbering")));
return Json.str(transfer, "monitorId");
}
String monitorId = sendToPartner("partner-a", List.of("/send/orders/PO-2026-0901.xlsx"));async function sendToPartner(partnerId, files) {
const partner = PARTNERS[partnerId];
const transfer = await client.api("POST", "/api/transfers/manual", {
sourceDevice: INTERNAL,
targetDevice: partner.device,
targetPath: partner.send.remote,
sourceItem: files.map((path) => ({ path, isDir: false })),
sendAllFolder: false,
transferOptions: { "target-action": "numbering" },
});
return transfer.monitorId;
}
const monitorId = await sendToPartner("partner-a", ["/send/orders/PO-2026-0901.xlsx"]);async Task<string> SendToPartnerAsync(string partnerId, IEnumerable<string> files)
{
Partner partner = Partners[partnerId];
var items = new JsonArray();
foreach (string path in files)
items.Add(new JsonObject { ["path"] = path, ["isDir"] = false });
JsonObject transfer = await client.ApiObjAsync("POST", "/api/transfers/manual",
new JsonObject
{
["sourceDevice"] = Internal,
["targetDevice"] = partner.Device,
["targetPath"] = partner.Send.Remote,
["sourceItem"] = items,
["sendAllFolder"] = false,
["transferOptions"] = new JsonObject { ["target-action"] = "numbering" },
});
return J.Str(transfer, "monitorId");
}
string monitorId = await SendToPartnerAsync("partner-a",
new[] { "/send/orders/PO-2026-0901.xlsx" });When sending folders, use sourcePaths with sendAllFolder: True.
Use numbering for files sent to partners so each submission is preserved. If the same purchase order is revised and resent, overwriting the previous version would make it difficult to determine which version was actually processed.
Receive Result Files#
Detect and retrieve files uploaded by partners
Because you do not know exactly when a partner will upload a file, configure real-time folder monitoring. Set transferType to sync and include watchFolderType in transferOptions so the file is transferred as soon as it arrives.
def create_receive_watch(partner_id, webhook=None):
partner = PARTNERS[partner_id]
remote = partner["receive"]["remote"]
local = f"{partner['receive']['local']}/{partner_id}"
name = f"receive {partner_id}"
body = {
"name": name,
"flowName": name,
"transferType": "sync",
"timezone": "Asia/Seoul",
"step": 1,
"isUpcoming": False,
"details": [
{
"senderId": partner["device"],
"receiverId": INTERNAL,
"sourceItem": [
{
"hash": encode_path(partner["device"], remote),
"filePath": remote,
"isDir": True,
}
],
"targetPath": encode_path(INTERNAL, local),
"step": 1,
"transferOptions": {
"noSchedule": True,
"target-action": "numbering",
"send-fileoption": {},
"syncType": 1,
"watchFolderType": 1, # 1 = on create, 2 = on modify
},
}
],
"schedules": [
{"type": "none", "startDateType": "now",
"startDate": now_iso(), "timezone": "Asia/Seoul"}
],
}
if webhook:
body["processors"] = [{
"category": "run",
"type": "http",
"config": {"url": webhook, "method": "POST"},
}]
return api("POST", "/api/automations", body)["automationId"]
receivers = {p: create_receive_watch(p, RECEIVE_HOOK) for p in PARTNERS}String createReceiveWatch(String partnerId, String webhook) {
Partner partner = PARTNERS.get(partnerId);
String remote = partner.receive().remote();
String local = partner.receive().local() + "/" + partnerId;
String name = "receive " + partnerId;
Map<String, Object> detail = Json.newObj(
"senderId", partner.device(),
"receiverId", INTERNAL,
"sourceItem", List.of(Json.newObj(
"hash", InnorixClient.encodePath(partner.device(), remote),
"filePath", remote,
"isDir", true)),
"targetPath", InnorixClient.encodePath(INTERNAL, local),
"step", 1,
"transferOptions", Json.newObj(
"noSchedule", true,
"target-action", "numbering",
"send-fileoption", Json.newObj(),
"syncType", 1,
"watchFolderType", 1)); // 1 = on create, 2 = on modify
Map<String, Object> body = new LinkedHashMap<>(Json.newObj(
"name", name, "flowName", name,
"transferType", "sync",
"timezone", "Asia/Seoul",
"step", 1, "isUpcoming", false,
"details", List.of(detail),
"schedules", List.of(Json.newObj(
"type", "none", "startDateType", "now",
"startDate", InnorixClient.nowIso(), "timezone", "Asia/Seoul"))));
if (webhook != null) {
body.put("processors", List.of(Json.newObj(
"category", "run", "type", "http",
"config", Json.newObj("url", webhook, "method", "POST"))));
}
return Json.str(client.apiObj("POST", "/api/automations", body), "automationId");
}async function createReceiveWatch(partnerId, webhook = null) {
const partner = PARTNERS[partnerId];
const remote = partner.receive.remote;
const local = `${partner.receive.local}/${partnerId}`;
const name = `receive ${partnerId}`;
const body = {
name,
flowName: name,
transferType: "sync",
timezone: "Asia/Seoul",
step: 1,
isUpcoming: false,
details: [{
senderId: partner.device,
receiverId: INTERNAL,
sourceItem: [{
hash: encodePath(partner.device, remote),
filePath: remote,
isDir: true,
}],
targetPath: encodePath(INTERNAL, local),
step: 1,
transferOptions: {
noSchedule: true,
"target-action": "numbering",
"send-fileoption": {},
syncType: 1,
watchFolderType: 1, // 1 = on create, 2 = on modify
},
}],
schedules: [{
type: "none",
startDateType: "now",
startDate: nowIso(),
timezone: "Asia/Seoul",
}],
};
if (webhook) {
body.processors = [{
category: "run",
type: "http",
config: { url: webhook, method: "POST" },
}];
}
const created = await client.api("POST", "/api/automations", body);
return created.automationId;
}async Task<string> CreateReceiveWatchAsync(string partnerId, string webhook = null)
{
Partner partner = Partners[partnerId];
string remote = partner.Receive.Remote;
string local = quot;{partner.Receive.Local}/{partnerId}";
string name = quot;receive {partnerId}";
var body = new JsonObject
{
["name"] = name,
["flowName"] = name,
["transferType"] = "sync",
["timezone"] = "Asia/Seoul",
["step"] = 1,
["isUpcoming"] = false,
["details"] = new JsonArray
{
new JsonObject
{
["senderId"] = partner.Device,
["receiverId"] = Internal,
["sourceItem"] = new JsonArray
{
new JsonObject
{
["hash"] = InnorixClient.EncodePath(partner.Device, remote),
["filePath"] = remote,
["isDir"] = true,
},
},
["targetPath"] = InnorixClient.EncodePath(Internal, local),
["step"] = 1,
["transferOptions"] = new JsonObject
{
["noSchedule"] = true,
["target-action"] = "numbering",
["send-fileoption"] = new JsonObject(),
["syncType"] = 1,
["watchFolderType"] = 1, // 1 = on create, 2 = on modify
},
},
},
["schedules"] = new JsonArray
{
new JsonObject
{
["type"] = "none",
["startDateType"] = "now",
["startDate"] = InnorixClient.NowIso(),
["timezone"] = "Asia/Seoul",
},
},
};
if (webhook != null)
{
body["processors"] = new JsonArray
{
new JsonObject
{
["category"] = "run",
["type"] = "http",
["config"] = new JsonObject { ["url"] = webhook, ["method"] = "POST" },
},
};
}
JsonObject created = await client.ApiObjAsync("POST", "/api/automations", body);
return J.Str(created, "automationId");
}There are four required considerations when creating the automation request.
| Item | Configuration |
|---|---|
isUpcoming |
Must be false. The server default of true ignores the schedule in the request and replaces it with a one-time five-minute schedule. Steps with triggerAutomation are forced to false by the server, so you only need to set it explicitly on the first step when no trigger is present. |
step |
Include it at both the top level and in details. It identifies the hop position within the flow. |
sourceItem |
Include both hash (path token) and filePath (plain-text path). |
syncType |
Include it inside transferOptions. 1 is one-way and 2 is two-way. |
Registration succeeds even if all four fields are omitted, but runtime behavior changes. If a recurring schedule runs only once, check isUpcoming first.
Include the partner identifier in the destination path. Partners may upload files with the same name, such as result.xlsx, so storing them in one path makes it impossible to distinguish the source.
The agent treats the file as fully written once its size stops changing, then emits the event. This prevents a partially uploaded large file from being collected.
Post-Receive Processing#
Start internal processing when a result file arrives
The callback arrives after the transfer completes. Process the receiving endpoint as follows.
def on_receive_hook(payload):
monitor_id = payload.get("monitorId")
if monitor_id:
detail = api("GET", f"/api/transfers/{monitor_id}")
if detail["status"] != STATUS_COMPLETE:
return notify_partner_failure(payload)
register_received_files(payload)void onReceiveHook(Map<String, Object> payload) {
String monitorId = Json.str(payload, "monitorId");
if (monitorId != null) {
Map<String, Object> detail = client.apiObj("GET", "/api/transfers/" + monitorId);
if (Json.intOr(detail, "status", -1) != InnorixClient.STATUS_COMPLETE) {
notifyPartnerFailure(payload);
return;
}
}
registerReceivedFiles(payload);
}async function onReceiveHook(payload) {
const monitorId = payload.monitorId;
if (monitorId) {
const detail = await client.api("GET", `/api/transfers/${monitorId}`);
if (detail.status !== STATUS_COMPLETE) return notifyPartnerFailure(payload);
}
return registerReceivedFiles(payload);
}async Task OnReceiveHookAsync(JsonObject payload)
{
string monitorId = J.Str(payload, "monitorId");
if (monitorId != null)
{
JsonObject detail = await client.ApiObjAsync("GET", "/api/transfers/" + monitorId);
if (J.Int(detail, "status", -1) != InnorixClient.StatusComplete)
{
await NotifyPartnerFailureAsync(payload);
return;
}
}
await RegisterReceivedFilesAsync(payload);
}The same transfer may generate multiple notifications, so make the receiving side idempotent and process the same event only once.
Review Exchange Status#
Review send and receive status by partner
from datetime import datetime, timedelta, timezone
def paginate(path, params=None, limit=200, max_pages=50):
query = dict(params or {})
query["limit"] = limit
cursor = None
for _ in range(max_pages):
if cursor:
query["cursor"] = cursor
result = api("GET", path, params=query) or {}
for record in result.get("data") or []:
yield record
pagination = result.get("pagination") or {}
if not pagination.get("hasMore"):
return
cursor = pagination.get("nextCursor")
if not cursor:
return
def partner_summary(partner_id, days=7):
partner = PARTNERS[partner_id]
end = datetime.now(timezone.utc)
fmt = "%Y-%m-%dT%H:%M:%SZ"
rows = list(paginate(
f"/api/devices/{partner['device']}/transfer-history", params={
"startDate": (end - timedelta(days=days)).strftime(fmt),
"endDate": end.strftime(fmt),
}))
return {
"sent": len([r for r in rows if r.get("sourceDeviceName") == INTERNAL]),
"received": len([r for r in rows if r.get("targetDeviceName") == INTERNAL]),
"failed": len([r for r in rows if r.get("status") in NOT_SUCCEEDED]),
"rows": rows,
}
for partner_id in PARTNERS:
summary = partner_summary(partner_id)
print(f"{partner_id:14} sent {summary['sent']:>3}"
f" received {summary['received']:>3} failed {summary['failed']:>3}")Map<String, Object> partnerSummary(String partnerId, int days) {
Partner partner = PARTNERS.get(partnerId);
Instant end = Instant.now();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
.withZone(ZoneOffset.UTC);
List<Map<String, Object>> rows = client.paginate(
"/api/devices/" + partner.device() + "/transfer-history",
Json.newObj("startDate", fmt.format(end.minus(days, ChronoUnit.DAYS)),
"endDate", fmt.format(end)), 200, 50);
int sent = 0, received = 0, failed = 0;
for (Map<String, Object> row : rows) {
if (INTERNAL.equals(Json.str(row, "sourceDeviceName"))) sent++;
if (INTERNAL.equals(Json.str(row, "targetDeviceName"))) received++;
Integer status = Json.intOrNull(row, "status");
if (status != null && InnorixClient.NOT_SUCCEEDED.contains(status)) failed++;
}
return Json.newObj("sent", sent, "received", received, "failed", failed, "rows", rows);
}async function* paginate(path, params = {}, limit = 200, maxPages = 50) {
const query = { ...params, limit };
let cursor = null;
for (let page = 0; page < maxPages; page += 1) {
if (cursor) query.cursor = cursor;
const result = (await client.api("GET", path, null, query)) || {};
for (const record of result.data || []) yield record;
const pagination = result.pagination || {};
if (!pagination.hasMore || !pagination.nextCursor) return;
cursor = pagination.nextCursor;
}
}
async function partnerSummary(partnerId, days = 7) {
const partner = PARTNERS[partnerId];
const end = new Date();
const fmt = (d) => d.toISOString().replace(/\.\d{3}Z$/, "Z");
const rows = [];
for await (const row of paginate(
`/api/devices/${partner.device}/transfer-history`, {
startDate: fmt(new Date(end.getTime() - days * 86400000)),
endDate: fmt(end),
})) rows.push(row);
return {
sent: rows.filter((r) => r.sourceDeviceName === INTERNAL).length,
received: rows.filter((r) => r.targetDeviceName === INTERNAL).length,
failed: rows.filter((r) => NOT_SUCCEEDED.has(r.status)).length,
rows,
};
}async Task<(int Sent, int Received, int Failed, List<JsonObject> Rows)>
PartnerSummaryAsync(string partnerId, int days = 7)
{
Partner partner = Partners[partnerId];
DateTime end = DateTime.UtcNow;
const string Fmt = "yyyy-MM-dd'T'HH:mm:ss'Z'";
List<JsonObject> rows = await client.PaginateAsync(
quot;/api/devices/{partner.Device}/transfer-history",
new Dictionary<string, object>
{
["startDate"] = end.AddDays(-days).ToString(Fmt),
["endDate"] = end.ToString(Fmt),
});
int sent = rows.Count(r => J.Str(r, "sourceDeviceName") == Internal);
int received = rows.Count(r => J.Str(r, "targetDeviceName") == Internal);
int failed = rows.Count(r =>
{
int? status = J.IntOrNull(r, "status");
return status != null && InnorixClient.NotSucceeded.Contains(status.Value);
});
return (sent, received, failed, rows);
}Transfer history is returned in the data.data array, and pagination information is returned in data.pagination.
Exception Handling#
Handle transfer failures differently from files that have not yet been received
A failed transfer and a partner that has not uploaded a file yet are different situations and require different responses.
def wait(monitor_id, timeout=3600, interval=3):
deadline = time.time() + timeout
while time.time() < deadline:
detail = api("GET", f"/api/transfers/{monitor_id}")
if is_terminal(detail):
return detail
time.sleep(interval)
raise TimeoutError(monitor_id)
def failed_files(monitor_id):
result = api("GET", f"/api/transfers/{monitor_id}/files", params={
"state": "any", "size": 500,
}) or {}
return [r for r in (result.get("children") or [])
if r.get("status") in NOT_SUCCEEDED]
def retry_failed(monitor_id):
rows = failed_files(monitor_id)
if not rows:
return 0
api("POST", f"/api/transfers/{monitor_id}/retry", {
"filesRetry": [
{"filePath": r["sourceFilePath"], "isDir": bool(r.get("isFolder"))}
for r in rows
]
})
return len(rows)public Map<String, Object> wait(String monitorId, int timeoutSeconds, int intervalSeconds) {
long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;
while (System.currentTimeMillis() < deadline) {
Map<String, Object> detail = apiObj("GET", "/api/transfers/" + monitorId);
if (isTerminal(detail)) return detail;
sleep(intervalSeconds * 1000L);
}
throw new ApiError(0, "timeout waiting for " + monitorId);
}
public List<Map<String, Object>> failedFiles(String monitorId) {
List<Map<String, Object>> failed = new ArrayList<>();
for (Map<String, Object> row : transferFiles(monitorId)) {
Integer status = Json.intOrNull(row, "status");
if (status != null && NOT_SUCCEEDED.contains(status)) failed.add(row);
}
return failed;
}
public int retryFailed(String monitorId) {
List<Map<String, Object>> rows = failedFiles(monitorId);
if (rows.isEmpty()) return 0;
List<Object> filesRetry = new ArrayList<>();
for (Map<String, Object> row : rows) {
String path = Json.str(row, "sourceFilePath");
if (path == null) continue;
filesRetry.add(Json.newObj("filePath", path, "isDir", Json.bool(row, "isFolder", false)));
}
api("POST", "/api/transfers/" + monitorId + "/retry", Json.newObj("filesRetry", filesRetry));
return rows.size();
}const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function wait(monitorId, { timeout = 3600, interval = 3 } = {}) {
const deadline = Date.now() + timeout * 1000;
while (Date.now() < deadline) {
const detail = await api("GET", `/api/transfers/${monitorId}`);
if (isTerminal(detail)) return detail;
await sleep(interval * 1000);
}
throw new Error(`timeout waiting for ${monitorId}`);
}
export async function failedFiles(monitorId) {
const result = (await api("GET", `/api/transfers/${monitorId}/files`, null, {
state: "any", size: 500,
})) || {};
return (result.children || []).filter((row) => NOT_SUCCEEDED.has(row.status));
}
export async function retryFailed(monitorId) {
const rows = await failedFiles(monitorId);
if (rows.length === 0) return 0;
await api("POST", `/api/transfers/${monitorId}/retry`, {
filesRetry: rows
.filter((row) => row.sourceFilePath)
.map((row) => ({ filePath: row.sourceFilePath, isDir: Boolean(row.isFolder) })),
});
return rows.length;
}public async Task<JsonObject> WaitAsync(string monitorId, int timeoutSeconds = 3600,
int intervalSeconds = 3)
{
long deadline = Environment.TickCount64 + timeoutSeconds * 1000L;
while (Environment.TickCount64 < deadline)
{
JsonObject detail = await ApiObjAsync("GET", "/api/transfers/" + monitorId)
.ConfigureAwait(false);
if (IsTerminal(detail)) return detail;
await Task.Delay(intervalSeconds * 1000).ConfigureAwait(false);
}
throw new ApiError(0, "timeout waiting for " + monitorId);
}
public async Task<List<JsonObject>> FailedFilesAsync(string monitorId)
{
List<JsonObject> rows = await TransferFilesAsync(monitorId).ConfigureAwait(false);
return rows.Where(row =>
{
int? status = J.IntOrNull(row, "status");
return status != null && NotSucceeded.Contains(status.Value);
}).ToList();
}
/// <summary>Only callable once the transfer has reached a terminal state.</summary>
public async Task<int> RetryFailedAsync(string monitorId)
{
List<JsonObject> rows = await FailedFilesAsync(monitorId).ConfigureAwait(false);
if (rows.Count == 0) return 0;
var filesRetry = new JsonArray();
foreach (JsonObject row in rows)
{
string path = J.Str(row, "sourceFilePath");
if (path == null) continue;
filesRetry.Add(new JsonObject
{
["filePath"] = path,
["isDir"] = J.Bool(row, "isFolder", false),
});
}
await ApiAsync("POST", "/api/transfers/" + monitorId + "/retry",
new JsonObject { ["filesRetry"] = filesRetry }).ConfigureAwait(false);
return rows.Count;
}def check_partner(partner_id, days=1):
partner = PARTNERS[partner_id]
summary = partner_summary(partner_id, days=days)
state = api("GET", f"/api/devices/{partner['device']}/connectivity") or {}
if not state.get("isConnected"):
return f"connection lost ({state.get('stateLabel')}) - recollect after recovery"
if summary["failed"]:
count = 0
for row in summary["rows"]:
if row.get("status") in NOT_SUCCEEDED and row.get("monitorId"):
count += retry_failed(row["monitorId"])
return f"transfer failed - retried {count} files"
if summary["received"] == 0:
return "nothing received - ask the partner"
return "ok"String checkPartner(String partnerId, int days) {
Partner partner = PARTNERS.get(partnerId);
Map<String, Object> summary = partnerSummary(partnerId, days);
Map<String, Object> state = client.apiObj("GET",
"/api/devices/" + partner.device() + "/connectivity");
if (!Json.bool(state, "isConnected", false)) {
return "connection lost (" + Json.str(state, "stateLabel") + ") - recollect after recovery";
}
if (Json.intOr(summary, "failed", 0) > 0) {
int count = 0;
for (Object node : Json.arrOf(summary, "rows")) {
Map<String, Object> row = Json.asObj(node);
Integer status = Json.intOrNull(row, "status");
String monitorId = Json.str(row, "monitorId");
if (status != null && InnorixClient.NOT_SUCCEEDED.contains(status) && monitorId != null) {
count += client.retryFailed(monitorId);
}
}
return "transfer failed - retried " + count + " files";
}
if (Json.intOr(summary, "received", 0) == 0) return "nothing received - ask the partner";
return "ok";
}async function checkPartner(partnerId, days = 1) {
const partner = PARTNERS[partnerId];
const summary = await partnerSummary(partnerId, days);
const state = (await client.api("GET",
`/api/devices/${partner.device}/connectivity`)) || {};
if (!state.isConnected) {
return `connection lost (${state.stateLabel}) - recollect after recovery`;
}
if (summary.failed) {
let count = 0;
for (const row of summary.rows) {
if (NOT_SUCCEEDED.has(row.status) && row.monitorId) {
count += await client.retryFailed(row.monitorId);
}
}
return `transfer failed - retried ${count} files`;
}
if (summary.received === 0) return "nothing received - ask the partner";
return "ok";
}async Task<string> CheckPartnerAsync(string partnerId, int days = 1)
{
Partner partner = Partners[partnerId];
var summary = await PartnerSummaryAsync(partnerId, days);
JsonObject state = await client.ApiObjAsync(
"GET", quot;/api/devices/{partner.Device}/connectivity");
if (!J.Bool(state, "isConnected", false))
{
return quot;connection lost ({J.Str(state, "stateLabel")}) - recollect after recovery";
}
if (summary.Failed > 0)
{
int count = 0;
foreach (JsonObject row in summary.Rows)
{
int? status = J.IntOrNull(row, "status");
string monitorId = J.Str(row, "monitorId");
if (status != null && InnorixClient.NotSucceeded.Contains(status.Value)
&& monitorId != null)
{
count += await client.RetryFailedAsync(monitorId);
}
}
return quot;transfer failed - retried {count} files";
}
if (summary.Received == 0) return "nothing received - ask the partner";
return "ok";
}| Category | Symptom | Response |
|---|---|---|
| Transfer Failure | Failed status in transfer history | Retry failed files |
| Not Received | No transfer-history record | Ask the partner to confirm |
| Connection Lost | isConnected is false |
Collect pending files after the connection recovers |
| Item | Details |
|---|---|
| Send | Transfers sent internally to the partner |
| Receive | Transfers retrieved through monitoring automation |
| Destination Path | Storage location separated by partner identifier |
| Status | Transfer status and success result |
| Connection | Connection state of the partner device |