Scanner API
The Scanner API is designed for mobile devices used by warehouse pickers, drivers, and intake staff.
Overview
The Scanner API provides:
- Barcode Lookup: Find sellable products by UPC/EAN barcode
- Inventory Scanning: Update quantities with FEFO/FIFO tracking
- Inventory Receiving: Receive new inventory from sweeps
- Order Picking: Access orders assigned for fulfillment
- Bag Scanning: Scan items into bags for order assembly
Authentication
Scanner devices authenticate using Supabase JWT tokens:
// Login with Supabase
const { data } = await supabase.auth.signInWithPassword({
email: 'picker@example.com',
password: 'password123'
})
// Use access token for API calls
const response = await fetch('/scanner/inventory/lookup?barcode=012345678901', {
headers: {
'Authorization': `Bearer ${data.session.access_token}`
}
})
Endpoints
Scanner Status
Check scanner API availability and user permissions.
GET /scanner
Authorization: Bearer <token>
Response:
{
"status": "ok",
"version": "2.0.0",
"features": {
"inventory_scan": true,
"barcode_lookup": true,
"location_tracking": true,
"fefo_fifo_picking": true,
"inventory_receiving": true
}
}
Product Lookup
Look up a sellable product by barcode. Returns inventory in FEFO/FIFO order.
GET /scanner/inventory/lookup?barcode=012345678901&location_id=loc-uuid
Authorization: Bearer <token>
Response:
{
"success": true,
"barcode": "012345678901",
"product": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Organic Bananas",
"brand": "Dole",
"image_url": "https://cdn.example.com/bananas.jpg",
"selling_price": 1.99,
"is_perishable": true,
"warehouse_zone": "C"
},
"inventory": {
"total_quantity": 48,
"total_reserved": 12,
"available_quantity": 36,
"items": [
{
"id": "inv-item-uuid",
"location_id": "loc-uuid",
"quantity": 24,
"reserved_quantity": 6,
"available": 18,
"expiration_date": "2025-01-15",
"received_at": "2024-12-15T10:00:00Z",
"lot_number": "LOT-2024-001"
}
]
}
}
Inventory items are returned in FEFO/FIFO order - items expiring soonest first, then oldest received.
Inventory Scan
Process an inventory scan operation.
POST /scanner/inventory/scan
Authorization: Bearer <token>
Content-Type: application/json
{
"barcode": "012345678901",
"location_id": "loc-uuid",
"quantity": 24,
"action": "receive",
"expiration_date": "2025-01-15",
"lot_number": "LOT-2024-001"
}
Actions
| Action | Description |
|---|
lookup | Just look up product and inventory |
adjust | Add or subtract from current quantity |
count | Set exact count (inventory audit) |
receive | Receive new inventory (e.g., from sweep) |
Response:
{
"success": true,
"barcode": "012345678901",
"action": "receive",
"product": {
"id": "prod-uuid",
"name": "Organic Bananas",
"is_perishable": true
},
"inventory": {
"item_id": "new-inv-item-uuid",
"quantity": 24,
"reserved_quantity": 0,
"available": 24,
"location_id": "loc-uuid",
"expiration_date": "2025-01-15"
},
"totals": {
"total_quantity": 48,
"total_reserved": 12,
"available": 36
},
"scanned_at": "2024-12-18T15:30:00Z",
"scanned_by": "staff-uuid"
}
List Orders
Get orders assigned for picking.
GET /scanner/orders?status=pending&limit=20
Authorization: Bearer <token>
Response:
{
"orders": [
{
"id": "order-uuid",
"display_id": 1234,
"status": "picking",
"source": "app",
"created_at": "2024-12-18T10:00:00Z",
"total": 45.99,
"location_id": "loc-uuid"
}
],
"count": 15,
"limit": 20,
"offset": 0
}
Order Details
Get full order details for picking, including totes and bags.
GET /scanner/orders/:id
Authorization: Bearer <token>
Response:
{
"order": {
"id": "order-uuid",
"display_id": 1234,
"status": "picking",
"source": "app",
"created_at": "2024-12-18T10:00:00Z",
"location_id": "loc-uuid",
"items": [
{
"id": "item-uuid",
"sellable_product_id": "prod-uuid",
"title": "Organic Bananas",
"quantity": 2,
"unit_price": 1.99,
"image_url": "https://cdn.example.com/bananas.jpg",
"fulfillment_source": "inventory",
"allocated_at": "2024-12-18T10:05:00Z"
}
],
"totes": [
{
"id": "tote-uuid",
"tote_code": "TOTE-001",
"status": "packing",
"bags": [
{
"id": "bag-uuid",
"bag_code": "BAG-001",
"bag_type": "chilled"
}
]
}
]
}
}
Bag and Tote Workflow
Picking Flow
- Picker receives pick_list assignment
- For each pick_list_item:
- Scan product barcode
- System shows inventory location (FEFO/FIFO)
- Picker retrieves item
- Scan into bag (creates bag_item)
- Seal bag and assign to tote
- Stage tote for robot delivery
Intake Flow (Sweep Returns)
- Driver returns from sweep
- For each sweep_item:
- Scan product barcode
- Scan into bag (creates bag_item)
- Links to order_item for fulfillment
- Bags assigned to totes
- Stage for delivery
Permissions
| Endpoint | Permission |
|---|
GET /scanner | scanner.use |
GET /scanner/inventory/lookup | inventory.read |
POST /scanner/inventory/scan | inventory.scan |
POST /scanner/inventory/scan (receive) | inventory.receive |
GET /scanner/orders | orders.read |
GET /scanner/orders/:id | orders.read |
Error Responses
401 Unauthorized
No valid authentication token.
{
"error": "Unauthorized"
}
403 Forbidden
Authenticated but missing required permission.
{
"error": "Forbidden: Missing required permission",
"required": "inventory.scan"
}
404 Not Found
Barcode not found in sellable catalog.
{
"error": "Product not found",
"barcode": "012345678901"
}
The barcode must exist in both scraped_products AND sellable_products to be found. If a product exists in scraped_products but hasn’t been curated into sellable_products, it will return 404.
Mobile SDK Example
class ScannerClient {
private token: string;
private baseUrl = 'https://api.switchyard.run';
async login(email: string, password: string) {
const { data } = await supabase.auth.signInWithPassword({ email, password });
this.token = data.session.access_token;
}
async lookupBarcode(barcode: string, locationId?: string) {
let url = `${this.baseUrl}/scanner/inventory/lookup?barcode=${barcode}`;
if (locationId) url += `&location_id=${locationId}`;
const response = await fetch(url, {
headers: { Authorization: `Bearer ${this.token}` }
});
return response.json();
}
async receiveInventory(
barcode: string,
locationId: string,
quantity: number,
expirationDate?: string,
lotNumber?: string
) {
const response = await fetch(`${this.baseUrl}/scanner/inventory/scan`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
barcode,
location_id: locationId,
quantity,
action: 'receive',
expiration_date: expirationDate,
lot_number: lotNumber
})
});
return response.json();
}
async adjustInventory(barcode: string, locationId: string, adjustment: number) {
const response = await fetch(`${this.baseUrl}/scanner/inventory/scan`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
barcode,
location_id: locationId,
quantity: adjustment,
action: 'adjust'
})
});
return response.json();
}
}