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-tenant permission 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

MetricAlert ThresholdAction
Token refresh failuresAnyCheck connectivity, credentials
API error rate> 5%Investigate error types
Label generation time> 10sCheck carrier API health
Reprint rate> 10%Review warehouse processes
PII access rejectionsUnusual patternsSecurity 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:

  1. Token lifecycle - Verify refresh works before expiry
  2. OAuth flow - Complete flow with test seller
  3. Order sync - Verify address scrubbing works
  4. Label generation - Test with all supported carriers
  5. Print jobs - Verify Device Hub connectivity
  6. Error handling - Test rate limits, invalid tokens, blocked orders

Next Steps

API Reference

Complete documentation for all API endpoints.

Security

Understand how Data Border protects your data.