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)
- 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 --> KIntegration 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.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'
},
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('tenantId', TENANT_ID)
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, tenant_id, 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