Getting Started
Core Concepts
Exchange files bidirectionally between your work environment and partner systems
Work with partners involves both files sent to them, such as purchase orders and design materials, and files received from them, such as inspection results and settlement data.
File exchange connects your work environment with partner systems and manages outbound and inbound files according to their respective paths and conditions.
Your Work Environment
│
Purchase Orders · Design Materials │
▼
┌────────────────┐
│ File Exchange │
└────────────────┘
▲
Inspection Results · Settlement Data │
│
Partner System

Configuring the files and paths exchanged with each partner lets you manage bidirectional file flows for operations such as orders, design, settlement, and inspection.
Exchange Flow
Send files, receive result files, and continue to the next task
Business files generated internally are transferred to partners, while result files processed by partners are received back and used for subsequent work.
Prepare Business Files
│
▼
Transfer to Partner
│
▼
Partner Processing
│
▼
Receive Result Files
│
▼
Internal Follow-up Work

Connecting outbound and inbound transfers in a single business flow lets you manage the entire process of files moving between partners and internal operations.
Business Benefits
Manage partner-specific file exchange through defined business workflows
Even when each partner has different required files and business processes, you can separate partner-specific files and exchange paths and use flows suited to each operation.
| Business | Send to Partner | Receive from Partner | Business Use |
|---|---|---|---|
| Orders | Purchase Orders · Order Data | Order Results | Order Processing |
| Settlement | Settlement Request Data | Settlement Results | Settlement Review |
| Design | Design Materials | Revised · Review Materials | Design Review |
| Inspection | Inspection Request Data | Inspection Results | Quality Review |
Business Teams
File Exchange
Select the files needed for work and exchange them with partners
Business teams select files required for current operations, such as orders, settlement, design, and inspection, then run the file exchange task for the appropriate partner.
Select Files
│
▼
Select Partner
│
▼
Transfer Files
│
▼
Receive Result Files
Received files can be used immediately for follow-up work such as review, revision, and approval.
Exchange Status
Check 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 | Outbound Files | Inbound Files | Progress Status |
|---|---|---|---|
| Partner A | Purchase Order | Order Result | Complete |
| Partner B | Design Materials | Revision Materials | In Progress |
| Partner C | Inspection Request | Inspection Result | Complete |

This lets business teams see which partner exchanges are currently in progress and use received results for subsequent work.
IT Engineers
Partner Connection
Connect partner systems to the internal file exchange environment
First, connect the partner systems with which files will be exchanged to the internal file 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 Operations Design Operations Settlement Operations

Exchange Configuration
Configure outbound and inbound paths and execution criteria as a single flow
For each partner, configure the path for files sent internally and the path for files received from the partner.
Define exchange targets based on file name, path, and type, and configure tasks to start at a defined time or when files are ready or arrive.
Your System Partner System
/send/orders ───────── Transfer ────────► /receive/orders
/receive/results ◄───── Receive ──────── /send/results
| Category | Your System | Partner System | Execution Criteria |
|---|---|---|---|
| Outbound | /send/orders | /receive/orders | Schedule · File Ready |
| Inbound | /receive/results | /send/results | Schedule · File Arrival |

Configuring outbound and inbound tasks together lets you manage partner file exchange as a single bidirectional flow.
Exchange Flow
Connect file exchange tasks for multiple partners by business operation
Each partner exchange can be configured as a single business flow by combining outbound and inbound tasks.
┌───────────────┐
│ Partner-Specific File Flow │
└───────┬───────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
Partner A Partner B Partner C
│ │ │
Order Exchange Design Exchange Settlement Exchange
│ │ │
└──────────┼──────────┘
▼
Internal Follow-up Work
Operational Review
Manage outbound and inbound results together by partner
Use Runs and detailed execution information to review each partner's file exchange tasks and processing results.
Reviewing outbound and inbound results by partner lets you manage current work and completed file exchanges together.
Partner A
├── Send → Completed
└── Receive → Completed
Partner B
├── Send → Completed
└── Receive → In Progress

When a task requires attention, use execution details and processing records to review connection status, file paths, and processing results, then rerun the required task.
Developers
Send files to partner systems and monitor partner-uploaded files for retrieval
Integration Preparation
Prepare shared request code and path conventions
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)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())Determine the transfer status using the values below. There are five terminal states, and the successful state is Complete (2).
| Status Value | Meaning | Terminal |
|---|---|---|
| 2 | Complete | Yes |
| 4 | Error | Yes |
| 5 | Cancelled | Yes |
| 9 | Partially Complete | Yes |
| 99 | Failed | Yes |
| 1 · 6 · 12 · 13 | Started · Transferring · Synchronizing · Receiving | No |
Partner Configuration
Manage partner-specific systems and paths as data
Each partner has different paths and business processes. Keeping the configuration as data means you do not need to change 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"If workspaces are separated by partner, send the workspace identifier with every request. This prevents an error by one partner from accessing another partner's files.
Send Files
Send internal files to partner systems
When sending files, explicitly set 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"])When sending folders, use sourcePaths with sendAllFolder: True.
Files sent to partners must retain their run history, so use numbering. If the same purchase order is revised and sent again, losing the previous version makes it impossible to determine which version was used as the basis for processing.
Receive Result Files
Detect and retrieve files uploaded by partners
Because the timing of partner uploads is unknown, use real-time watch automation. Set transferType to sync and include watchFolderType in transferOptions so files are transferred as soon as they arrive.
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}There are four requirements that must always be followed when creating an automation request.
| Item | How to Configure |
|---|---|
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. A stage with triggerAutomation forces the value to false, so you only need to set it directly on the first stage without a trigger |
step | Set it in both the top level and details. It indicates the hop position within the workflow |
sourceItem | Include both hash (the path token) and filePath (the plain-text path) |
syncType | Set it inside transferOptions. 1 is one-way and 2 is two-way |
Registration succeeds even if all four items are omitted, but the behavior changes at execution time. If a recurring schedule was registered but runs only once, check isUpcoming first.
Include the partner identifier in the destination path. Since each partner can upload a file with the same name, such as result.xlsx, using one path makes the source impossible to distinguish.
The agent considers writing complete when file size stops changing and then sends the event. This prevents retrieving a truncated file while a partner is still uploading a large file.
Connect Post-Receipt Processing
Start internal business work when a result file arrives
The call occurs after transfer completion, and the receiving endpoint processes the request 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)The same transfer can generate multiple notifications, so the receiver should handle repeated delivery of the same event only once.
Review Exchange Status
Check outbound and inbound 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}")Transfer history is returned in the data.data array, while pagination information is returned in data.pagination.
Exception Handling
Distinguish transfer failures from missing inbound files and handle them separately
A failed transfer and a file that the partner has not uploaded 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)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"| Category | Symptom | Response |
|---|---|---|
| Transfer Failed | Transfer history shows a failed status | Retransmit the failed files |
| Not Received | No transfer history exists | Ask the partner to confirm |
| Disconnected | isConnected is false | Collect queued files after reconnecting |
| Review Item | Details |
|---|---|
| Outbound | Transfer sent internally to the partner |
| Inbound | Transfer retrieved by watch automation |
| Destination Path | Storage location separated by partner identifier |
| Status | Transfer status and success |
| Connection | Partner system connection status |