@fluentcommerce/fc-connect-sdk
v0.1.54
Published
Fluent Commerce SDK - Deno & Node.js Compatible
Maintainers
Readme
Fluent Commerce Connect SDK
TypeScript SDK for building Fluent Commerce integrations across Node.js, Deno, and the Versori platform.
- Package:
@fluentcommerce/fc-connect-sdk - Documentation: Complete markdown guides included in the package
Table of Contents
- Overview - What is this SDK?
- Features - What can it do?
- Which API Should I Use? - Batch vs Event vs GraphQL 🆕
- Install - Get started
- Quick Start - 5-minute example
- What's Next? - Templates, guides, next steps
- Universal Mapping - Field transformations
- Mapping Approaches - Which mapper to use? 🆕
- Mapper Comparison Guide - Real-world scenario comparison 🆕
- XML/JSON → GraphQL - Auto-generate mutations 🆕
- Complete Workflow Examples - Full integration patterns
- Architecture - How it works
- More Examples - Common patterns
- Core Services - API reference
- CLI Tools - Command-line utilities
- Standalone Usage - Node.js/Deno app integration
- CLI Profile Integration - Reuse Fluent CLI profiles
- Authentication & Webhooks - Security
- Security & Compliance - Security audits, SOC 2, vulnerability management 🆕
- Common Pitfalls - Troubleshooting
- Support - Help & resources
Overview
Build Fluent Commerce integrations faster with a production-ready SDK that handles the hard parts: authentication, data parsing, field mapping, error handling, and multi-runtime compatibility.
What you get:
- ✅ Universal Mapping – Transform CSV, XML, JSON, Parquet with one config
- ✅ Auto-Pagination – Extract thousands of records automatically
- ✅ Multi-Runtime – Same code works in Node.js, Deno, and Versori platform
- ✅ Production Features – Connection pooling, retry logic, state management, webhook validation
- ✅ 23+ Templates – Copy-paste ready workflows for common patterns
The SDK provides composable services – you own the workflow. Mix and match data sources, parsers, mappers, and API clients to move data between Fluent Commerce and external systems.
Read → Parse → Map → Your Logic → Archive🤔 Quick Decisions:
- Not sure which mapper? → Mapping Approaches Guide
- Which API to use? → API Decision Guide (Batch vs Event vs GraphQL)
- Want a ready-made solution? → 23+ Production Templates
🎯 Use This SDK If You're Building...
Quick pattern matching for common integration scenarios:
| Scenario | What It Looks Like | |----------|-------------------| | 📦 Inventory Sync | Daily/hourly inventory updates from WMS → Fluent (CSV/Parquet files) | | 🛒 Order Integration | E-commerce orders (SFCC, Shopify) → Fluent via webhooks (XML/JSON) | | 📊 Data Warehouse ETL | Extract Fluent data → Snowflake/BigQuery for analytics (GraphQL queries) | | 🔄 B2B File Exchange | Process EDI/XML files from trading partners via SFTP | | 📱 Admin Tools | Internal dashboards with file upload/processing capabilities | | 🔔 Real-time Events | Webhook processing for inventory updates, order status changes |
Not sure if SDK fits? See When to Use This SDK for detailed decision matrix.
Feature Highlights
Core Services:
- Universal Mapper – one mapping config for CSV, XML, JSON, Parquet (16 built-in resolvers, custom resolver registry)
- Extraction Orchestrator – high-level GraphQL extraction with auto-pagination, path-based extraction, validation, statistics
- GraphQL Error Classification – automatic retry guidance based on Fluent error codes (C/S/T prefix), retryability detection with actionable recommendations (see
docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md) - Batch Operations – use
client.createJob()andclient.sendBatch()for batch processing.FluentBatchManageravailable for Versori workflows (seedocs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md) - State Management – distributed state with KV storage, file deduplication, job caching, distributed locking with stale detection
- Job Tracker – lifecycle tracking, automatic expiration handling, metadata persistence
- Preflight Validator – pre-execution validation saves API quota, catches config errors early
- Webhook Validation Service – cryptographic signature validation (RSA SHA256/SHA1/SHA512)
- Webhook Authentication Service – Secure API key generation and validation (Deno-compatible)
- Partial Batch Recovery – graceful handling of partial batch failures with retry logic
Data Layer:
- S3 Data Source – streaming I/O with presigned URLs (works anywhere, no AWS SDK required), constant memory footprint
- SFTP Data Source – connection pooling + wait queues, exponential backoff retry, Deno compatibility for AWS Transfer Family
- Streaming Parsers – CSV, XML, JSON, Parquet with memory-efficient processing, validation hooks
- Smart Error Handling – 4xx errors fail fast, 5xx exponential backoff, job expiration auto-recreation. Consistent
statusCodefield across success and error responses
Architecture:
- Multi-Runtime – single codebase for Node.js ≥18, Deno, Versori platform (auto-detection via universal client factory)
- Stateless & Parallel-Safe – all services safe for
Promise.all(), idempotent operations - Layered Design – clear separation: Client → Service → Data → Orchestration
- Universal Compatibility – consistent API regardless of environment
Supported Runtimes
| Runtime | Support | Notes |
| ---------------- | ------- | --------------------------------------- |
| Node.js ≥ 18 | ✅ | Native fetch & FormData required |
| Deno | ✅ | Import using npm: specifier. SFTP: Only AES-128 ciphers supported (see troubleshooting guide) |
| Versori Platform | ✅ | Pass Versori ctx directly to services |
Deno Compatibility Notes:
- ✅ All ESM imports have proper extensions
- ✅ All Node.js imports use
node:prefix - ✅ Buffer support via
import { Buffer } from 'node:buffer'; - ⚠️ SFTP: Limited to
aes128-ctrandaes128-cbcciphers (Deno crypto limitations) - 📚 Complete Guide:
docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md
When to Use This SDK
| Scenario | When SDK Helps | When to Skip It | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | UI Applications | ✅ Admin tools with file upload/processing✅ Internal dashboards with data export✅ Complex data transformations | ⚠️ Simple CRUD operations⚠️ Real-time UI updates⚠️ Mobile apps (bundle size) | | Event Streaming | ✅ Webhook-based event processing✅ Scheduled batch reactions to events | ⚠️ Real-time streaming⚠️ Sub-second latency requirements | | Simple Scripts | ✅ Need parsing (CSV/XML/JSON)✅ Need field mapping/transformation✅ Need retry/error handling | ⚠️ Single GraphQL query⚠️ No transformation needed⚠️ Prototyping/exploration | | Non-Fluent Systems | ❌ SDK is Fluent Commerce specific | ❌ Use system-specific SDKs |
💡 Key Insight: The SDK shines when you need data transformation, file processing, or multi-step workflows. For simple API calls, direct GraphQL may be lighter.
🔌 Which API Should I Use?
Choose the right Fluent Commerce API for your use case:
| What You're Doing | Use This API | SDK Method | Why |
|----------------------|------------------|----------------|---------|
| Bulk inventory sync (CSV/Parquet files, 1000+ records) | Batch API | createJob() + sendBatch() | Optimized for high volume, BPP change detection, handles thousands of records efficiently |
| Product catalog sync (products, variants, prices) | Event API | sendEvent() | Triggers Rubix workflows, validates business rules, workflow orchestration |
| Location setup (stores, warehouses) | Event API | sendEvent() | Requires workflow orchestration |
| Order creation (one-time orders) | GraphQL | client.graphql() with createOrder mutation | Triggers Order CREATED event, full control |
| Order updates/events (status changes) | Event API | sendEvent() | Triggers workflows for order state changes |
| Audit/search event history (trace flows, investigate failures) | Event API (GET) | getEvents() + getEventById() | Read-only event/audit retrieval with filters and pagination |
| Customer data (registration, profiles) | GraphQL | client.graphql() with mutations | No Rubix workflow support for customers |
| Data extraction (export to S3/SFTP) | GraphQL | client.graphql() + ExtractionOrchestrator | Query with auto-pagination, extract thousands of records |
| Single operations (create one product, update one location) | GraphQL | client.graphql() | Direct control, immediate feedback |
Quick Rules
- Batch API: Only supports
INVENTORY+UPSERT(no products, orders, etc.) - Event API: For entities that need workflow triggers (products, locations, custom entities)
- GraphQL: For queries, single mutations, customers, and when you need direct control
📚 Complete Guide: See templates for detailed examples - docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/ (Batch API) and docs/01-TEMPLATES/versori/workflows/ingestion/event-api/ (Event API)
Core Services
| Area | Services & Utilities | Typical Use Case | Notes |
| -------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------- | ----------------------------------------- |
| Data sources | S3DataSource, SftpDataSource (connection pooling) | Daily inventory files, EDI integrations | Stream-safe reads, automatic retries |
| Parsers | CSVParserService, XMLParserService, JSONParserService, ParquetParserService | Price lists, B2B orders, API responses, analytics | Works standalone or with the orchestrator |
| Mapping | UniversalMapper, sdkResolvers, custom resolver registry | Field normalization, date/time conversions | One mapping config for every format |
| Execution | ExtractionOrchestrator, PreflightValidator, PartialBatchRecovery, JobTracker | Data exports, config validation, error recovery | Build full workflows quickly |
| Auth / Clients | createClient, FluentClient, FluentVersoriClient, FluentConnectionTester | Node.js scripts, Versori workflows | Runtime-aware factory, OAuth2 refresh, automatic logging, optional connection validation. See docs/04-REFERENCE/testing/modules/04-REFERENCE-testing-03-fluent-testing.md for connection testing |
| Webhooks | FluentClient.validateWebhook, parseWebhookPayload, WebhookAuthService | Secure webhook processing, B2B event handling | RSA signature validation, API keys |
| Errors | classifyError, classifyErrors, ErrorClassification | GraphQL error handling, retry decisions | Auto-classifies Fluent error codes |
| Logging | createConsoleLogger, toStructuredLogger, generateCorrelationId | Standalone scripts, request tracking | Pure-function log utilities |
📚 Learn More: docs/02-CORE-GUIDES/ - Complete API reference and guides for all services
Note: For batch operations, use client.createJob() and client.sendBatch() directly (shown in examples). FluentBatchManager is available for Versori workflows - see docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md for details.
Install
# npm (recommended)
npm install @fluentcommerce/fc-connect-sdk
# yarn
yarn add @fluentcommerce/fc-connect-sdk
# pnpm
pnpm add @fluentcommerce/fc-connect-sdkDeno:
import { createClient } from 'npm:@fluentcommerce/fc-connect-sdk';Optional TypeScript Settings
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2022"],
"esModuleInterop": true,
"resolveJsonModule": true,
"strict": true
}
}Verify Installation
Quick test to confirm SDK is working:
import { createClient } from '@fluentcommerce/fc-connect-sdk';
console.log('✅ SDK imported successfully!');
// Test that core services are accessible
console.log('Available:', { createClient: typeof createClient });Expected output:
✅ SDK imported successfully!
Available: { createClient: 'function' }If you see this, you're ready for the 5-Minute Quickstart!
🏃 5-Minute Quickstart
Goal: Send your first batch of inventory to Fluent Commerce.
Step 1: Install
npm install @fluentcommerce/fc-connect-sdkStep 2: Configure Authentication
Create .fluentrc.json:
{
"baseUrl": "https://api.fluentcommerce.com",
"clientId": "YOUR_CLIENT_ID",
"clientSecret": "YOUR_CLIENT_SECRET",
"username": "YOUR_USERNAME",
"password": "YOUR_PASSWORD",
"retailerId": "YOUR_RETAILER_ID"
}Step 3: Send Test Data
import { createClient } from '@fluentcommerce/fc-connect-sdk';
import * as fs from 'fs';
// Load config
const config = JSON.parse(fs.readFileSync('.fluentrc.json', 'utf8'));
// Create authenticated client
const client = await createClient({ config });
// Optional: Validate connection immediately (fail fast)
// Recommended for production to catch auth issues early
// const client = await createClient({ config }, { validateConnection: true });
// This validates OAuth2 tokens and API connectivity before returning
// Create job
const job = await client.createJob({
name: 'quickstart-test',
retailerId: config.retailerId,
});
// Send test inventory
const batch = await client.sendBatch(job.id, {
entityType: 'INVENTORY',
action: 'UPSERT',
source: 'QUICKSTART', // Optional: tracks data source
event: 'InventoryQuantityUpdate', // Optional: targets Rubix workflow event (defaults to InventoryChanged)
entities: [
{
skuRef: 'TEST-SKU-001',
qty: 10,
locationRef: 'DC01',
retailerId: config.retailerId,
},
{
skuRef: 'TEST-SKU-002',
qty: 5,
locationRef: 'DC01',
retailerId: config.retailerId,
},
],
});
console.log('✅ Successfully sent test inventory!');
console.log(`📊 Job ID: ${job.id}, Batch ID: ${batch.id}`);Step 4: Verify
Check the Fluent Commerce console to see your test inventory records.
🎉 Success! You've sent your first batch. See included documentation for production templates and patterns.
🐛 Troubleshooting Quick Fixes
Common issues:
- 401 Unauthorized → Check
clientId/clientSecret, verify OAuth2 credentials - Batch API only supports INVENTORY → Use Event API for products/orders, GraphQL for customers
- Connection pool errors → Always call
dataSource.dispose()after use - Deno/Versori Buffer errors → Add
import { Buffer } from 'node:buffer'; - Schema validation errors → Use
npx fc-connect validate-schemato check mapping config
📚 Full Troubleshooting: docs/00-START-HERE/TROUBLESHOOTING-QUICK-REFERENCE.md - Fixes 90% of common issues
🎯 What's Next?
Now that you've sent your first batch, explore these resources to build production integrations:
📋 Production-Ready Templates
Copy-paste 23+ working templates for common scenarios:
- SFTP CSV → Batch API - Daily inventory sync from SFTP
docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md
- S3 Parquet → Extraction - Extract data to data warehouse
docs/01-TEMPLATES/standalone/(seegraphql-query-export.md,graphql-to-parquet-partitioned-s3.md)
- XML Webhook → GraphQL - Real-time order ingestion
docs/01-TEMPLATES/versori/webhooks/xml-order-ingestion.md
- Payment Gateway Integration - Adyen payment flows (Capture, Refund, Cancel, ReAuth)
docs/01-TEMPLATES/versori/webhooks/template-payment-gateway-integration.md
- Browse All Templates - Find the right pattern for your use case
docs/01-TEMPLATES/ordocs/TEMPLATE-LOADING-MATRIX.md
📚 Core Guides
Learn SDK features in depth:
- Complete Mapping Guide - Field transformations, custom resolvers
docs/02-CORE-GUIDES/mapping/
- Auto-Pagination - Handle large GraphQL result sets
docs/02-CORE-GUIDES/auto-pagination/
- Data Sources - S3 and SFTP operations
docs/02-CORE-GUIDES/data-sources/
- Troubleshooting - Fixes 90% of common issues
docs/00-START-HERE/TROUBLESHOOTING-QUICK-REFERENCE.md
🏗️ Deep Dive
Understand the SDK architecture and advanced patterns:
- SDK Architecture - Jump to Architecture section
- Complete Workflow Examples - Jump to Examples
- Mapper Comparison - Which mapper to use?
🗺️ Universal Mapping
One mapping pattern for ALL data formats - XML, JSON, CSV, Parquet.
XML/JSON → Fluent with Custom Resolvers
Transform any XML or JSON structure to Fluent Commerce entities:
import { UniversalMapper, XMLParserService } from '@fluentcommerce/fc-connect-sdk';
// XML input example
const xmlData = `
<InventoryUpdate>
<Item>
<SKU>PROD-001</SKU>
<Location>DC-01</Location>
<Qty>100</Qty>
<LastUpdated>2025-01-27T10:30:00Z</LastUpdated>
<Price currency="USD">29.99</Price>
</Item>
</InventoryUpdate>
`;
// Parse XML to JavaScript object
const parser = new XMLParserService();
const parsed = await parser.parse(xmlData, {
arrayPath: 'InventoryUpdate.Item', // Extract array of items
});
// Setup mapper with custom resolvers
const mapper = new UniversalMapper(
{
fields: {
// Direct mapping
skuRef: { source: 'SKU', required: true },
locationRef: { source: 'Location', required: true },
// Built-in SDK resolver
qty: { source: 'Qty', resolver: 'sdk.parseInt' },
// Custom resolver for nested data
price: {
source: 'Price',
resolver: 'custom.extractPrice',
},
// Custom resolver for date transformation
lastModified: {
source: 'LastUpdated',
resolver: 'custom.parseISODate',
},
// Static value
type: { defaultValue: 'DELTA' },
status: { defaultValue: 'ACTIVE' },
},
},
{
// Register custom resolvers
customResolvers: {
'custom.extractPrice': (value, sourceData, helpers) => {
// Extract value from nested object: { _text: "29.99", "@_currency": "USD" }
return helpers.parseFloatSafe(value?._text || value, 0);
},
'custom.parseISODate': (value, sourceData, helpers) => {
// Transform ISO date to Fluent format
const date = new Date(value);
return date.toISOString().split('T')[0]; // "2025-01-27"
},
},
}
);
// Map each item
for (const item of parsed) {
const result = await mapper.map(item);
if (result.success) {
console.log('Mapped:', result.data);
// Output: { skuRef: 'PROD-001', locationRef: 'DC-01', qty: 100, price: 29.99, lastModified: '2025-01-27', ... }
} else {
console.error('Mapping failed:', result.errors);
}
}JSON Mapping Example
// JSON input
const jsonData = {
product_id: 'SKU-001',
warehouse: 'WH-NYC',
stock_level: '50',
updated_at: '2025-01-27T10:30:00Z',
};
const mapper = new UniversalMapper({
fields: {
skuRef: { source: 'product_id', required: true },
locationRef: { source: 'warehouse', required: true },
qty: { source: 'stock_level', resolver: 'sdk.parseInt' },
type: { defaultValue: 'DELTA' },
},
});
const result = await mapper.map(jsonData);
// Output: { skuRef: 'SKU-001', locationRef: 'WH-NYC', qty: 50, type: 'DELTA' }Array Mapping with GraphQL Edges/Nodes
IMPORTANT: The SDK supports two patterns for GraphQL edges/nodes arrays. Both produce identical results - choose based on preference:
Input Data (GraphQL Response):
{
items: {
edges: [
{ node: { ref: "FFI-001", quantity: 10 } },
{ node: { ref: "FFI-002", quantity: 5 } }
]
}
}Pattern 1: Wildcard syntax (recommended)
{
"fields": {
"items": {
"source": "items.edges[*].node", // ✅ SDK extracts nodes: [{ ref: "..." }, ...]
"isArray": true,
"fields": {
"ref": { "source": "ref" }, // ✅ Direct access (no "node." prefix)
"quantity": { "source": "quantity", "resolver": "sdk.parseInt" }
}
}
}
}Result: { items: [{ ref: "FFI-001", quantity: 10 }, { ref: "FFI-002", quantity: 5 }] }
Pattern 2: Direct edges access
{
"fields": {
"items": {
"source": "items.edges", // ✅ SDK extracts edges: [{ node: {...} }, ...]
"isArray": true,
"fields": {
"ref": { "source": "node.ref" }, // ✅ Need "node." prefix
"quantity": { "source": "node.quantity", "resolver": "sdk.parseInt" }
}
}
}
}Result: { items: [{ ref: "FFI-001", quantity: 10 }, { ref: "FFI-002", quantity: 5 }] } (identical to Pattern 1)
For XML with <Item> wrapper elements, add nested object wrapper to either pattern:
{
"fields": {
"Items": {
"source": "items.edges[*].node", // or "items.edges"
"isArray": true,
"fields": {
"Item": { // ✅ Creates <Item> elements
"fields": {
"@ref": { "source": "ref" }, // or "node.ref" for Pattern 2
"quantity": { "source": "quantity", "resolver": "sdk.parseInt" }
}
}
}
}
}
}XML Output (both patterns):
<Items>
<Item ref="FFI-001"><quantity>10</quantity></Item>
<Item ref="FFI-002"><quantity>5</quantity></Item>
</Items>Key Difference: Pattern 1 extracts nodes first (cleaner paths), Pattern 2 extracts edges (more explicit). Both produce identical mapped data and XML output.
📚 Learn More: docs/02-CORE-GUIDES/mapping/modules/02-CORE-GUIDES-mapping-05-advanced-patterns.md - Complete array mapping guide with step-by-step examples
Built-in SDK Resolvers
String: sdk.uppercase, sdk.lowercase, sdk.trim, sdk.toString
Number: sdk.parseInt, sdk.parseFloat, sdk.number
Date: sdk.formatDate, sdk.formatDateShort, sdk.parseDate
Type: sdk.boolean, sdk.parseJson, sdk.toJson
Utility: sdk.identity, sdk.coalesce, sdk.default
📚 Learn More:
- Complete Mapping Guide:
docs/02-CORE-GUIDES/mapping/modules/- Universal mapping documentation (7 modules) - Custom Resolvers:
docs/02-CORE-GUIDES/mapping/resolvers/- Building custom transformation logic (7 modules) - GraphQL Mutations:
docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/- XML/JSON to GraphQL (12 modules) - Advanced Patterns:
docs/02-CORE-GUIDES/mapping/modules/02-CORE-GUIDES-mapping-05-advanced-patterns.md- Array mapping, GraphQL edges/nodes (all 3 options)
🎯 Mapping Approaches: Which One Do I Use?
Quick Decision Table
| Your Scenario | Use This | Why |
|-------------------|--------------|---------|
| 📦 Bulk inventory files (CSV/Parquet) → Fluent | UniversalMapper + Batch API | Optimized for thousands of records, change detection |
| 📬 XML webhooks (SFCC orders, EDI) → Fluent | GraphQLMutationMapper | Auto-generates GraphQL mutations from XML |
| 🌐 JSON webhooks (Shopify, APIs) → Fluent | GraphQLMutationMapper | Schema validation + auto-query building |
| 🔧 Simple field transforms (rename, type convert) | UniversalMapper | Lightweight, no GraphQL needed |
| 🎨 Custom business logic (tax calc, SKU generation) | UniversalMapper + Custom Resolvers | Maximum flexibility |
Detailed Comparison
| Feature | UniversalMapper | GraphQLMutationMapper | Manual GraphQL | |---------|----------------|----------------------|----------------| | Best For | CSV/Parquet → Batch API | XML/JSON → GraphQL mutations | Simple queries | | Input Formats | Any (CSV, XML, JSON, Parquet) | XML, JSON | Any (you build it) | | Output | Plain JavaScript objects | GraphQL mutation + variables | You build everything | | Schema Validation | ❌ No | ✅ Yes (validates against GraphQL schema) | ❌ No | | Auto-generates GraphQL | ❌ No | ✅ Yes | ❌ No | | Bulk Ingestion | ✅ Yes (Batch API) | ❌ No (one mutation at a time) | Depends | | Learning Curve | 🟢 Easy | 🟡 Medium | 🟢 Easy | | Setup Complexity | Low | Medium (needs mapping config) | Low | | Custom Logic | ✅ Custom resolvers | ✅ Custom resolvers | ✅ Write your own code | | Error Handling | ✅ Built-in | ✅ Built-in with validation | ❌ Manual |
📚 Quick Decision: See Mapper Quick Decision Guide (2-minute read)
📚 Complete Comparison: See Mapper Comparison Guide for an in-depth, code-verified comparison with a real-world inventory scenario, performance benchmarks, and decision matrix.
📬 XML/JSON Webhooks → GraphQL Mutations
Use Case: Transform external XML/JSON into Fluent GraphQL mutations (orders, products, etc.)
Example: SFCC XML Order → Fluent createOrder
import { GraphQLMutationMapper, XMLParserService, createClient } from '@fluentcommerce/fc-connect-sdk';
// 1. Parse incoming XML webhook
const xmlParser = new XMLParserService();
const orderData = await xmlParser.parse(xmlString);
// 2. Configure mapping (XML paths → GraphQL fields)
const mappingConfig = {
mutation: 'createOrder',
sourceFormat: 'xml',
returnFields: ['id', 'ref', 'status', 'totalPrice'], // Fields returned in response
arguments: {
input: {
ref: { source: 'order.@id' },
type: { value: 'HD' },
retailer: { source: 'order.retailer', resolver: 'sdk.parseInt' },
customer: {
fields: {
firstName: { source: 'order.customer.first-name' },
lastName: { source: 'order.customer.last-name' },
email: { source: 'order.customer.email' }
}
},
items: {
source: 'order.product-lineitems.product-lineitem',
fields: {
skuRef: { source: '@product-id' },
quantity: { source: 'quantity', resolver: 'sdk.parseInt' },
price: { source: 'price', resolver: 'sdk.parseFloat' }
}
}
}
}
};
// 3. Generate mutation automatically
const client = await createClient({ /* config */ });
const mapper = new GraphQLMutationMapper(mappingConfig, logger, { fluentClient: client });
const payload = await mapper.map(orderData); // Returns { query, variables }
// 4. Execute (mutation is auto-built!)
const result = await client.graphql(payload);
const createdOrder = result.data.createOrder;
console.log('✅ Order created:', {
id: createdOrder.id, // From returnFields
ref: createdOrder.ref, // From returnFields
status: createdOrder.status, // From returnFields
totalPrice: createdOrder.totalPrice // From returnFields
});What GraphQLMutationMapper Does For You:
- ✅ Parses complex XML/JSON structures
- ✅ Validates against GraphQL schema
- ✅ Builds the GraphQL mutation query automatically
- ✅ Handles nested objects and arrays
- ✅ Provides detailed error messages
vs Manual Approach:
// ❌ Without GraphQLMutationMapper (you write everything)
const mutation = `
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
id ref type
customer { firstName lastName email }
items { skuRef quantity price }
}
}
`;
const variables = {
input: {
ref: orderData.order['@id'],
type: 'HD',
retailer: parseInt(orderData.order.retailer),
customer: {
firstName: orderData.order.customer['first-name'],
lastName: orderData.order.customer['last-name'],
email: orderData.order.customer.email
},
items: orderData.order['product-lineitems']['product-lineitem'].map(item => ({
skuRef: item['@product-id'],
quantity: parseInt(item.quantity),
price: parseFloat(item.price)
}))
}
};
await client.graphql({ query: mutation, variables });📚 Complete Guide: docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/ - 12 comprehensive modules
📋 Complete Workflow Examples
Real-world integration patterns showing the full data pipeline.
Workflow Pattern: Data → Parse → Map → Batch API
External Storage (S3/SFTP)
↓
Download File
↓
Parse (CSV/XML/JSON/Parquet)
↓
Map Fields (UniversalMapper)
↓
Send to Batch API
↓
Archive/Cleanup📚 Learn More: See docs/01-TEMPLATES/ for 23+ production-ready templates
Example 1: S3 CSV → Batch API (Daily Inventory Sync)
Complete workflow reading CSV from S3, mapping fields, and sending to Fluent.
import {
createClient,
S3DataSource,
CSVParserService,
UniversalMapper,
} from '@fluentcommerce/fc-connect-sdk';
// ============= SETUP =============
// Create authenticated client (auto-detects Node.js/Deno/Versori)
const client = await createClient({
config: {
baseUrl: 'https://api.fluentcommerce.com',
clientId: process.env.FLUENT_CLIENT_ID!,
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
username: process.env.FLUENT_USERNAME!,
password: process.env.FLUENT_PASSWORD!,
retailerId: process.env.FLUENT_RETAILER_ID!,
},
});
// Configure S3 data source
const s3 = new S3DataSource(
{
type: 'S3_CSV',
connectionId: 'daily-inventory',
name: 'Daily Inventory',
s3Config: {
bucket: process.env.S3_BUCKET!,
region: process.env.S3_REGION ?? 'us-east-1',
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
},
console // logger
);
// Setup CSV parser and field mapper
const parser = new CSVParserService();
const mapper = new UniversalMapper({
fields: {
skuRef: { source: 'sku_id', required: true },
locationRef: { source: 'location_code', required: true },
qty: { source: 'quantity', resolver: 'sdk.parseInt' },
type: { source: 'type', defaultValue: 'DELTA' },
status: { source: 'status', defaultValue: 'AVAILABLE' },
},
});
// ============= WORKFLOW =============
// 1. Read file from S3
const [file] = await s3.listFiles({ prefix: 'inventory/' });
const csv = await s3.downloadFile(file.path, { encoding: 'utf8' });
// 2. Parse CSV and map fields
const rows = await parser.parse(csv, { columns: true, trim: true });
const payload = [];
for (const row of rows) {
const mapped = await mapper.map(row);
if (mapped.success) {
// Each entity must include retailerId
payload.push({
...mapped.data,
retailerId: process.env.FLUENT_RETAILER_ID!,
});
}
}
// 3. Send to Fluent Commerce Batch API
const job = await client.createJob({
name: 'inventory-sync',
retailerId: process.env.FLUENT_RETAILER_ID!,
});
const batch = await client.sendBatch(job.id, {
entityType: 'INVENTORY',
action: 'UPSERT',
source: 'S3_CSV', // Optional: tracks data source
event: 'InventoryQuantityUpdate', // Optional: targets Rubix workflow event
entities: payload,
});
// 4. Archive processed file
await s3.moveFile(file.path, `inventory/archive/${file.name}`);
console.log(`✅ Synced ${payload.length} inventory records`);
console.log(`📊 Job ID: ${job.id}, Batch ID: ${batch.id}`);📚 Learn More:
- S3 Operations:
docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md - CSV Parsing:
docs/02-CORE-GUIDES/parsers/modules/02-CORE-GUIDES-parsers-02-csv-parser.md - Universal Mapping:
docs/02-CORE-GUIDES/mapping/modules/ - Batch API:
docs/02-CORE-GUIDES/ingestion/modules/02-CORE-GUIDES-ingestion-06-batch-api.md
Example 2: SFTP XML → Batch API (EDI Integration)
Reading XML from SFTP server, parsing with XPath, and batch processing.
import {
createClient,
SftpDataSource,
XMLParserService,
UniversalMapper,
} from '@fluentcommerce/fc-connect-sdk';
// Setup SFTP connection
const sftp = new SftpDataSource(
{
type: 'SFTP_XML',
connectionId: 'edi-sftp',
name: 'EDI SFTP',
sftpConfig: {
settings: {
host: process.env.SFTP_HOST!,
port: 22,
username: process.env.SFTP_USERNAME!,
password: process.env.SFTP_PASSWORD!,
},
},
},
console
);
// Setup XML parser with path resolution
const parser = new XMLParserService();
const mapper = new UniversalMapper({
fields: {
skuRef: { source: 'Item.SKU', required: true },
locationRef: { source: 'Item.Location', required: true },
qty: { source: 'Item.Quantity', resolver: 'sdk.parseInt' },
type: { defaultValue: 'DELTA' },
status: { defaultValue: 'AVAILABLE' },
},
});
// Create client
const client = await createClient({
config: {
/* OAuth2 config */
},
});
// ============= WORKFLOW =============
// 1. List and download XML from SFTP
const files = await sftp.listFiles({ prefix: '/inbound/', pattern: '*.xml' });
for (const file of files) {
const xmlContent = await sftp.downloadFile(file.path, { encoding: 'utf8' });
// 2. Parse XML and extract items
const parsed = await parser.parse(xmlContent, {
arrayPath: 'InventoryUpdate.Items.Item', // Extract array of items
});
// 3. Map fields
const payload = [];
for (const item of parsed) {
const mapped = await mapper.map(item);
if (mapped.success) {
payload.push({
...mapped.data,
retailerId: process.env.FLUENT_RETAILER_ID!,
});
}
}
// 4. Send to Batch API
const job = await client.createJob({
name: `sftp-${file.name}`,
retailerId: process.env.FLUENT_RETAILER_ID!,
});
await client.sendBatch(job.id, {
entityType: 'INVENTORY',
action: 'UPSERT',
source: 'SFTP_XML',
entities: payload,
});
// 5. Archive processed file
await sftp.moveFile(file.path, `/archive/${file.name}`);
console.log(`✅ Processed ${file.name}: ${payload.length} records`);
}📚 Learn More:
- SFTP Operations:
docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md - XML Parsing:
docs/02-CORE-GUIDES/parsers/modules/02-CORE-GUIDES-parsers-04-xml-parser.md - Complete Templates:
docs/01-TEMPLATES/standalone/anddocs/01-TEMPLATES/versori/
Note: Batch API supports inventory + UPSERT only. Use GraphQL mutations for products, orders, etc.
SDK Architecture
The SDK follows a layered, service-oriented architecture with comprehensive service orchestration:
┌─────────────────────────────────────────────────────────────────────┐
│ 🔌 CLIENT LAYER │
├─────────────────────────────────────────────────────────────────────┤
│ createClient Factory (Auto-detects Runtime) │
│ ├─→ FluentClient (OAuth2) │
│ └─→ FluentVersoriClient (Platform Auth) │
└────────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────────┴────────────────────────────────────────┐
│ ⚙️ SERVICE LAYER │
├─────────────────────────────────────────────────────────────────────┤
│ Core Services: │
│ • UniversalMapper - Field transformations │
│ • GraphQLMutationMapper - XML/JSON → GraphQL │
│ • ExtractionOrchestrator - GraphQL extraction workflows │
│ • FluentBatchManager - Batch operations + BPP │
│ │
│ Support Services: │
│ • StateService - KV storage management │
│ • WebhookValidationService - RSA signature validation │
│ • WebhookAuthService - API key auth │
│ • PreflightValidator - Configuration validation │
│ • JobTracker - Lifecycle management │
│ • PartialBatchRecovery - Failure handling │
└────────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────────┴────────────────────────────────────────┐
│ 📁 DATA LAYER │
├─────────────────────────────────────────────────────────────────────┤
│ Data Sources: │
│ • S3DataSource - Presigned URLs + streaming │
│ • SftpDataSource - Connection pool + wait queue │
│ │
│ Parsers: │
│ • CSVParser - Streaming CSV processing │
│ • XMLParser - Path resolution + streaming │
│ • JSONParser - Streaming JSON processing │
│ • ParquetParser - Columnar data format │
└────────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────────┴────────────────────────────────────────┐
│ 🌐 EXTERNAL SYSTEMS │
├─────────────────────────────────────────────────────────────────────┤
│ • Fluent Commerce API - GraphQL, Batch API, Event API │
│ • AWS S3 - Object storage │
│ • SFTP Servers - File transfer │
│ • KV Storage - Versori KV / Redis │
└─────────────────────────────────────────────────────────────────────┘
Data Flow: Read → Parse → Map → Your Logic → Send/Archive| Layer | Responsibilities | Key Services |
| -------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| 🔌 Client Layer | Runtime detection, authentication, API communication | createClient(), FluentClient, FluentVersoriClient |
| ⚙️ Service Layer | Business logic, state management, validation | UniversalMapper, GraphQLMutationMapper, ExtractionOrchestrator, StateService, JobTracker, WebhookValidationService, WebhookAuthService |
| 📁 Data Layer | File I/O, streaming parsers, constant memory footprint | S3DataSource (presigned URLs), SftpDataSource (pooled), CSV/XML/JSON/Parquet parsers |
| 🎯 Orchestration Layer | High-level workflows, composition of services | IngestionService, ExtractionOrchestrator |
| 🌐 External Systems | Fluent Commerce API, cloud storage, distributed state | GraphQL + Batch + Event APIs, S3, SFTP, KV storage |
💡 Design Principles:
- Stateless & Parallel-Safe: All services safe for
Promise.all(), no hidden state - Streaming-First: Constant memory footprint for large files
- Universal Compatibility: Single codebase across Node.js, Deno, Versori
- Smart Error Handling: 4xx fail fast, 5xx exponential backoff, job expiration auto-recreation. Consistent
statusCodefield across success and error responses
📚 Learn More: docs/04-REFERENCE/architecture/ - Complete architecture documentation
🚀 More Quick Examples
GraphQL Mutation (Create Product)
import { createClient } from '@fluentcommerce/fc-connect-sdk';
const client = await createClient({
config: {
/* OAuth2 */
},
});
// Create a variant product using GraphQL mutation
const result = await client.graphql({
query: `
mutation CreateVariantProduct($input: CreateVariantProductInput!) {
createVariantProduct(input: $input) {
id
ref
type
gtin
name
summary
}
}
`,
variables: {
input: {
ref: 'PROD-001',
type: 'VARIANT',
gtin: '01234567890128',
name: 'Sample Product',
summary: 'A sample variant product',
catalogue: {
ref: 'DEFAULT',
},
product: {
ref: 'BASE-PROD-001',
},
},
},
});
console.log('✅ Product created:', result.data.createVariantProduct);
// Output: { id: '123', ref: 'PROD-001', type: 'VARIANT', gtin: '01234567890128', name: 'Sample Product', summary: 'A sample variant product' }📚 Learn More: docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/ - XML/JSON to GraphQL mutations
GraphQL Query with Auto-Pagination
Recommended: Using ExtractionOrchestrator (All Templates Use This)
import { createClient, ExtractionOrchestrator } from '@fluentcommerce/fc-connect-sdk';
const client = await createClient({
config: {
/* OAuth2 */
},
});
const orchestrator = new ExtractionOrchestrator(client, console);
// Extract with auto-pagination + validation + statistics
const result = await orchestrator.extract({
query: `
query GetVirtualPositions($first: Int, $after: String) {
virtualPositions(first: $first, after: $after) {
edges {
node {
ref
quantity
productRef
locationLink { ref }
}
cursor
}
pageInfo {
hasNextPage
}
}
}
`,
resultPath: 'virtualPositions.edges.node', // Extract node objects directly
maxPages: 10,
});
console.log(
`✅ Extracted ${result.stats.totalRecords} positions across ${result.stats.totalPages} pages`
);
// Output: ✅ Extracted 847 positions across 9 pages
// result.data = [{ ref: 'VP001', quantity: 10, productRef: 'PROD-001', locationLink: { ref: 'DC01' } }, ...]
// result.stats = { totalPages: 9, totalRecords: 847, truncated: false }Handling Partial Responses (Errors with Data):
// When GraphQL returns errors but some data is available
const result = await orchestrator.extract({
query: ORDERS_QUERY,
resultPath: 'orders.edges.node',
errorHandling: 'partial', // Continue extraction even with errors
});
// Check for partial errors
if (result.stats.partialErrors) {
console.warn(`Extraction completed with ${result.stats.partialErrors.length} errors`);
console.log(`Still extracted ${result.data.length} records`);
// Errors are in result.stats.partialErrors
}
// Edge case: If GraphQL returns errors with null data, result.data will be []
// but errors are still available in result.stats.partialErrorsAlternative: Using FluentClient (For Custom Response Handling)
const result = await client.graphql({
query: `query GetVirtualPositions($first: Int, $after: String) { ... }`,
variables: { first: 100 },
pagination: { maxPages: 10 },
});
// Manual extraction from GraphQL structure
const nodes = result.data?.virtualPositions?.edges?.map(edge => edge.node) ?? [];
console.log(`✅ Fetched ${nodes.length} virtual positions`);When to Use Each Approach:
| Approach | Use When | Returns |
| -------------------------- | ------------------------------------------------------------- | ------------------------------- |
| ExtractionOrchestrator | Extracting data to files/systems (recommended for extraction) | Flattened data[] + stats |
| FluentClient.graphql() | Mutations, custom queries, need full GraphQL response | Full GraphQL response structure |
📚 Learn More:
- Auto-Pagination:
docs/02-CORE-GUIDES/auto-pagination/- Cursor-based pagination guide (7 modules) - Extraction:
docs/02-CORE-GUIDES/extraction/- Complete extraction guide
Event API (Upsert Product)
Real-world example sending product data via Event API (triggers Rubix workflows).
import { createClient } from '@fluentcommerce/fc-connect-sdk';
const client = await createClient({
config: {
/* OAuth2 */
},
});
// Send product upsert event
const result = await client.sendEvent({
name: 'UPSERT_PRODUCT',
entityType: 'PRODUCT_CATALOGUE',
entitySubtype: 'MASTER',
entityRef: 'PC:MASTER:2',
rootEntityType: 'PRODUCT_CATALOGUE',
rootEntityRef: 'PC:MASTER:2',
retailerId: '2',
attributes: {
ref: 'VAR_PRODUCT',
type: 'VARIANT',
status: 'ACTIVE',
gtin: '1234567',
name: 'Chaz Kangeroo Hoodie-XS-Orange',
summary: 'Comfortable hoodie in orange',
categoryRefs: ['STANDARD_CATEGORY'],
price: [
{
type: 'DEFAULT',
currency: 'USD',
value: 49.99,
},
],
taxType: {
country: 'US',
group: 'HD',
},
},
});
console.log('✅ Event sent successfully');
// Output: { id: 'evt_123', status: 'CREATED' }📚 Learn More:
- Event API Templates:
docs/01-TEMPLATES/versori/workflows/ingestion/event-api/(8 templates for S3/SFTP with CSV, XML, JSON, Parquet) - Batch API Guide:
docs/02-CORE-GUIDES/ingestion/modules/02-CORE-GUIDES-ingestion-06-batch-api.md
Event Log API (Search + Audit)
Query and retrieve events/audit logs from the Fluent Commerce REST Event API. Use these read-only methods for troubleshooting workflows, tracing event chains, monitoring orchestration, and auditing batch processing.
Methods:
getEvents(params)→GET /api/v4.1/event- Search/filter events with flexible query parametersgetEventById(eventId)→GET /api/v4.1/event/{eventId}- Get a single event by ID
Important: These methods query the Event Log (audit trail). They do NOT trigger workflows — use sendEvent() for that.
Basic Usage
import { createClient } from '@fluentcommerce/fc-connect-sdk';
const client = await createClient({
config: { baseUrl, clientId, clientSecret, username, password },
});
// 1) Search for failed orchestration audit events on orders
const logs = await client.getEvents({
'context.rootEntityType': 'ORDER',
eventType: 'ORCHESTRATION_AUDIT',
eventStatus: 'FAILED',
count: 100,
});
console.log(`Found ${logs.results.length} events (hasMore=${logs.hasMore})`);
for (const event of logs.results) {
console.log(` ${event.name} [${event.eventStatus}] on ${event.context?.entityType}:${event.context?.entityRef}`);
}
// 2) Get full details for a specific event by ID
if (logs.results[0]?.id) {
const event = await client.getEventById(logs.results[0].id);
console.log(`Event: ${event.name} - Status: ${event.eventStatus}`);
console.log(` Entity: ${event.context?.entityType}:${event.context?.entityRef}`);
console.log(` Root: ${event.context?.rootEntityType}:${event.context?.rootEntityRef}`);
console.log(` Generated: ${event.generatedOn} by ${event.generatedBy}`);
// Trace parent events (for debugging event chains)
if (event.context?.sourceEvents?.length) {
for (const parentId of event.context.sourceEvents) {
const parent = await client.getEventById(parentId);
console.log(` Parent: ${parent.name} (${parent.eventStatus})`);
}
}
}Response Shape
getEvents() returns FluentEventLogResponse:
{
start: 1, // Pagination offset
count: 1000, // Results in this page
hasMore: false, // More pages available?
results: [ // Array of FluentEventLogItem
{
id: "e2cc5040-...", // UUID
name: "BATCH_COMPLETE", // Event/ruleset name (can be null)
type: "ORCHESTRATION_AUDIT",
accountId: "MYACCOUNT",
retailerId: "5", // String in response (not number)
category: "BATCH",
context: {
sourceEvents: ["babc5f37-..."], // Parent event IDs for tracing
entityType: "BATCH",
entityId: "12",
entityRef: "12",
rootEntityType: "JOB",
rootEntityId: "13",
rootEntityRef: "13"
},
eventStatus: "COMPLETE",
attributes: null, // null OR array of { name, value, type }
source: null,
generatedBy: "Rubix User",
generatedOn: "2026-02-05T06:31:56.895+00:00"
}
]
}getEventById(id) returns a single FluentEventLogItem (same shape as each item in results).
Query Parameters
All parameters are optional. Context filters use dot-notation keys which require quotes in JavaScript/TypeScript:
// ⚠️ Dot-notation keys MUST be quoted (JavaScript syntax requirement)
await client.getEvents({
'context.rootEntityType': 'ORDER', // ✅ Quotes required (has dot)
'context.entityRef': 'ORD-123', // ✅ Quotes required (has dot)
eventType: 'ORCHESTRATION_AUDIT', // ✅ No quotes needed (simple key)
count: 100, // ✅ No quotes needed (simple key)
});| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
| context.rootEntityType | string | Root entity type | 'ORDER', 'JOB', 'LOCATION' |
| context.rootEntityId | string | Root entity ID | '12345' |
| context.rootEntityRef | string | Root entity reference | 'ORD-12345' |
| context.entityType | string | Sub-entity type | 'FULFILMENT', 'BATCH', 'ARTICLE' |
| context.entityId | string | Sub-entity ID | '67890' |
| context.entityRef | string | Sub-entity reference | 'FUL-67890' |
| eventType | string | Event type filter | 'ORCHESTRATION_AUDIT' |
| eventStatus | string | Status filter | 'FAILED', 'COMPLETE' |
| name | string | Event/ruleset name | 'BATCH_COMPLETE' |
| category | string | Category filter | 'ruleSet', 'ACTION' |
| from | string | Start date (UTC ISO 8601) | '2026-02-01T00:00:00.000Z' |
| to | string | End date (UTC ISO 8601) | '2026-02-15T23:59:59.999Z' |
| retailerId | string/number | Retailer filter (pass explicitly; no auto-fallback) | '5' |
| start | number | Pagination offset (default: 0) | 0 |
| count | number | Results per page (default: 100, max: 5000) | 1000 |
Valid Values Reference
| Parameter | Valid Values |
|-----------|-------------|
| eventType | ORCHESTRATION, ORCHESTRATION_AUDIT, API, INTEGRATION, SECURITY, GENERAL |
| eventStatus | PENDING, SCHEDULED, NO_MATCH, SUCCESS, FAILED, COMPLETE |
| category | snapshot, ruleSet, rule, ACTION, CUSTOM, exception, ORDER_WORKFLOW, BATCH |
| rootEntityType | ORDER, LOCATION, FULFILMENT_OPTIONS, PRODUCT_CATALOGUE, INVENTORY_CATALOGUE, VIRTUAL_CATALOGUE, CONTROL_GROUP, RETURN_ORDER, BILLING_ACCOUNT, JOB |
| entityType | ORDER, FULFILMENT, ARTICLE, CONSIGNMENT, LOCATION, WAVE, FULFILMENT_OPTIONS, FULFILMENT_PLAN, PRODUCT_CATALOGUE, CATEGORY, PRODUCT, INVENTORY_CATALOGUE, INVENTORY_POSITION, INVENTORY_QUANTITY, VIRTUAL_CATALOGUE, VIRTUAL_POSITION, CONTROL_GROUP, CONTROL, RETURN_ORDER, RETURN_FULFILMENT, BILLING_ACCOUNT, CREDIT_MEMO, BATCH |
Common Use Cases
// 1. Find failed events in the last 24 hours
const failedEvents = await client.getEvents({
eventStatus: 'FAILED',
from: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
to: new Date().toISOString(),
count: 1000,
});
// 2. Track batch job processing (all events for a specific job)
const batchEvents = await client.getEvents({
'context.rootEntityType': 'JOB',
'context.rootEntityId': '13',
eventType: 'ORCHESTRATION_AUDIT',
count: 500,
});
// 3. Paginate through large result sets
let start = 0;
const allEvents: any[] = [];
let hasMore = true;
while (hasMore) {
const page = await client.getEvents({
'context.rootEntityType': 'ORDER',
start,
count: 1000,
});
allEvents.push(...page.results);
hasMore = page.hasMore;
start += page.count;
}
console.log(`Total events collected: ${allEvents.length}`);
// 4. Extract performance timing from event attributes
const auditEvents = await client.getEvents({
eventType: 'ORCHESTRATION_AUDIT',
'context.rootEntityType': 'JOB',
count: 100,
});
for (const event of auditEvents.results) {
if (Array.isArray(event.attributes)) {
const startTimer = event.attributes.find(a => a.name === 'startTimer');
const stopTimer = event.attributes.find(a => a.name === 'stopTimer');
if (startTimer && stopTimer) {
const duration = Number(stopTimer.value) - Number(startTimer.value);
console.log(`${event.name}: ${duration}ms`);
}
}
}
// 5. Find events for a specific order by reference
const orderEvents = await client.getEvents({
'context.rootEntityType': 'ORDER',
'context.rootEntityRef': 'HD_12345',
eventType: 'ORCHESTRATION_AUDIT',
});When to Use Which Method
| Method | Use When | Returns | Example |
|--------|----------|---------|---------|
| sendEvent() | Trigger a workflow or business action | Event creation response | Product upsert, order cancel, location update |
| getEvents() | Search/filter historical events | { results[], hasMore, start, count } | Find failed events, audit batch processing |
| getEventById() | Get full details for one event by ID | Single event object | Drill into context, attributes, trace sourceEvents |
Limitations & Operational Notes
| Constraint | Value | Notes |
|------------|-------|-------|
| Max count per request | 5000 | Use pagination (start/count/hasMore) for larger result sets |
| Time range (from) | 4 months back max | API rejects queries older than ~4 months |
| Time range (to) | 1 month forward max | API rejects future dates beyond ~1 month |
| Default time window | Past 30 days | Applied when from is omitted |
| Date format | UTC ISO 8601 | YYYY-MM-DDTHH:mm:ss.SSSZ |
| retailerId | Pass explicitly | Unlike sendEvent(), no auto-fallback to client config — pass in params if needed |
| attributes field | Nullable | Can be null (not empty array) — always check before iterating |
| name field | Nullable | Can be null for some system events |
| GraphQL | Not available | Event queries use REST only — no GraphQL events root field exists |
Key points:
getEvents()andgetEventById()are read-only — they do NOT trigger workflows- Empty
results: []is valid (not an error) — your filter may simply match zero events - Always use bounded time ranges (
from/to) for predictable performance - Use
eventType: 'ORCHESTRATION_AUDIT'for workflow execution history - Use
context.sourceEventsarray on each event to trace parent event chains - The
attributesfield isnullfor most events; only rule/ruleset events populate it retailerIdin responses is a string (e.g.,"5") even though you can pass a number in params
📚 Detailed Guide: docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md
Error Classification
Automatically classify GraphQL errors for retry decisions and error handling.
import { classifyError, classifyErrors, type ErrorClassification } from '@fluentcommerce/fc-connect-sdk';
// Handle errors from GraphQL response
const result = await client.graphql({ query, variables });
if (result.errors) {
const classifications = classifyErrors(result.errors);
for (const classification of classifications) {
console.log({
code: classification.code,
severity: classification.severity, // 'client' | 'server' | 'transient'
retryable: classification.retryable, // boolean
action: classification.action // Recommended action
});
}
// Check if any errors are retryable
const shouldRetry = classifications.some(c => c.retryable);
if (shouldRetry) {
// Implement retry logic
}
}
// Classify a single error
const singleError = { message: 'Rate limited', extensions: { code: 'T001' } };
const classification = classifyError(singleError);
console.log(classification.retryable); // true (transient error)Error Code Prefixes:
- C (Client) - Bad request, validation errors → Not retryable
- S (Server) - Internal errors → May be retryable
- T (Transient) - Rate limits, timeouts → Retryable
📚 Learn More:
- Comprehensive Error Handling Guide:
docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md- Complete reference with examples for all APIs - Quick Reference:
docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md- Cheat sheet for error codes and retry strategies - API Reference:
docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md- Error classification utilities
CLI Tooling
All CLI commands are distributed with the package. Use via npx:
npx fc-connect <command> [options]| Command | Purpose |
| -------------------------------------- | ------------------------------------------ |
| npx fc-connect analyze-source-structure 🆕 | Analyze data files and generate mapping templates |
| npx fc-connect introspect-schema | Download the Fluent GraphQL schema |
| npx fc-connect generate-mutation-mapping | Build ingestion mapping templates |
| npx fc-connect generate-query-mapping | Build extraction mapping templates |
| npx fc-connect validate-schema | Validate mapping config against the schema |
| npx fc-connect analyze-coverage | Report required / optional field coverage |
🚀 Quick Start: Analyze Your Data
# Analyze XML/JSON/CSV/Parquet files
npx fc-connect analyze-source-structure --file ./your-data.xml
# Generate mapping template + documentation
npx fc-connect analyze-source-structure \
--file ./inventory.xml \
--output ./mapping.json \
--markdown ./docs/analysis.mdWhat you get:
- ✅ All available fields and paths
- ✅ Array detection and structure insights
- ✅ Smart resolver suggestions (parseFloat, trim, etc.)
- ✅ Ready-to-use mapping templates
- ✅ Shareable documentation
📚 Learn More: docs/00-START-HERE/CLI-ANALYZE-SOURCE-STRUCTURE-GUIDE.md - Complete user guide | docs/02-CORE-GUIDES/mapping/ - Field mapping guides | docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md - Complete CLI documentation
Authentication & Webhooks
OAuth2 with Automatic Token Management:
- ✅ Refresh token support (uses
grant_type=refresh_tokenwhen available) - ✅ Automatic 401 retry with exponential backoff (3 retries max)
- ✅ Thread-safe token refresh (prevents duplicate requests)
- ✅ 60-second expiry buffer (prevents mid-flight token expiry)
- ✅ Handles clock skew, server-side revocation, multi-process conflicts
Webhook Validation:
- Cryptographic signature validation with RSA (
SHA256withRSA,SHA1withRSA,SHA512withRSA)
// Authentication happens automatically - just create client
const client = createClient({ fetch, log, activation });
// Webhook validation with signature verification
const isValid = await client.validateWebhook(body, headers['fluent-signature'], rawBody);
if (!isValid) return new Response('Invalid signature', { status: 401 });Webhook Authentication Service:
- Secure API key generation (cryptographically random, 64-char hex)
- API key validation with SHA-256 hashing
- Universal compatibility - Works in Node.js, Deno, Versori, Cloudflare Workers, Vercel Edge, and more!
- Uses Web Crypto API (native to all modern runtimes)
retailerId Configuration
🆕 Latest: retailerId is now OPTIONAL at client creation!
When Do You Need It?
| API | Needs retailerId? |
|-----|-------------------|
| GraphQL queries/mutations | ❌ NO (pass in mutation input if needed) |
| Job API (createJob) | ✅ YES |
| Event API (sendEvent) | ✅ YES |
| Batch API (sendBatch) | ❌ NO (uses jobId) |
Three Ways to Provide It
// Option 1: In client config (standalone/Node.js)
const client = await createClient({
config: {
baseUrl: 'https://api.fluentcommerce.com',
clientId: '...',
clientSecret: '...',
retailerId: '1' // ← Optional
}
});
// Option 2: Set after client creation (Versori fn() workflows)
const client = await createClient({ data: {}, log, openKv });
client.setRetailerId('1'); // ← Only if using Job/Event APIs
// Option 3: Pass directly to methods (multi-tenant)
await client.createJob({ name: 'sync', retailerId: '1' });
await client.sendEvent({ name: 'Test', entityType: 'PRODUCT', retailerId: '2' });📖 Complete Guide: docs/00-START-HERE/RETAILERID-CONFIGURATION.md
import { WebhookAuthService } from '@fluentcommerce/fc-connect-sdk';
// Initialize service
const authService = new WebhookAuthService(logger);
// API Key: Generate
const apiKey = await authService.generateApiKeyWithPrefix('prod', {
userId: 'client1',
scopes: ['webhook'],
});
// API Key: Validate
const validation = await authService.validateApiKey(
apiKey,
async (key) => {
// Lookup API key hash in your storage
const hash = await authService.hashApiKey(key);
return await lookupApiKeyMetadata(hash);
}
);📚 Learn More:
docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md- API key security guidedocs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md- Connection-based securitydocs/02-CORE-GUIDES/webhook-validation/- Complete webhook security guide (5 modules)docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md- Authentication API reference
🚀 Standalone Usage (Outside Versori)
Use the SDK in your own TypeScript/Node.js applications - not just in Versori workflows.
Quick Example: Custom REST API Calls
import { createClient } from '@fluentcommerce/fc-connect-sdk';
// Create client with OAuth2 authentication
const client = await createClient({
config: {
baseUrl: 'https://api.fluentcommerce.com',
clientId: 'FLUENT_INTEGRATION',
clientSecret: 'your-client-secret',
username: 'your-username',
password: 'your-password',
retailerId: 'your-retailer-i