The MSP ProDesk Vendor API Console is a single-file HTML dashboard that centralizes MSP procurement workflows across Ingram Micro and TD SYNNEX. It gives procurement teams a unified interface for product search, real-time pricing, BOM optimization, order submission, shipment tracking, spend analytics, and market intelligence — without switching between vendor portals.
The console runs in full demo mode out of the box — all data is generated from the static products[] array and hardcoded mock objects. When PROXY_BASE is set, each action calls the proxy which routes to the real vendor APIs and returns normalized JSON. The UI, render functions, and interaction model are production-ready and require no changes to go live.
developer.ingrammicro.com. TD SYNNEX hardware: real SOAP/XML API only — no REST equivalent. TD SYNNEX cloud/licenses: StreamOne Ion REST API. All paths in the dashboard have been corrected to match the real APIs. See Section 02 for the full audit.
/api/ingram/catalog.POST /resellers/v6/orders (Ingram) or SYNNEX SOAP when proxy is live.GET /resellers/v6.1/orders/{orderNumber}.Every data-fetching action, button, and displayed endpoint path was audited against live vendor documentation before any code was changed. The following table is the complete finding set.
| ORIGINAL PATH IN CODE | VERDICT | CORRECTED TO |
|---|---|---|
/v1/inventory/search |
Wrong version | GET /resellers/v6/catalog/productsearch — Ingram v6 RESTSOAP pnaserviceV05 — TD SYNNEX hardware |
GET /v1/pricing/reseller |
Wrong version + method | POST /resellers/v6/catalog/priceandavailability — Ingram (POST body, up to 50 SKUs)SOAP pnaserviceV05 — TD SYNNEX |
GET /v1/inventory/{sku}/detail |
Wrong version | GET /resellers/v6/catalog/details/{ingramPartNumber} — Ingram v6 |
POST /v1/orders/create |
Wrong version + path | POST /resellers/v6/orders — Ingram v6SOAP poserviceV02 — TD SYNNEX hardware |
POST /v1/orders/validate |
Endpoint does not exist | No separate validation endpoint in Ingram API. Validation is server-side during POST /orders itself. Kept as UI-only preflight with code comment. |
GET /v1/orders/{id}/tracking |
Wrong version | GET /resellers/v6.1/orders/{orderNumber} — returns status + shipment dataSOAP posserviceV02 — TD SYNNEX PO status |
POST /v1/returns/rma |
Wrong version + path | POST /resellers/v6/returns/createrequest — Ingram v6 |
GET /v1/cloud/subscriptions |
Wrong path | GET /resellers/v6/catalog/productsearch?keyword=cloud for Ingram catalog; cloud subscription management is a separate XI API |
PUT /v1/licenses/fulfill |
Wrong API + method | POST /ion.tdsynnex.com/api/v3/accounts/{id}/subscriptions — StreamOne Ion REST (cloud/SaaS only) |
GET /v1/procurement/optimize |
Does not exist | No vendor API. Optimization is local logic in the proxy comparing P&A responses from both vendors. Documented in code comment. |
GET /v1/clients/{id}/hardware-profile |
Does not exist | No vendor API. Client profiles are local configuration data in the proxy or a CRM. Documented in code comment. |
GET /v1/projects/export?format=csv |
Does not exist | No vendor API. BOM export is client-side CSV generation. Documented in code comment. |
ws.synnex.com/webservice/). There is no REST JSON equivalent. Your proxy must call the SOAP endpoints and normalize the XML responses to JSON before returning them to this console. The modern StreamOne Ion REST API (ion.tdsynnex.com/api/v3) covers cloud subscriptions and licenses only — not physical hardware.
The console is a self-contained single HTML file. No framework, no CDN dependencies except Google Fonts. All rendering is pure DOM manipulation using template literals.
The proxy must present a single normalized JSON API to this console regardless of which vendor backs each request. Ingram calls are pure REST. SYNNEX hardware calls require SOAP translation. The console never knows which protocol is used underneath.
The default landing pane. Shows a search bar, category/stock filter chips, and a responsive product grid. Filtering is instant as-you-type — no button press needed after initial load. The search button fires runSearch() and also shows the response terminal panel below the grid.
name, sku, cat, and desc fields. Case-insensitive. Live filtering via input event listener. Demo: filters the 24-item static products[] array.p.vendor field. Live. Activates live: proxy sends vendor parameter to respective API.p.cat.p.stock.sortProducts() function, no API call.viewProduct(id) opens modal with full detail, pricing, and MSRP estimate. "Add to Order" button → addToCart(id). "Check Pricing" → logs POST /resellers/v6/catalog/priceandavailability.renderProducts(products) in loadDashboard() with apiFetch('/api/ingram/catalog?limit=24', () => products).then(renderProducts). The proxy calls GET /resellers/v6/catalog/productsearch?keyword=all&pageSize=50&pageNumber=1 and returns the same product object shape.
Two-column layout: left has order configuration (vendor, PO number, shipping, line items); right shows the cart summary, order total, estimated lead time, and an API endpoint preview panel.
customerOrderNumber in the Ingram POST /resellers/v6/orders request body, or the equivalent SYNNEX SOAP field.removeItem(i). "Add Line Item" appends a blank row. renderLineItems() syncs with cartData[].POST /orders call itself. The button is a UX convenience only.logApiCall('POST', '/resellers/v6/orders', 'ingram') and shows the response terminal with a 201 Created mock response. Activates live when proxy is set.BOM-driven procurement optimization. Three hardcoded demo items (Fortinet FG-70F, Ubiquiti USW-48-POE, Ubiquiti U6 Pro). Compares Ingram vs SYNNEX pricing and stock for each line, then routes each item to the cheapest available distributor.
applyClientProfile() → logs /[LOCAL] client-profiles/{id}. No vendor API — local config data only.cacheGet() / cacheSet() functions use an in-memory JS object — not a real Redis instance. Proxy should implement real Redis caching.mfr|partNumber (e.g. fortinet/fg70f) for vendor-agnostic SKU matching.runProcurementOpt(). For each BOM line: if Ingram has stock AND Ingram total ≤ SYNNEX total → route Ingram, else SYNNEX. Shows optimal plan with per-item routing and grand total. Logs POST /resellers/v6/catalog/priceandavailability [BOM-OPT]. No vendor API for optimization — all logic is local./[LOCAL] bom-export.csv [client-side]. No vendor API. CSV generation is client-side only. To implement: build a CSV string from bomData[] and trigger a download via Blob URL.POST /resellers/v6/orders (Ingram) and POST /ws.synnex.com/webservice/poserviceV02 [SOAP] (SYNNEX). Activates live when proxy is set.Enter any order ID in the input and click TRACK. Three demo orders are wired: IM-2024-091422 (Ingram, in transit), TD-2024-003291 (SYNNEX, processing), IM-2024-091380 (Ingram, delivered). Unknown IDs fall back to the first demo order.
done (green check), active (blue pulsing ring), pending (empty circle). A vertical connector line links steps. Rendered by fetchTracking() from the allTrackingOrders object.fetchTracking() loops over .stat-row elements and matches by label text.pendingOrdersData[]. Shows order ID (color-coded by vendor) and status (color-coded by state). No live data in demo mode.jumpToTracking(orderId) which switches to the tracking pane and runs a fetch.fetchTracking(), replace const order = allTrackingOrders[id] with const order = await apiFetch('/api/ingram/orders/' + id, () => allTrackingOrders[id] || allTrackingOrders['IM-2024-091422']). The proxy calls GET /resellers/v6.1/orders/{orderNumber} (Ingram) or the SYNNEX SOAP PO Status service and returns a normalized step array.
Opens via the Analytics nav pill. Triggers animateAnalytics() which animates sparkline bars and vendor split bars. All values are hardcoded — no live data connection in any field of this pane.
GET /resellers/v6/invoices/search.GET /resellers/v6/orders/search?customerNumber={id} response total.logApiCall() which prepends to apiLogEntries[].Five intelligence cards. All static demo data. No vendor API natively supports stock predictions, hardware substitution rankings, or client hardware profiles — these features require a proxy-side analytics layer built on top of vendor API data.
POST /resellers/v6/catalog/priceandavailability periodically, tracks stock trends, flags items trending toward 0. No vendor pushes this data.GET /resellers/v6.1/orders/{id}.setVendor(v, el) — UI state only, no API call.renderSidebarOrderTiles() from the static sidebarOrders[] array. Status badges: tile-shipped (green), tile-transit (blue), tile-processing (yellow), tile-delivered (muted green), tile-backorder (red). Clicking calls jumpToTracking(orderId).logApiCall() invocation. Starts at 2 on load (two init calls). Persists for the browser session only.cartData.length. Updates on add/remove.https://api.ingrammicro.com:443/resellers/v6Sandbox:
https://api.ingrammicro.com:443/sandbox/resellers/v6Register:
developer.ingrammicro.com — requires active Ingram Micro reseller account number.
| ENDPOINT | METHOD | WHAT IT RETURNS | USED BY |
|---|---|---|---|
/resellers/v6/catalog/productsearch | GET | Product list by keyword, vendor part number, UPC, or category | Search pane |
/resellers/v6/catalog/details/{imPartNumber} | GET | Full product detail — specs, description, images, content data | Product modal |
/resellers/v6/catalog/priceandavailability | POST | Reseller pricing, discounts, stock by warehouse, lead time (up to 50 SKUs per call) | Check Pricing, BOM Optimizer |
/resellers/v6/orders | POST | Create and place a new order | Order Builder, Project Builder |
/resellers/v6.1/orders/{orderNumber} | GET | Full order detail including status, line items, shipment info, tracking | Tracking pane |
/resellers/v6/orders/search | GET | Search orders by PO number, order number, status, date range | Analytics (order count) |
/resellers/v6/returns/createrequest | POST | Create RMA/return request | sidebar endpoint list |
/resellers/v6/invoices/search | GET | Search invoices by PO, order number, date range | Analytics (MTD spend) |
These are the endpoints that back the hardware product search, pricing, and order workflows in this console. Your proxy must call SOAP, parse the XML response, and return JSON.
Used only for cloud subscription management — Microsoft 365, Acronis, and similar SaaS products. This is what backs the /v1/licenses/fulfill call that was corrected during the audit.
These are genuine vendor API gaps — features the console displays that no vendor API can fulfill directly. Each is documented in the dashboard source code with a comment. Your proxy must implement workarounds where noted.
| FEATURE | LIMITATION | PROXY WORKAROUND |
|---|---|---|
| Ingram order validation | No /orders/validate endpoint exists. Validation happens server-side during POST /orders. |
The Validate button is a UI-only preflight. Wire it to call POST /orders with simulate=true query param if Ingram ever adds dry-run support; otherwise leave as toast-only. |
| TD SYNNEX REST for hardware | No REST JSON API for hardware P&A or order submission. SOAP/XML only. | Proxy must call SOAP endpoints and normalize XML responses to JSON. The console never knows SOAP was involved. |
| Procurement optimization endpoint | No vendor API for routing optimization. | Proxy calls both P&A APIs for each BOM SKU, compares total price + availability, returns the optimal vendor assignment. All logic is in the proxy. |
| Client hardware profiles | No vendor API stores MSP client preferences. | Maintain a local JSON config file in the proxy (or pull from CRM) keyed by client ID. Return on GET /api/clients/{id}/profile. |
| Stock shortage prediction | No vendor API pushes stock trend data. | Proxy stores historical P&A snapshots in a database and computes trends. Requires periodic polling of P&A for watched SKUs. |
| Hardware substitution engine | No vendor API for compatibility scoring. | Maintain a local compatibility matrix. When a SKU is out of stock, proxy looks up alternatives in the matrix and checks their P&A status. |
| Price trend history | Ingram and SYNNEX APIs return current price only — no history endpoint. | Proxy stores P&A responses in a time-series database (Cosmos DB, InfluxDB) and serves historical data from its own store. |
| BOM CSV export | No vendor API. Export is client-side only. | Generate CSV from bomData[] using Blob + URL.createObjectURL(). No proxy call needed. |
developer.ingrammicro.com. Requires an active Ingram reseller account number in good standing. Sign-up authorizes your account for API channel access. You receive client_id and client_secret for OAuth2. Approval takes ~2 business days. Sandbox is available immediately.helpdeskus@tdsynnex.com with subject "Register for Price & Availability (PA) API access". You receive a customer number and EC Express credentials.PROXY_BASE to your Function App URL, set DEMO_MODE = false while testing, then replace the static data calls in loadDashboard() and individual action functions with apiFetch() calls. The console renders correctly with either real or demo data — the render functions are data-source agnostic.
- 1Get OAuth tokenYour proxy must call
POST https://api.ingrammicro.com:443/oauth/oauth20/tokenwithgrant_type=client_credentialsand your client credentials from Key Vault. Cache the token (expires in ~3600s). Add it asAuthorization: Bearer {token}to every downstream call. - 2Product search endpointWire
GET /api/ingram/catalog?keyword={q}&pageSize=50→ proxy callsGET /resellers/v6/catalog/productsearch?keyword={q}&pageSize=50&pageNumber=1. Map the responsecatalog[]array to the console'sproducts[]shape (vendor, sku, name, desc, price, stock, lead, cat). - 3Price and availabilityWire
POST /api/ingram/pricing(body:{skus:[]}) → proxy callsPOST /resellers/v6/catalog/priceandavailabilitywith body{products:[{ingramPartNumber,quantity}]}. Returns pricing, stock by warehouse, discounts. Note: this is a POST, not GET — the console was logging it incorrectly before the audit fix. - 4Order creationWire
POST /api/ingram/orders→ proxy callsPOST /resellers/v6/orders. Required body fields:customerOrderNumber,shipToInfo,lines[]. The proxy must also includeIM-CustomerNumber,IM-CountryCode, andIM-CorrelationIDheaders on every call. - 5Order trackingWire
GET /api/ingram/orders/{id}→ proxy callsGET /resellers/v6.1/orders/{orderNumber}. Map the response'sshipmentDetailsandorderStatusinto the step-timeline format the tracking pane expects.
- 1SOAP client in proxyAdd a SOAP client library to your Function App (e.g.
node-soapfor Node.js). Load the WSDL fromhttps://ws.synnex.com/webservice/pnaserviceV05?wsdl. Authenticate with your EC Express customer number and password (stored in Key Vault). - 2P&A (Price and Availability)Wire
POST /api/synnex/pricing→ proxy callspnaserviceV05.getPriceAndAvailability({skus:[]}). Parse the XML response — each item has<price>,<qty>,<status>. Return normalized JSON matching the Ingram P&A shape so the BOM optimizer can compare them directly. - 3Order submissionWire
POST /api/synnex/orders→ proxy callsposerviceV02.submitPO({...}). SOAP PO body includes: customer number, PO number, ship-to info, line items with vendor part numbers, quantities. Returns an order confirmation number. - 4Order statusWire
GET /api/synnex/orders/{id}→ proxy callsposserviceV02.getPOStatus({poNumber}). Returns shipment status, carrier, tracking number. Map to the same step-timeline format used by the Ingram tracking response. - 5Cloud licenses (StreamOne Ion)Wire
POST /api/synnex/licenses→ proxy callsPOST https://ion.tdsynnex.com/api/v3/accounts/{id}/subscriptionswith Bearer token from StreamOne Ion. This path is only for SaaS products (Microsoft 365, Acronis, etc.) — not physical hardware.
- ✓PROXY_BASE set. Updated in the dashboard's script block to your Function App URL (or left empty if same-origin).
- ✓DEMO_MODE = false during testing. Set to false so fetch errors appear in browser console. Return to true for production if you want silent fallback.
- ✓Ingram credentials in Key Vault. Application Settings show
@Microsoft.KeyVault(...)reference strings foringram-client-id,ingram-client-secret, andingram-customer-number. - ✓SYNNEX EC Express credentials in Key Vault.
synnex-customer-number,synnex-ec-username,synnex-ec-password— used for SOAP calls. - ✓StreamOne Ion credentials in Key Vault (if wiring cloud products).
ion-client-id,ion-client-secretorion-refresh-token. - ✓Product search returns results. Open the Search pane, type "cisco", check DevTools → Network tab. Confirm the proxy call to
/api/ingram/catalogreturns 200 with a JSON array. Products should appear in the grid. - ✓Price check returns live data. Click "Check Pricing" on any product. Confirm
POST /api/ingram/pricingreturns 200 with price data. Toast should show the actual reseller price from Ingram. - ✓Order submission sandbox tested. Set proxy to point at Ingram sandbox (
/sandbox/resellers/v6/orders). Submit a test order. Confirm 201 response with a valid order number. Test account:20-222222(Ingram sandbox). - ✓Tracking returns step data. Enter a known order number and click TRACK. Confirm the timeline updates with real steps from
GET /resellers/v6.1/orders/{id}. - ✓API call log shows real paths. Open Analytics pane. Confirm all entries in the API Call History log show the corrected
/resellers/v6/paths — not the old/v1/paths.
| SYMPTOM | CAUSE | FIX |
|---|---|---|
| Console shows demo data with proxy set | apiFetch() caught an error and fell back silently | Set DEMO_MODE = false. Open DevTools → Console. Find the [ProDesk] fetch failed message with the error detail. |
| Ingram API returns 401 | OAuth token expired or not sent | Token TTL is ~3600s. Your proxy must refresh it before expiry. Check that the Authorization: Bearer {token} header is present on every call. Verify IM-CustomerNumber header is also included. |
| Ingram API returns 400 on P&A call | Sending GET instead of POST, or wrong body shape | The P&A endpoint is POST /resellers/v6/catalog/priceandavailability with a JSON body containing {products:[{ingramPartNumber, quantity}]}. It accepts up to 50 SKUs per request. |
| SYNNEX SOAP returns parse error | XML namespace issues or wrong WSDL version | Use the exact WSDL URL: https://ws.synnex.com/webservice/pnaserviceV05?wsdl. Confirm your SOAP client loads the WSDL at startup and regenerates stubs if the WSDL changes. |
| BOM optimizer shows same prices for both vendors | SYNNEX SOAP not connected; proxy returning Ingram prices for both | Check proxy logs for SOAP call failures. Verify EC Express credentials in Key Vault. Test the SOAP P&A call directly from the proxy with a single known SYNNEX SKU. |
| Tracking pane shows demo order regardless of input | fetchTracking() still using static allTrackingOrders lookup | Replace the static lookup in fetchTracking() with an apiFetch() call. The demo fallback only fires when the proxy returns an error. |
| API call log shows /v1/ paths | Stale path in a code path not yet updated | Search the source for /v1/. The one remaining instance is in fetchTracking_OLD_REPLACED() — a dead function that is never called. Safe to ignore or delete the entire function. |
| Intelligence pane never shows live data | All five intelligence cards require proxy-side analytics — no vendor API backs them | Build the analytics layer in your proxy (historical P&A storage, substitution matrix, client profile config). See Section 13 for the workaround for each card. |