npm package: @fluentcommerce/fc-connect-sdk
Description: Fluent Commerce SDK - Deno & Node.js Compatible
Author: Fluent Commerce
Latest version: 0.1.55

# Fluent Commerce Connect SDK

[![npm version](https://img.shields.io/npm/v/@fluentcommerce/fc-connect-sdk.svg)](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
[![npm downloads](https://img.shields.io/npm/dm/@fluentcommerce/fc-connect-sdk.svg)](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.4+-blue)](https://www.typescriptlang.org/)
[![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18.0.0-green)](https://nodejs.org/)
[![Deno Compatible](https://img.shields.io/badge/Deno-Compatible-black)](https://deno.land/)

TypeScript SDK for building **Fluent Commerce** integrations across Node.js, Deno, and the Versori platform.

- **Package:** [`@fluentcommerce/fc-connect-sdk`](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
- **Documentation:** Complete markdown guides included in the package

---

## Table of Contents

- [Overview](#overview) - What is this SDK?
- [Features](#feature-highlights) - What can it do?
- [**Which API Should I Use?**](#-which-api-should-i-use) - **Batch vs Event vs GraphQL** 🆕
- [Install](#install) - Get started
- [Quick Start](#-5-minute-quickstart) - 5-minute example
- [What's Next?](#-whats-next) - Templates, guides, next steps
- [Universal Mapping](#️-universal-mapping) - Field transformations
- [**Mapping Approaches**](#-mapping-approaches-which-one-do-i-use) - **Which mapper to use?** 🆕
  - [Mapper Comparison Guide](docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md) - Real-world scenario comparison 🆕
- [XML/JSON → GraphQL](#-xmljson-webhooks-graphql-mutations) - Auto-generate mutations 🆕
- [Complete Workflow Examples](#-complete-workflow-examples) - Full integration patterns
- [Architecture](#sdk-architecture) - How it works
- [More Examples](#-more-quick-examples) - Common patterns
- [Core Services](#core-services) - API reference
- [CLI Tools](#cli-tooling) - Command-line utilities
- [Standalone Usage](#-standalone-usage-outside-versori) - Node.js/Deno app integration
- [CLI Profile Integration](#fluent-cli-profile-integration-no-hardcoded-credentials) - Reuse Fluent CLI profiles
- [Authentication & Webhooks](#authentication-webhooks) - Security
- [Security & Compliance](#security--compliance) - Security audits, SOC 2, vulnerability management 🆕
- [Common Pitfalls](#common-pitfalls-and-fixes) - Troubleshooting
- [Support](#additional-resources) - 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](#-mapping-approaches-which-one-do-i-use)
> - **Which API to use?** → [API Decision Guide](#-which-api-should-i-use) (Batch vs Event vs GraphQL)
> - **Want a ready-made solution?** → [23+ Production Templates](docs/01-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](#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()` and `client.sendBatch()` for batch processing. `FluentBatchManager` available for Versori workflows (see `docs/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 `statusCode` field 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-ctr` and `aes128-cbc` ciphers (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<br/>✅ Internal dashboards with data export<br/>✅ Complex data transformations | ⚠️ Simple CRUD operations<br/>⚠️ Real-time UI updates<br/>⚠️ Mobile apps (bundle size) |
| **Event Streaming**    | ✅ Webhook-based event processing<br/>✅ Scheduled batch reactions to events                                               | ⚠️ Real-time streaming<br/>⚠️ Sub-second latency requirements                          |
| **Simple Scripts**     | ✅ Need parsing (CSV/XML/JSON)<br/>✅ Need field mapping/transformation<br/>✅ Need retry/error handling                   | ⚠️ Single GraphQL query<br/>⚠️ No transformation needed<br/>⚠️ 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

```bash
# npm (recommended)
npm install @fluentcommerce/fc-connect-sdk

# yarn
yarn add @fluentcommerce/fc-connect-sdk

# pnpm
pnpm add @fluentcommerce/fc-connect-sdk
```

Deno:

```ts
import { createClient } from 'npm:@fluentcommerce/fc-connect-sdk';
```

### Optional TypeScript Settings

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "lib": ["ES2022"],
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "strict": true
  }
}
```

### Verify Installation

Quick test to confirm SDK is working:

```typescript
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)!

---

## 🏃 5-Minute Quickstart

**Goal:** Send your first batch of inventory to Fluent Commerce.

### Step 1: Install

```bash
npm install @fluentcommerce/fc-connect-sdk
```

### Step 2: Configure Authentication

Create `.fluentrc.json`:

```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

```typescript
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-schema` to 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/` (see `graphql-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/` or `docs/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](#sdk-architecture)
- **Complete Workflow Examples** - [Jump to Examples](#-complete-workflow-examples)
- **Mapper Comparison** - [Which mapper to use?](#-mapping-approaches-which-one-do-i-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:

```typescript
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

```typescript
// 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):**
```javascript
{
  items: {
    edges: [
      { node: { ref: "FFI-001", quantity: 10 } },
      { node: { ref: "FFI-002", quantity: 5 } }
    ]
  }
}
```

**Pattern 1: Wildcard syntax (recommended)**
```json
{
  "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**
```json
{
  "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:
```json
{
  "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):**
```xml
<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](docs/00-START-HERE/mapper-quick-decision-guide.md) (2-minute read)  
**📚 Complete Comparison:** See [Mapper Comparison Guide](docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md) 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

```typescript
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:**
1. ✅ Parses complex XML/JSON structures
2. ✅ Validates against GraphQL schema
3. ✅ Builds the GraphQL mutation query automatically
4. ✅ Handles nested objects and arrays
5. ✅ Provides detailed error messages

**vs Manual Approach:**
```typescript
// ❌ 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.

```typescript
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.

```typescript
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/` and `docs/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 `statusCode` field across success and error responses

**📚 Learn More:** `docs/04-REFERENCE/architecture/` - Complete architecture documentation

---

## 🚀 More Quick Examples

### GraphQL Mutation (Create Product)

```typescript
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)**

```typescript
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):**

```typescript
// 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.partialErrors
```

**Alternative: Using FluentClient (For Custom Response Handling)**

```typescript
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).

```typescript
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 parameters
- `getEventById(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

```typescript
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`:

```typescript
{
  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:

```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

```typescript
// 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()` and `getEventById()` 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.sourceEvents` array on each event to trace parent event chains
- The `attributes` field is `null` for most events; only rule/ruleset events populate it
- `retailerId` in 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.

```typescript
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`:

```bash
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

```bash
# 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.md
```

**What 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_token` when 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`)

```ts
// 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

```typescript
// 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`](docs/00-START-HERE/RETAILERID-CONFIGURATION.md)

```ts
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 guide**
- `docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md` - **Connection-based security**
- `docs/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

```typescript
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