Partner and Supply Chain File Exchange

TeamsIT EngineersDevelopers

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.

text
                 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.

text
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.

BusinessSend to PartnerReceive from PartnerBusiness Use
OrdersPurchase Orders · Order DataOrder ResultsOrder Processing
SettlementSettlement Request DataSettlement ResultsSettlement Review
DesignDesign MaterialsRevised · Review MaterialsDesign Review
InspectionInspection Request DataInspection ResultsQuality 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.

text
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.

PartnerOutbound FilesInbound FilesProgress Status
Partner APurchase OrderOrder ResultComplete
Partner BDesign MaterialsRevision MaterialsIn Progress
Partner CInspection RequestInspection ResultComplete

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.

text
                    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.

text
Your System                              Partner System

/send/orders  ───────── Transfer ────────►  /receive/orders


/receive/results ◄───── Receive ────────  /send/results
CategoryYour SystemPartner SystemExecution Criteria
Outbound/send/orders/receive/ordersSchedule · File Ready
Inbound/receive/results/send/resultsSchedule · 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.

text
                 ┌───────────────┐
                  │ 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.

text
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 ValueMeaningTerminal
2CompleteYes
4ErrorYes
5CancelledYes
9Partially CompleteYes
99FailedYes
1 · 6 · 12 · 13Started · Transferring · Synchronizing · ReceivingNo

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.

ItemHow to Configure
isUpcomingMust 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
stepSet it in both the top level and details. It indicates the hop position within the workflow
sourceItemInclude both hash (the path token) and filePath (the plain-text path)
syncTypeSet 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"
CategorySymptomResponse
Transfer FailedTransfer history shows a failed statusRetransmit the failed files
Not ReceivedNo transfer history existsAsk the partner to confirm
DisconnectedisConnected is falseCollect queued files after reconnecting
Review ItemDetails
OutboundTransfer sent internally to the partner
InboundTransfer retrieved by watch automation
Destination PathStorage location separated by partner identifier
StatusTransfer status and success
ConnectionPartner system connection status