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.
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).
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
- Download Aeion Bridge from aeionos.com/bridge
- Launch Bridge and sign in with your Aeion admin credentials
- 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
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:
- Creates an immutable movement-ledger entry recording who made the change
- Updates the warehouse's on-hand quantity for that SKU
- Recomputes sellable availability (on-hand minus what's already allocated to orders)
- Emits
fulfillment.low_stockif 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.
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:
- Address geocoding — If the shipping address has no coordinates yet, it's geocoded to resolve latitude/longitude
- Distance sorting — Every active warehouse with coordinates is ranked by distance from the customer
- Stock filtering — Only warehouses with enough sellable stock for the requested quantity are eligible
- Greedy allocation — Allocation starts from the nearest warehouse. If it can't fulfill the full quantity, the order splits across multiple warehouses
- 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
- Movement ledger — Every allocation creates an immutable ledger entry
- Event emission —
fulfillment.order_allocatedfires with the full allocation map - Pick list generation — Call
POST /api/fulfillment/picking-lists/generateto 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".
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.
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
}
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 inventorycondition: "opened"→ restocked to sellable inventorycondition: "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
| Scale | Response | Parsed |
|---|---|---|
| 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