User guide

Distributor Integration

Looking for what it does rather than how to use it? Read the Distributor Integration overview .

Audience: platform administrators who operate the shared product catalogue. Scope: TD Synnex and Ingram Micro SFTP feeds — catalogue, cost, and availability.


What it is

Solidlio downloads product, cost, and stock feeds from Ingram Micro and TD Synnex over SFTP, parses them, and merges them into one shared catalogue where each product carries the cost and stock position of every distributor that sells it.

The catalogue is global and untenanted.


Concepts

ConceptWhat it is
DistributorOne supplier row, keyed by a unique code (INGRAM, TDSYNNEX). Holds costCurrency, isActive, last-sync stamps.
FeedOne file on the distributor’s SFTP host. Ingram publishes three; TD Synnex publishes one combined price-and-availability file.
DistributorStockOne row per (product, distributor): the distributor’s SKU, unitCost, msrp, per-warehouse quantities, availability.
DistributorCategoryA category code as the distributor names it, plus its mapping to a Solidlio CatalogCategory.
DistributorBrandA manufacturer name as the distributor spells it, plus its mapping to a Solidlio Brand.
DistributorWarehouseA named distribution centre. The warehouse code is the key used inside DistributorStock.warehouseStock.
DistributorSyncLogOne row per import run: type, status, counts, duration, and the error list.
CatalogProductThe distributor-agnostic product. Created or matched during a price sync, stamped with a platform SKU.

Sync types

TypeIngramTD Synnex
PRICEPRICE.ZIPPRICE.TXTThe P&A flat file (also carries stock)
AVAILALLETA.ZIPALLETA.TXTNot a separate feed — included in P&A
CATEGORIESNEWCATZ.ZIPNEWCATZ.TXTNot a separate feed — codes named from spec

Requesting AVAIL or CATEGORIES for TD Synnex returns 400 with Availability sync not supported for distributor: TDSYNNEX.

Sync statuses

PENDINGRUNNINGCOMPLETED | FAILED | CANCELLED

Feed formats

Ingram PRICE.TXT — comma-delimited, positional, 23 fields, no header row. Field 1 is the Ingram part number, field 3 the vendor (brand) name, field 6 the retail price, field 7 the manufacturer part number, field 9 the UPC, field 14 our cost, field 20 the four-character category code.

Ingram ALLETA.TXT — comma-delimited, quote-aware. Field 0 is the part number, field 1 the total quantity, then repeating three-field groups of warehouse code, ETA, and quantity.

Ingram NEWCATZ.TXT — three quoted columns: category name, parent code, subcategory code.

TD Synnex P&A — tilde-delimited (~), with HDR and DTL record types. Only DTL rows are imported.

Warehouses

Warehouse rows are created automatically by the sync from the codes the feed actually reports; an unrecognised code becomes a WH-<code> placeholder.

DistributorFeed codeSolidlio codeLocation
Ingram10CA-VVancouver (Richmond), BC
Ingram40CA-MToronto (Mississauga), ON
Ingram05CA-VDCVirtual DC — services and electronic delivery
TD Synnexcol 16DHADartmouth, NS
TD Synnexcol 20DCGCalgary, AB
TD Synnexcol 22DGUGuelph, ON
TD Synnexcol 61DMSMississauga, ON
TD Synnexcol 62DRNRichmond (Nelson), BC

Ingram codes 10 and 40 are counter-intuitive — 10 is the Vancouver warehouse.

Quantity 9999 is a flag, not a count. Both distributors use 9999 to mean “available, quantity undisclosed” on services, warranties and electronically delivered licences. Ingram does not — see §9.


Roles and permissions

No distributor page carries its own guard.


Walkthroughs

Bring a distributor online

Credentials are read from environment variables, not from the database.

  1. Go to Platform → Catalog → Distributors. The distributor row appears automatically the first time a sync runs — Ingram self-creates a row with code INGRAM, TD Synnex with code TDSYNNEX.
  2. Open the distributor → Settings → set Cost Currency to the currency the distributor invoices you in, and press Save Currency. This drives the store’s multi-currency conversion, so set it before the first price import.

The SFTP Configuration card on that page writes to Distributor.sftpConfig, which only the Test Connection button reads. It does not change where the importer connects. See §9.

Run the first Ingram import

Run the three feeds in this order — availability only updates stock rows that the price sync creates, and categories should exist before products reference them.

  1. Platform → Catalog → Distributors → Ingram Micro → Manage.
  2. In the Categories sync card, press Sync Now. Wait for the card to show a completed timestamp.
  3. In the Price sync card, press Sync Now. This is the long one — it creates or matches every product and writes DistributorStock.
  4. In the Availability sync card, press Sync Now.
  5. Open Category Mapping → Manage and Brand Mapping → Manage to review what mapped (§4.4).

Each button returns 202 Accepted immediately and runs in the background; the page does not block. Watch progress on the Recent Syncs table on the distributor overview page.

Run the TD Synnex import

TD Synnex has one feed, so there is one button.

  1. Platform → Catalog → Distributors → TD Synnex → Manage.
  2. Press Sync Now on the Price card.

