Getting Started with Aeion Fulfillment

Configure your first warehouse, connect Bridge hardware (thermal printers, scales, scanners), allocate inventory across facilities, and process your first pick list — in under 20 minutes.

1

Set Up Your First Warehouse

Navigate to Logistics → Warehouse Ops → Warehouses and create your first facility.

Required fields:

  • Name — e.g., "West Coast DC, Rialto CA"
  • Address — Full street, city, state, zip, country
  • Status — Active / Inactive

Automatic geocoding: Aeion automatically geocodes the warehouse address using OpenStreetMap/Nominatim when you call:

POST /api/fulfillment/warehouses/:id/geocode

Or use the admin UI — navigate to the warehouse record and click "Geocode Address" in the actions menu. This resolves address{ lat, lng } for Haversine routing.

json // Warehouse coordinates populated automatically { "id": "wh_west_coast", "name": "West Coast DC, Rialto CA", "address": { "street": "1234 Distribution Pkwy", "city": "Rialto", "state": "CA", "zip": "92316", "country": "US" }, "coordinates": { "lat": 34.0911, "lng": -117.3884 }, "geocodedAt": "2026-05-11T10:30:00Z", "status": "active" }

Tip: Warehouse coordinates are the anchor for the Haversine routing engine. Every allocation decision starts here. If geocoding fails for a rural address, enter coordinates manually from Google Maps (right-click on location → copy coordinates).

2

Connect Warehouse Hardware (Bridge App)

The Aeion Bridge desktop app connects ZPL thermal printers, USB/serial package scales, and HID barcode scanners to your WMS.

Download and Connect Bridge

  1. Download Aeion Bridge from aeionos.com/bridge
  2. Launch Bridge and sign in with your Aeion admin credentials
  3. Connect hardware via USB or serial:

Verify Connection

In the Aeion admin, open Logistics → Warehouse Ops → Settings → Hardware. The Bridge connection status panel shows:

