WMS Setup Guide
Complete guide for integrating ShipStream Data Border into your WMS
WMS Setup Guide
This guide walks through integrating ShipStream Data Border into your Warehouse Management System, from initial setup to production operation.
Prerequisites
Before starting, ensure you have:
- Access to a Data Border instance (self-hosted or managed)
- A JWT with
create-tenantpermission from your Data Border administrator - HTTPS endpoints for OAuth callbacks
- Secure storage for tokens and secrets
Architecture Overview
Your WMS integration follows this pattern:
flowchart LR
subgraph YourWMS["Your WMS"]
A[Order Management]
B[Token Manager]
C[Label Generation]
D[Print Queue]
end
subgraph ADB["ShipStream Data Border"]
E[Auth APIs]
F[Passthrough API]
G[Label Proxy]
H[Print API]
end
subgraph External["External Services"]
I[Amazon SP-API]
J[Carriers]
K[Device Hub]
end
B --> E
A --> F
C --> G
D --> H
E --> I
F --> I
G --> I
G --> J
H --> K
Integration Checklist
Use this checklist to track your integration progress:
Recommended Order: Complete each section before moving to the next.
Phase 1: Authentication Setup
- Register your WMS as a tenant
- Implement Data Border access token refresh
- Build the seller OAuth flow
- Implement seller access token refresh
- Test token lifecycle
Phase 2: Order Sync
- Connect passthrough API for orders
- Handle order data (with scrubbed addresses)
- Set up order change notifications (optional)
Phase 3: Label Generation
- Integrate label proxy with your carrier
- Map PII placeholders to your data model
- Handle label response and document storage
Phase 4: Printing
- Configure Device Hub connection
- Implement print job submission
- Handle reprint detection
Phase 5: Production Readiness
- Implement error handling
- Add monitoring and alerting
- Document operational procedures
- Security review
Implementation Details
1. Token Manager Module
Create a centralized token manager to handle all authentication:
// Example Token Manager structure
class ADBTokenManager {
constructor(config) {
this.adbBaseUrl = config.adbBaseUrl
this.tenantId = config.tenantId
this.tenantRefreshToken = config.tenantRefreshToken
this.jwt = config.jwt // For tenant management only
this.adbAccessToken = null
this.adbTokenExpiry = null
this.sellerTokens = new Map() // sellerId -> {token, expiry, refreshToken, secret}
}
// Get valid ADB access token
async getAdbAccessToken() {
if (this.adbAccessToken && Date.now() < this.adbTokenExpiry - 7 * 24 * 60 * 60 * 1000) {
return this.adbAccessToken
}
const response = await fetch(`${this.adbBaseUrl}/api/get-adb-access-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.jwt}`
},
body: JSON.stringify({
tenant_id: this.tenantId,
refresh_token: this.tenantRefreshToken
})
})
const data = await response.json()
this.adbAccessToken = data.data.access_token
this.adbTokenExpiry = Date.now() + 30 * 24 * 60 * 60 * 1000 // 30 days
return this.adbAccessToken
}
// Get valid seller access token
async getSellerAccessToken(sellerId) {
const seller = this.sellerTokens.get(sellerId)
if (seller?.token && Date.now() < seller.expiry - 60 * 60 * 1000) {
return { token: seller.token, secret: seller.secret }
}
const adbToken = await this.getAdbAccessToken()
const response = await fetch(`${this.adbBaseUrl}/api/get-seller-access-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-adb-access-token': adbToken
},
body: JSON.stringify({
seller_id: sellerId,
refresh_token: seller.refreshToken
})
})
const data = await response.json()
seller.token = data.data.access_token
seller.expiry = Date.now() + 24 * 60 * 60 * 1000 // 24 hours
return { token: seller.token, secret: seller.secret }
}
// Store new seller credentials after OAuth
storeSeller(sellerId, refreshToken, amazonTokenSecret) {
this.sellerTokens.set(sellerId, {
refreshToken,
secret: amazonTokenSecret,
token: null,
expiry: 0
})
// Also persist to database
}
}
2. Seller OAuth Flow
Implement the OAuth connection flow:
// Express.js example
const crypto = require('crypto')
// Step 1: Start OAuth - redirect seller to this endpoint
app.get('/connect-amazon', async (req, res) => {
const { internalSellerId } = req.query
// Generate secret for this seller
const amazonTokenSecret = crypto.randomBytes(32).toString('base64')
// Store temporarily (use a cache or session)
await cache.set(`oauth:${internalSellerId}`, {
amazonTokenSecret,
startedAt: Date.now()
}, { ttl: 600 }) // 10 minute TTL
// Build OAuth URL
const oauthUrl = new URL(`${ADB_BASE_URL}/auth/initialize`)
oauthUrl.searchParams.set('state', internalSellerId)
oauthUrl.searchParams.set('marketplace_region', 'us-east-1')
oauthUrl.searchParams.set('amazonTokenSecret', amazonTokenSecret)
oauthUrl.searchParams.set('sellerRedirectUrl', `${MY_BASE_URL}/oauth/callback`)
res.redirect(oauthUrl.toString())
})
// Step 2: Handle callback
app.get('/oauth/callback', async (req, res) => {
const { state: internalSellerId, seller_id, code } = req.query
// Retrieve the stored secret
const oauthData = await cache.get(`oauth:${internalSellerId}`)
if (!oauthData) {
return res.status(400).send('OAuth session expired')
}
// Claim the authorization
const adbToken = await tokenManager.getAdbAccessToken()
const response = await fetch(`${ADB_BASE_URL}/api/claim-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-adb-access-token': adbToken
},
body: JSON.stringify({
seller_id,
name: `Seller ${internalSellerId}`,
callback_url: `${MY_BASE_URL}/webhooks/amazon`,
code
})
})
const data = await response.json()
// Store the seller credentials
tokenManager.storeSeller(seller_id, data.data.refresh_token, oauthData.amazonTokenSecret)
// Also save to database
await db.sellers.create({
internalId: internalSellerId,
adbSellerId: seller_id,
refreshToken: encrypt(data.data.refresh_token),
amazonTokenSecret: encrypt(oauthData.amazonTokenSecret)
})
// Clean up
await cache.del(`oauth:${internalSellerId}`)
res.redirect('/sellers?connected=true')
})
3. Order Synchronization
Fetch orders through the passthrough API:
async function syncOrders(sellerId, marketplaceId) {
const { token, secret } = await tokenManager.getSellerAccessToken(sellerId)
const response = await fetch(
`${ADB_BASE_URL}/passthrough-api/orders/v0/orders?MarketplaceIds=${marketplaceId}&CreatedAfter=${lastSync}`,
{
headers: {
'x-seller-access-token': token,
'x-amazon-token-secret': secret
}
}
)
const data = await response.json()
// Note: ShippingAddress is automatically scrubbed
for (const order of data.payload.Orders) {
await db.orders.upsert({
amazonOrderId: order.AmazonOrderId,
sellerId,
status: order.OrderStatus,
orderTotal: order.OrderTotal,
// Address fields will be empty/scrubbed
})
}
}
4. Label Generation with Label Proxy
Generate shipping labels using the label proxy:
async function createShippingLabel(order, parcel) {
const { token, secret } = await tokenManager.getSellerAccessToken(order.sellerId)
// Build request with PII placeholders
const labelRequest = {
shipment: {
to_address: {
name: '{{ship_to_name}}',
street1: '{{ship_to_address1}}',
street2: '{{ship_to_address2}}',
city: '{{ship_to_city}}',
state: '{{ship_to_state}}',
zip: '{{ship_to_zip}}',
country: '{{ship_to_country}}',
phone: '{{ship_to_phone}}'
},
from_address: {
name: 'Your Warehouse',
street1: '123 Warehouse Ave',
city: 'Commerce City',
state: 'CO',
zip: '80022',
country: 'US'
},
parcel: {
length: parcel.length,
width: parcel.width,
height: parcel.height,
weight: parcel.weight
}
}
}
const response = await fetch(`${ADB_BASE_URL}/api/label-proxy/forward`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-seller-access-token': token,
'x-amazon-token-secret': secret,
'x-original-url': 'https://api.easypost.com/v2/shipments',
'x-amazon-order-id': order.amazonOrderId,
'x-unique-shipment-id': `${order.id}-${Date.now()}`,
'Authorization': `Bearer ${EASYPOST_API_KEY}`
},
body: JSON.stringify(labelRequest)
})
const data = await response.json()
// Store the scrubbed result and document references
await db.shipments.create({
orderId: order.id,
adbShipmentId: data.data.shipment_id,
trackingNumber: data.data.scrubbed_response.tracking_code,
documents: data.data.documents // Array of {uuid, path}
})
return data.data
}
5. Print Job Submission
Send labels to Device Hub for printing:
async function printLabel(shipment, printerId, printerType = 'label') {
const { token, secret } = await tokenManager.getSellerAccessToken(shipment.sellerId)
const response = await fetch(`${ADB_BASE_URL}/api/print/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-seller-access-token': token,
'x-amazon-token-secret': secret
},
body: JSON.stringify({
shipment_id: shipment.adbShipmentId,
printer_type: printerType,
printer_id: printerId,
label_uuids: shipment.documents.map(d => d.uuid)
})
})
const data = await response.json()
if (data.data.is_reprint) {
// Log or alert on reprint
console.warn(`Reprint detected for shipment ${shipment.id}. First print: ${data.data.warning}`)
}
return data.data
}
Error Handling
Implement robust error handling for all Data Border calls:
class ADBError extends Error {
constructor(response, body) {
super(body.error?.message || 'Unknown ADB error')
this.status = response.status
this.details = body.error?.details
this.isAdbError = body.is_adb_error
}
}
async function callADB(url, options) {
const response = await fetch(url, options)
const body = await response.json()
if (!response.ok || body.success === false) {
const error = new ADBError(response, body)
// Handle specific errors
switch (response.status) {
case 401:
// Token expired - trigger refresh
throw new TokenExpiredError(error.message)
case 429:
// Rate limited - implement backoff
throw new RateLimitError(error.message)
case 403:
// Access denied - check permissions or PII blocking
throw new AccessDeniedError(error.message)
default:
throw error
}
}
return body
}
Monitoring Recommendations
Key Metrics to Track
| Metric | Alert Threshold | Action |
|---|---|---|
| Token refresh failures | Any | Check connectivity, credentials |
| API error rate | > 5% | Investigate error types |
| Label generation time | > 10s | Check carrier API health |
| Reprint rate | > 10% | Review warehouse processes |
| PII access rejections | Unusual patterns | Security review |
Logging Best Practices
// Log structure for ADB operations
logger.info({
type: 'adb_operation',
operation: 'label_proxy',
sellerId: order.sellerId,
orderId: order.amazonOrderId,
duration: endTime - startTime,
success: true,
shipmentId: result.shipment_id
})
// Never log tokens or secrets!
// Bad: logger.info({ token: sellerToken })
// Good: logger.info({ hasToken: !!sellerToken })
Testing
Sandbox Environment
For testing, use the sandbox seller creation endpoint:
curl -X POST https://adb.example.com/api/create-sandbox-seller-with-tokens \
-H "Content-Type: application/json" \
-H "x-adb-access-token: YOUR_ADB_TOKEN" \
-d '{
"seller_name": "Test Seller",
"marketplace_region": "us-east-1",
"amazonTokenSecret": "YOUR_BASE64_SECRET",
"sandbox_refresh_token": "YOUR_SANDBOX_REFRESH_TOKEN"
}'
This endpoint is only available when Data Border is running in sandbox mode (IS_SANDBOX=yes).
Integration Tests
Test each integration point:
- Token lifecycle - Verify refresh works before expiry
- OAuth flow - Complete flow with test seller
- Order sync - Verify address scrubbing works
- Label generation - Test with all supported carriers
- Print jobs - Verify Device Hub connectivity
- Error handling - Test rate limits, invalid tokens, blocked orders