The importer lists the SFTP directory, sorts filenames descending and takes the first — it assumes the newest P&A file sorts last by name.

Map categories and brands

Distributor taxonomies do not match Solidlio’s, so each distributor code must be mapped to a Solidlio category or brand.

  1. Distributor → Category Mapping → Manage (or Brand Mapping).
  2. Filter by Source = UNMAPPED to see what needs attention.
  3. Either set a target on a row with the per-row dropdown — that records mappingSource = MANUAL at confidence 1.0 — or press Auto-Map All.
  4. Auto-Map All asks for confirmation, then applies every suggestion at or above the auto-approve threshold and leaves the rest as suggestions.

How the confidence is produced

StrategyMax confidenceCan auto-approve?
Brand — exact name match1.00Yes (threshold 0.95)
Brand — Icecat supplier match0.95Yes
Brand — leading-token match0.95Yes
Brand — alias table0.90No — review required
Brand — fuzzy (Levenshtein/Jaccard/ngram)0.85No — review required
Category — Icecat bridge0.95Yes (threshold 0.90)
Category — name similarity0.85No — review required
Category — path similarity0.80No — review required

The Icecat bridge is how categories actually get mapped at scale: for a distributor category, Solidlio looks at up to 100 of its products that already matched an Icecat-linked catalogue category and takes the dominant one.

A loose alias plus an unordered lookup once auto-mapped a large block of real products onto the “Microsoft Demo” supplier; alias matching is now whole-word and the Icecat supplier lookup is an exact match.

Manage warehouses

Distributor → Settings → Warehouses. Add, edit, or delete rows. The code field is immutable once created, because it is the key inside DistributorStock.warehouseStock. Deleting a warehouse asks for confirmation and does not rewrite existing stock rows.

Test SFTP reachability

Distributor → Settings → SFTP Configuration → Test Connection. This dials the host in Distributor.sftpConfig, lists /, and reports connected/not-connected plus latency and file count.

Key paths are constrained to SFTP_KEY_DIR (default /etc/solidlio/sftp-keys) and any path containing .. is rejected before the filesystem is touched.


Configuration

Every value is optional; a sync throws if the host or username for its feed is empty.

Ingram Micro

Ingram issues a separate login per feed. All three must be set for a full import.

VariableDefaultUsed for
INGRAM_SFTP_USERPrice feed username
INGRAM_SFTP_USERNAMEAlternative to INGRAM_SFTP_USER
INGRAM_CATS_USERNAMECategories feed username
INGRAM_AVAIL_USERNAMEAvailability feed username
INGRAM_SFTP_KEY_PATHKey auth; if set, replaces password auth on all feeds
INGRAM_SFTP_DIRECTORY/outbound/pricingSee note below
INGRAM_ACCOUNT_NUMBERRecorded only

Set it only if your account uses a genuinely different directory.

TD Synnex

VariableDefaultUsed for
SYNNEX_SFTP_USERRequired; sync throws without it
SYNNEX_SFTP_KEY_PATHKey auth; if set, replaces password
SYNNEX_SFTP_DIRECTORY/Where the P&A file is listed
SYNNEX_TRADING_PARTNERRecorded only

Shared

VariableDefaultEffect
SFTP_KEY_DIR/etc/solidlio/sftp-keysDirectory test-connection key paths must live under

Database-held settings

FieldWhere editedEffect
Distributor.costCurrencySettings → Cost CurrencyReal. Stamped on products the Ingram sync creates; drives FX-normalised lowest-cost.
Distributor.isActiveRead by the list filter; no UI writes it.
Distributor.sftpConfigSettings → SFTP ConfigurationRead only by Test Connection. Not used by the importer.
Distributor.syncScheduleSettings → Sync ScheduleStored; does not schedule anything. See §9.

Changing costCurrency after products exist does not restate stored costs.


Plan tiers

None. Distributor integration is gated only by the platform administrator role.


Troubleshooting

Symptom / messageCauseFix
Availability sync not supported for distributor: TDSYNNEX (400)TD Synnex has no separate availability feedRun the price sync; it carries stock
Sync not implemented for distributor: <CODE> (400)The distributor code is not in the sync registry (INGRAM, TDSYNNEX, SYNNEX)Only these two distributors have importers
No matching file found in /The expected archive is absent from the SFTP directoryCheck INGRAM_SFTP_DIRECTORY; remember the /outbound/pricing fallback
No P&A files found on SFTPSYNNEX_SFTP_DIRECTORY has no .txt/.dat/.csv/.zip/.ap fileConfirm the directory and that TD Synnex has published
No TXT file found in ZIP archiveThe downloaded archive did not contain the expected .TXTRe-run; if it persists, the feed layout changed
Invalid SFTP key path (400)The key path contains .., or resolves outside SFTP_KEY_DIRMove the key under SFTP_KEY_DIR
Warehouse with this code already exists (409)Duplicate warehouse code for this distributorEdit the existing row instead
A sync log is stuck at RUNNINGThe pod died mid-importOn next boot, runs older than 6 hours are reconciled to FAILED with Reconciled: orphaned by service restart
Availability sync updated nothingAvailability only updates stock rows the price sync createdRun price first, then availability