json { "connected": true, "capabilities": { "labelPrinter": true, // ZPL thermal printer detected "scale": true, // Serial scale detected "barcodeScanner": true, // USB HID scanner detected "packingSlipPrinter": true } }

If Bridge is disconnected, all fulfillment operations continue in browser-only mode — printing and scanning require Bridge. Hardware is an enhancement, not a requirement.

Configure Serial Port (Scales)

If you have multiple serial devices, specify the port explicitly in the body:

```bash POST /api/fulfillment/bridge/scale/read { "port": "/dev/ttyUSB0" }

# Or let Bridge auto-detect (first scale-class serial device) POST /api/fulfillment/bridge/scale/read ```

For a continuous reading stream (useful for live UI gauges):

bash POST /api/fulfillment/bridge/scale/stream/start DELETE /api/fulfillment/bridge/scale/stream

Supported port paths:

  • Linux/macOS: /dev/ttyUSB0, /dev/tty.usbserial-A50285BI
  • Windows: COM3, COM4
3

Add Inventory to a Warehouse

Navigate to Logistics → Inventory and add SKUs to your warehouse.

Manual receipt (CLI/API):

POST /api/fulfillment/stock/receive

json { "sku": "WGT-001-RD-XL", "warehouseId": "wh_west_coast", "quantity": 250, "reference": "PO-2026-0511" }

Behind the scenes, this:

  1. Creates an immutable movement-ledger entry recording who made the change
  2. Updates the warehouse's on-hand quantity for that SKU
  3. Recomputes sellable availability (on-hand minus what's already allocated to orders)
  4. Emits fulfillment.low_stock if availability drops to or below the reorder point

Bulk import: Use the admin CSV import at Logistics → Inventory → Import CSV with columns: sku, warehouseId, quantity, onHand, reorderPoint.

4

Allocate Orders to Warehouses

When a Commerce order is placed and payment is captured, allocation runs automatically. The flow is event-driven — it's triggered from Commerce or from a Blueprint workflow subscribed to the order-created event, not from a public REST endpoint you need to call yourself.

Preview the optimal warehouse first without committing the allocation:

bash GET /api/fulfillment/optimal-origin?sku=WGT-001-RD-XL&quantity=2&lat=34.05&lng=-118.24

json { "data": { "id": "wh_west_coast", "name": "West Coast DC, Rialto CA", "distanceMiles": 12.4, "available": 248 }, "alternatives": [ { "warehouse": { "id": "wh_east_coast", "name": "East Coast DC" }, "distanceMiles": 2789.1 } ] }

Use this to show the customer an estimated ship-from on the checkout page. The real allocation then runs automatically when the order is paid.

What happens internally when allocation fires:

  1. Address geocoding — If the shipping address has no coordinates yet, it's geocoded to resolve latitude/longitude
  2. Distance sorting — Every active warehouse with coordinates is ranked by distance from the customer
  3. Stock filtering — Only warehouses with enough sellable stock for the requested quantity are eligible
  4. Greedy allocation — Allocation starts from the nearest warehouse. If it can't fulfill the full quantity, the order splits across multiple warehouses
  5. Atomic inventory update — Each warehouse's allocation is a guarded, atomic operation — it only succeeds if the stock is genuinely available, closing the race-condition window that causes overselling
  6. Movement ledger — Every allocation creates an immutable ledger entry
  7. Event emissionfulfillment.order_allocated fires with the full allocation map
  8. Pick list generation — Call POST /api/fulfillment/picking-lists/generate to aggregate items across allocated orders

Response:

json { "allocations": [ { "sku": "WGT-001-RD-XL", "warehouseId": "wh_west_coast", "quantity": 2, "warehouseName": "West Coast DC, Rialto CA" } ] }

If a SKU has no available stock anywhere: fulfillment.allocation_failed is emitted with reason: "Insufficient stock across all warehouses".

5

Print Shipping Labels and Run Pick Lists

Generate a Pick List

POST /api/fulfillment/picking-lists/generate

json { "warehouseId": "wh_west_coast", "orderIds": ["ord_01J8XK4N3P", "ord_01J8XK4N4Q"], "listName": "Batch 2026-05-11 AM" }

The engine aggregates all line items across the specified orders, groups by SKU, and creates a consolidated pick list:

json { "name": "Batch 2026-05-11 AM", "warehouseId": "wh_west_coast", "status": "pending", "items": [ { "productId": "WGT-001-RD-XL", "quantityToPick": 12, "orderIds": ["ord_01J8XK4N3P", "ord_01J8XK4N4Q"] } ] }

Print ZPL Shipping Label

POST /api/fulfillment/bridge/labels/shipping

json { "recipientName": "Jane Smith", "address": "567 Commerce Blvd", "city": "Los Angeles", "state": "CA", "zip": "90012", "country": "US", "trackingNumber": "1Z999AA10123456784", "carrier": "UPS", "serviceLevel": "GROUND", "weight": 2.5, "weightUnit": "lb", "orderRef": "AE-2026-0511" }

Aeion generates 4x6" ZPL II label with:

  • Sender block (top-left)
  • Carrier badge + service level + weight (top-right)
  • Recipient address (center, 32pt)
  • Code 128 tracking barcode (100 dots tall)
  • Human-readable tracking number
  • Full label border

Label is sent directly to the Bridge-connected thermal printer.

Scan Items During Pick Run

POST /api/fulfillment/bridge/scan/pick/start

json { "orderId": "ord_01J8XK4N3P" }

Bridge continuously polls the barcode scanner. Each scan returns:

json { "barcode": "WGT-001-RD-XL", "format": "code128", "onPickList": true, "itemName": "Widget Pro - Red - XL", "expectedQty": 2, "scannedQty": 1, "scannedAt": "2026-05-11T10:45:00Z" }

If the picker scans an item not on the pick list, onPickList: false flags it for supervisor review.

6

Ship and Manifest

Print Packing Slip

POST /api/fulfillment/bridge/labels/packing-slip

json { "orderNumber": "AE-2026-0511", "orderDate": "2026-05-11", "recipientName": "Jane Smith", "address": "567 Commerce Blvd", "city": "Los Angeles", "state": "CA", "zip": "90012", "country": "US", "items": [ { "sku": "WGT-001-RD-XL", "name": "Widget Pro - Red - XL", "quantity": 2, "imageUrl": "https://cdn.aeionos.com/wgt-001-red-xl.jpg" } ], "message": "Thank you for your order!", "returnPolicy": "Returns within 30 days — aeionos.com/returns" }

Prints to a standard (non-thermal) printer via Bridge. Includes product image thumbnails.

Scan Manifest Batch

At the dock, scan all packages for a carrier manifest:

POST /api/fulfillment/bridge/scan/manifest

json { "timeout": 60000, "maxScans": 100 }

Operator scans 100 packages max (or until 60-second timeout). Each scan looks up the package's tracking number and builds the manifest:

json { "packages": [ { "barcode": "1Z999AA10123456784", "format": "code128", "trackingNumber": "1Z999AA10123456784", "scannedAt": "2026-05-11T11:00:00Z" }, { "barcode": "1Z999AA10123456785", "format": "code128", "trackingNumber": "1Z999AA10123456785", "scannedAt": "2026-05-11T11:00:05Z" } ], "totalScanned": 2 }

7

Process Returns (RMA)

Navigate to Logistics → Orders & Returns → Returns and open a return authorization.

Process a Received Return

POST /api/fulfillment/returns/:id/process

json { "id": "rma_01J8XM2N5R", "status": "received", "restockInventory": true, "warehouseId": "wh_west_coast", "items": [ { "variantSku": "WGT-001-RD-XL", "quantity": 1, "condition": "opened" }, { "variantSku": "ACC-CABLE-USB", "quantity": 1, "condition": "damaged" } ], "rmaNumber": "RMA-2026-0511-001" }

Restock logic:

  • condition: "new" → restocked to sellable inventory
  • condition: "opened" → restocked to sellable inventory
  • condition: "damaged"Not restocked — dispositioned per business rules

After processing, status → completed and fulfillment.return_processed event fires.

RMA condition values: new, opened, damaged, defective, wrong_item

Connecting Hardware — Protocol Reference

ScaleResponseParsed
Toledo`ST,GS, +0002.50 kg``{ weight: 2.50, unit: "kg", stable: true }`
CAS`+002.50 kg S``{ weight: 2.50, unit: "kg", stable: true }`
Ohaus` 2.50 lb GS``{ weight: 2.50, unit: "lb", stable: true }`

Events Reference

Subscribe to these events in Blueprint workflows or custom integrations:

```typescript // Subscribe in your module's setup() api.events.subscribe("fulfillment.order_allocated", async (event) => { const { orderId, orderNumber, allocations } = event; // → Trigger notification to warehouse team // → Update order status in Commerce // → Generate shipping labels });

api.events.subscribe("fulfillment.allocation_failed", async (event) => { const { orderId, sku, quantity, reason } = event; // → Alert buyer to reorder // → Notify customer of partial fulfillment });

api.events.subscribe("fulfillment.low_stock", async (event) => { const { sku, warehouseId, available } = event; // → Trigger purchase order via SCM // → Alert procurement manager });

api.events.subscribe("fulfillment.return_processed", async (event) => { const { returnId, rmaNumber } = event; // → Update return status in admin // → Trigger refund workflow }); ```

Low-Stock Alert Setup

Set a reorder point on any inventory item to trigger automatic alerts:

Via admin UI: Edit any inventory item → Reorder Point field → enter threshold

Via API:

PATCH /api/v1/fulfillment_inventory/:id

json { "reorderPoint": 25 }

When available (onHand - allocated) drops to 25 or below, fulfillment.low_stock fires. Connect this to a Blueprint workflow:

Trigger: fulfillment.low_stock → Condition: available < 10 (emergency reorder) → Action: Send alert to procurement manager (email + Slack) → Action: Auto-create purchase order in SCM module