@flexops/sdk
v2.0.0
Published
Official FlexOps Platform Node.js/TypeScript SDK — multi-carrier shipping, order management, and logistics API
Maintainers
Readme
@flexops/sdk
Official Node.js/TypeScript SDK for the FlexOps multi-carrier shipping platform.
Features
- Full TypeScript support with complete type definitions
- All 4 carriers: USPS, UPS, FedEx, DHL
- High-level shipping operations (rate shopping, labels, tracking)
- Direct carrier-specific endpoint access (214 VisionSuite endpoints)
- Automatic retry with exponential backoff
- JWT and API key authentication
- Webhook signature verification (HMAC-SHA256)
- ESM and CommonJS support
- Zero external runtime dependencies
Installation
npm install @flexops/sdkQuick Start
API Key Authentication (server-to-server)
import { FlexOps } from '@flexops/sdk';
const client = new FlexOps({
apiKey: 'fxk_live_...',
workspaceId: 'your-workspace-id',
});Email/Password Authentication
const client = new FlexOps({
baseUrl: 'https://gateway.flexops.io',
});
await client.auth.login({
email: '[email protected]',
password: 'your-password',
});
// Token is automatically stored for subsequent requestsShipping Operations
Rate Shopping
const rateRequest = {
origin: { addressLine1: '123 Main St', city: 'New York', stateProvince: 'NY', postalCode: '10001' },
destination: { addressLine1: '456 Oak Ave', city: 'Los Angeles', stateProvince: 'CA', postalCode: '90210' },
package: { weight: 16, weightUnit: 'oz' },
};
// Get all available rates
const rates = await client.shipping.getRates(rateRequest);
// Get the single cheapest rate
const cheapest = await client.shipping.getCheapestRate(rateRequest);
// Get the single fastest rate
const fastest = await client.shipping.getFastestRate(rateRequest);Create a Shipping Label
const operation = await client.shipping.prepareLabel({
...rateRequest, carrierCode: 'USPS', serviceCode: 'PRIORITY',
}, 10.00, crypto.randomUUID());
if (operation.preview) console.log({ quote: operation.preview.quotedPostageAmount,
maximum: operation.preview.maximumPostageAmount, currency: operation.preview.currency,
expiresAt: operation.preview.expiresAt }); // omit the signed token from display/logs
// Keep operation securely with the original Gateway and API-key identity.
// In a terminal example, ask explicitly before any live purchase:
const { createInterface } = await import('node:readline/promises');
const prompt = createInterface({ input: process.stdin, output: process.stdout });
try {
const approved = operation.sandboxLabel || await prompt.question('Type approve to buy at the displayed maximum (later adjustments/fees excluded): ') === 'approve';
if (approved) {
const label = await client.shipping.purchaseLabel(operation);
console.log(label.trackingNumber);
}
} finally { prompt.close(); }Tracking
const tracking = await client.shipping.track('9400111899223456789012');
for (const event of tracking.data.events) {
console.log(`${event.timestamp}: ${event.description}`);
}Address Validation
const result = await client.shipping.validateAddress({
name: 'John Doe',
street1: '123 Main St',
city: 'New York',
state: 'NY',
zip: '10001',
country: 'US',
});
if (!result.data.isValid) {
console.log('Corrected:', result.data.correctedAddress);
}Batch Labels
// Preview (dry-run) before purchasing
const preview = await client.shipping.previewBatch({
items: [/* array of CreateLabelRequest */],
});
// Create the batch
const batch = await client.shipping.createBatch({
items: [/* array of CreateLabelRequest */],
});
// Check status
const status = await client.shipping.getBatchStatus(batch.data.jobId);Direct Carrier Access
For full control, use carrier-specific endpoints:
USPS
// Domestic label with full USPS payload
const label = await client.carriers.usps.createDomesticLabel({
imageType: 'PDF',
mailClass: 'PRIORITY_MAIL',
weightInOunces: 16,
// ... full USPS API payload
});
// Rate calculation
const rates = await client.carriers.usps.getDomesticRates({
originZIPCode: '10001',
destinationZIPCode: '90210',
weight: 1.0,
});
// Find drop-off locations
const locations = await client.carriers.usps.findDropOffLocations({
zipCode: '10001',
});UPS
const label = await client.carriers.ups.createLabel({ /* UPS payload */ });
const rates = await client.carriers.ups.getRates({ /* UPS payload */ });
const tracking = await client.carriers.ups.track({ trackingNumber: '1Z...' });
const transitTimes = await client.carriers.ups.getTransitTimes({ /* payload */ });FedEx
const shipment = await client.carriers.fedex.createShipment({ /* FedEx payload */ });
const rates = await client.carriers.fedex.getRates({ /* FedEx payload */ });
const tracking = await client.carriers.fedex.track({ /* payload */ });
// Multi-package (Open Ship)
await client.carriers.fedex.createOpenShipment({ /* payload */ });
await client.carriers.fedex.addPackagesToOpenShipment({ /* payload */ });
await client.carriers.fedex.confirmOpenShipment({ /* payload */ });DHL
const shipment = await client.carriers.dhl.createShipment({ /* DHL payload */ });
const rates = await client.carriers.dhl.getRates({ countryCode: 'US', weight: '1' });
const tracking = await client.carriers.dhl.track({ shipmentTrackingNumber: '123456' });
const landedCost = await client.carriers.dhl.calculateLandedCost({ /* payload */ });Workspace Management
// List workspaces
const workspaces = await client.workspaces.list();
// Switch workspace
client.workspaceId = 'different-workspace-id';
// Invite a member
await client.workspaces.inviteMember('[email protected]', 'Member');Webhooks
// Create a subscription
const webhook = await client.webhooks.create({
url: 'https://your-app.com/webhooks/flexops',
events: ['label.created', 'tracking.updated', 'shipment.delivered'],
});
// Rotate the signing secret
await client.webhooks.rotateSecret(webhook.data.id);
// Verify incoming webhook signatures in your handler
import { WebhooksResource } from '@flexops/sdk';
app.post('/webhooks/flexops', (req, res) => {
const signature = req.headers['x-flexops-signature'];
const isValid = WebhooksResource.verifySignature(
JSON.stringify(req.body),
signature,
process.env.FLEXOPS_WEBHOOK_SECRET,
);
if (!isValid) return res.status(401).send('Invalid signature');
// Process webhook...
});Curl Quickstart
Every SDK method is a thin wrapper around the FlexOps REST API. If you want to verify the API before committing to the SDK — or you're integrating from a language we don't ship a SDK for — these curl invocations hit the same endpoints:
# Shop rates across all connected carriers
curl -X POST https://gateway.flexops.io/api/shipping/rates \
-H "X-API-Key: fxk_live_..." \
-H "Content-Type: application/json" \
-d '{
"origin": {"addressLine1": "123 Main St", "city": "New York", "stateProvince": "NY", "postalCode": "10001", "countryCode": "US"},
"destination": {"addressLine1": "456 Oak Ave", "city": "Los Angeles", "stateProvince": "CA", "postalCode": "90210", "countryCode": "US"},
"package": {"weight": 16, "weightUnit": "oz"}
}'
# Create a label
curl -X POST https://gateway.flexops.io/api/workspaces/ws_abc123/shipping/labels \
-H "X-API-Key: fxk_live_..." \
-H "Content-Type: application/json" \
-d '{
"carrier": "USPS",
"service": "PRIORITY_MAIL",
"fromAddress": {"name": "Warehouse", "street1": "123 Main St", "city": "New York", "state": "NY", "zip": "10001", "country": "US"},
"toAddress": {"name": "Customer", "street1": "456 Oak Ave", "city": "Los Angeles", "state": "CA", "zip": "90210", "country": "US"},
"parcel": {"weight": 16, "weightUnit": "oz"}
}'
# Track a shipment
curl https://gateway.flexops.io/api/workspaces/ws_abc123/shipping/track/9400111899223456789012 \
-H "X-API-Key: fxk_live_..."
# Cancel a label (via the unified carrier-agnostic endpoint)
curl -X DELETE https://gateway.flexops.io/api/v3.0/shipping/Usps/cancel/9400111899223456789012 \
-H "X-API-Key: fxk_live_..."Use an fxk_test_... key instead of fxk_live_... to hit the sandbox environment; mock carriers respond, no real charges, no real labels.
Wallet & Billing
const balance = await client.wallet.getBalance();
console.log(`Balance: $${balance.data.balance}`);
// Add funds
await client.wallet.addFunds(100.00);
// Configure auto-reload
await client.wallet.configureAutoReload({
enabled: true,
threshold: 25.00,
amount: 100.00,
});Insurance
// Check available providers
const providers = await client.insurance.getProviders();
// Returns ["FlexOps"] or ["FlexOps", "U-PIC"]
// Get a quote
const quote = await client.insurance.getQuote({
carrier: 'USPS',
declaredValue: 500.00,
provider: 'FlexOps',
});
// Purchase insurance
const policy = await client.insurance.purchase({
trackingNumber: '9400111899223456789012',
carrier: 'USPS',
declaredValue: 500.00,
});Returns & RMA
// Create a return authorization
const rma = await client.returns.create({
orderId: 'ORD-12345',
reason: 'Defective product',
items: [{ sku: 'WIDGET-001', quantity: 1 }],
});
// Approve and generate return label
await client.returns.approve(rma.data.rmaId);
await client.returns.generateLabel(rma.data.rmaId);
// Mark items as received
await client.returns.markReceived(rma.data.rmaId, [
{ sku: 'WIDGET-001', quantity: 1, condition: 'damaged' },
]);Analytics
const trend = await client.analytics.shipmentsTrend({
startDate: '2026-01-01',
endDate: '2026-03-01',
period: 'week',
});
const carriers = await client.analytics.carrierSummary();
const returns = await client.analytics.returnsMetrics();
const performance = await client.analytics.deliveryPerformance();Automation Rules
// Create a shipping rule
await client.rules.create({
name: 'Route light packages via USPS',
isActive: true,
priority: 1,
conditions: [
{ field: 'Weight', operator: 'LessThan', value: '16' },
{ field: 'DestinationCountry', operator: 'Equals', value: 'US' },
],
actions: [
{ type: 'SetCarrier', value: 'USPS' },
{ type: 'SetService', value: 'GROUND_ADVANTAGE' },
],
});Error Handling
import { FlexOpsError, FlexOpsAuthError, FlexOpsRateLimitError } from '@flexops/sdk';
try {
await client.shipping.createLabel({ /* ... */ });
} catch (error) {
if (error instanceof FlexOpsRateLimitError) {
console.log(`Rate limited. Retry after ${error.retryAfter}s`);
} else if (error instanceof FlexOpsAuthError) {
console.log('Re-authenticate and try again');
} else if (error instanceof FlexOpsError) {
console.log(`API error ${error.status}: ${error.message}`);
console.log('Validation errors:', error.errors);
}
}Configuration
const client = new FlexOps({
baseUrl: 'https://gateway.flexops.io', // Default
apiKey: 'fxk_live_...',
workspaceId: 'ws-abc123',
timeout: 30000, // 30s default
retry: {
maxRetries: 3, // Default
baseDelay: 1000, // 1s, exponential backoff
retryableStatusCodes: [429, 500, 502, 503, 504],
},
headers: {
'X-Custom-Header': 'value', // Added to every request
},
});OpenAPI Spec Generation
The SDK includes tooling to generate TypeScript types from the live API spec:
# 1. Start the FlexOps platform (Aspire orchestrator)
dotnet run --project FlexOps.Platform.AppHost
# 2. Export the Gateway API spec
./scripts/export-gateway-spec.sh http://localhost:XXXX
# 3. Pre-process and merge all specs
npm run preprocess -- --gateway specs/raw/gateway-v1.json
# 4. Generate TypeScript client (optional — for full auto-generated client)
npm run generate
# 5. Build the SDK
npm run buildLicense
MIT © FlexOps, LLC. See LICENSE for full text.
Approval and retries
Use these methods only with Gateway's bounded approval contract; coordinate client/Gateway activation. Preparation does not automatically approve. The ceiling covers pre-dispatch postage, excluding later carrier adjustments and separate fees. Persist the operation securely if it must survive process restart. Retry the same request/key after response loss; OutcomeUnknown requires reconciliation, never a new purchase key. Completed replay remains possible after token expiry. Sandbox operations return a synthetic label without real postage. The legacy createLabel and USPS domestic wrapper reject preview responses; use shipping.prepareLabel/purchaseLabel for live purchases.
Guarded label release compatibility
This release requires a Gateway deployment with the bounded label preview/approval contract (Gateway PR #509 or later). Do not use its live label preparation against an older Gateway: older servers may purchase immediately. Upgrade the Gateway and affected callers together during a purchase maintenance window.
Live label creation now requires a USD maximum, preview, explicit approval, and replay of the saved request with its original idempotency key. Never retry an uncertain purchase with a new key. Sandbox results do not certify production postage.