Limits and known behaviour

Every distributor sync was therefore operator-triggered only.

Whether a scheduled run actually happened is now answerable. See Platform → Background Jobs: it reports each job’s last run, its last success, and flags a job that has not run within its expected cadence. The “Last sync” figures on this page are row watermarks and still stand still on a day the feed changed nothing — which is exactly why the job health page exists.

Both sync services read host, username, password and key path from environment variables. The form also has no password field, so a password-authenticated distributor can never be fully described by it.

There is no way to create, rename, deactivate or delete a distributor. No POST /admin/distributors or DELETE /admin/distributors/:id route exists, and no screen calls the PATCH that could set isActive. Rows appear only via the seed or the first sync run.

There is no sync-logs page. Six links across five screens point at /platform/catalog/distributors/<id>/logs; that route does not exist and all of them 404. The data is available from GET /sync/logs.

Sync preview is a stub. GET /:id/sync/preview validates its query and then returns { preview: [], message: "Preview functionality requires SFTP connection" } regardless of input.

A running sync cannot be cancelled from the UI. DELETE /sync/cancel works and is tested, but no screen calls it.

Availability and category syncs cannot be started from the distributor list. Only the distributor detail page has those buttons; the list page’s Sync Now always requests price.

Sync-log status is always written COMPLETED on a non-throwing run. The services compute a success flag separately — Ingram’s price sync tolerates up to 10% errored records — but the log row is written COMPLETED before that flag is evaluated. A run with many per-record errors reads as successful in the log.

Two counts on the Recent Syncs table are wrong. The api-client SyncLogEntry type declares recordsProcessed/recordsCreated/recordsUpdated/ recordsFailed/errorMessage; the columns are actually processedCount, createdCount, updatedCount, errorCount, errors. The UI renders sync.recordsProcessed ?? 0, so Records is permanently 0. The same type omits CANCELLED from its status union.

Ingram does not apply the 9999 availability flag. TD Synnex excludes 9999 from its Canadian total; Ingram writes it through verbatim, so an Ingram service or ESD SKU reports 9,999 units on hand. Anything comparing stock across the two distributors will prefer Ingram for these SKUs incorrectly.

TD Synnex products are not stamped with the distributor’s cost currency. The Ingram sync copies Distributor.costCurrency onto each product it creates; the TD Synnex sync does not, so those products take the CatalogProduct.costCurrency default of CAD regardless of how the distributor is configured.

TD Synnex never stamps lastAvailSync or lastCategorySync, because it only runs the price path. Those fields stay null forever and any health check reading them will report the feed as never having run.

TD Synnex parse errors are not persisted. The Ingram syncs push parse errors into DistributorSyncLog.errors; the TD Synnex sync logs the first five and discards the rest, so a run that dropped thousands of malformed lines still reads clean.

The Ingram price parser does not handle quoted fields. It splits on commas with no quote awareness. A comma inside a description shifts every later field — cost, UPC, MPN — and the shifted line still passes the field-count check, so it lands in the catalogue with no recorded error.

TD Synnex ETAs are discarded. The P&A parser reads an ETA date but toDistributorStockData hard-codes eta: null for every warehouse, so warehouseDetails never carries one. The Ingram availability path does populate it.

TD Synnex file selection is by filename sort. syncFromSftp lists the directory, filters to .txt/.dat/.csv/.zip/.ap, sorts, and takes the last. Any unrelated file in that directory that sorts later will be picked instead.

On a seeded database both rows exist, and all synced stock and logs attach to tdsynnex — so triggering a sync from the SYNNEX row appears to do nothing.

/platform/integrations/distributors is not a working screen. It renders hardcoded sample data (useDemoData = true) and every action on it — Full Catalog Sync, Test Connection, Disconnect, per-feed Sync Now, Save Settings, and the account-number/API-key/API-secret Connect form — has no handler attached. Use Platform → Catalog → Distributors. Note that the TD Synnex and Ingram Micro cards in the MSP, Org and Settings integration lists link to this page.

They are not reachable from the UI.

Dead schema fields. DistributorSyncLog.fileSize is never written. fileName is written only by the local-file TD Synnex path, never by an SFTP run. duration and errors are written but nothing reads them — the UI recomputes duration from the timestamps.

Auto-mapping is a full-table scan per row. Both mappers load every active brand or catalogue category and score it, once per distributor record, inside a sequential loop. Expect auto-map to be slow on a large catalogue.

Foreign-exchange rates are used without a staleness bound. Lowest-cost comparison converts through getRate without passing maxAgeMs, so if FX sync stops, cost comparison silently keeps using the last known rate.


Test coverage

The POST /sync/{price,avail,categories} happy paths are deliberately not covered end to end, because they require live SFTP credentials; the tests cover the guard, conflict, cancellation and error branches instead.

Questions this guide did not answer?

Ask us. You will get a reply from someone who uses the product every day.

Book a demo Contact us

A 30-minute walkthrough against your own workflow. No slides.