Closed Loop System

Confirming pick-to-light picks with callback acknowledgements and transaction tracking.

What Is a Closed Loop System?

Running a closed-loop system lets your server know whether the picker acknowledged the pick-to-light task. This feedback loop is essential for maintaining accurate inventory and ensuring every pick is verified.

Modern devices support barcodes and QR-Codes that are a natural choice for transaction confirmations. In fact, the encoded data can be customized for each individual pick.

Callback Confirmations

To track each pick, include a unique transaction ID with the command. In the REST API, this field is called a nonce. When the picker responds, Voodoo sends that same ID back to your server so you know exactly which pick was confirmed.

REST API Example

import requests, json

session = requests.Session()
url = "https://www.voodoodevices.com/api/"

# Login
x = session.post(url + "user/login/",
    json={'username': 'yourusername', 'password': 'yourpassword'})
z = json.loads(x.text)

# Send command with nonce for closed-loop tracking
session.post(
    url + "device/D4F660:AFA0CB/",
    headers={'referer': url, 'x-csrf-token': z['token']},
    json={
        'command': 'flash',
        'line1': 'Jessica',
        'line2': 'Pick 3',
        'line3': 'SKU:001256',
        'nonce': 'transactionID001',
        'seconds': 120
    }
)

QueryString Example

QueryString URL with Transaction ID
https://www.voodoodevices.com/api/D4F660:AFA0CB/pick/Jessica Pick 3/SKU:001256/140,c5,4/20/transactionID001

Acknowledgement State Model

After you send a command with a transaction ID, your server can receive one of three results:

ackAcknowledged (ACK)

The picker confirmed the task by pressing the button. Your callback URL receives:

https://your.domain.com/voodoodevices/transactionID001/ack
nackNot Acknowledged (NACK / Timeout)

The timer expired and the picker did not confirm the task. Your callback URL receives:

https://your.domain.com/voodoodevices/transactionID001/nack
lostNo Response

The device did not respond, usually because it was out of range or disconnected. By default, no callback is sent.

Callback URL Configuration

The prefix of the response URL (e.g., http://your.domain.com:8000/voodoodevices/) is a string you choose and enter in www.voodoodevices.com on the Settings page. The URL can be on any port, use HTTP or HTTPS and use GET or POST methods.

Inbound Connectivity Required

When using www.voodoodevices.com, the callback is sent from Voodoo's cloud server on the public Internet to the URL you provide. If that destination is a server inside your corporate firewall, most firewalls will block the connection by default because it is an inbound request from the Internet into your private network. For that reason, you must allow inbound messages from www.voodoodevices.com to reach that internal server. If your callback URL points to a public cloud endpoint instead, no firewall change is needed. If your environment cannot permit that inbound access, you may be a better fit for a self-hosted server deployment. See the Server Deployment guide.

Receiving the Callback

Your server needs an endpoint that Voodoo can reach. Below is a minimal Python example that listens on port 8000 and prints every callback it receives — a useful starting point before you wire the result into your own order or inventory system.

from http.server import BaseHTTPRequestHandler, HTTPServer
import json

# Voodoo calls back using either GET or POST, depending on
# the method you configured on the Settings page.
#
# GET example — result encoded in the URL path:
#   GET /voodoodevices/transactionID001/ack
#
# POST example — result sent as a JSON body:
#   {
#     "acknack":  "ACK",
#     "location": "A1-SHELF002",
#     "deviceid": "D4F660",
#     "nonce":    "transactionID001",
#     "qty":      3
#   }

class CallbackHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        print(f"GET  {self.path}")
        self.send_response(200)
        self.end_headers()

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length)
        data = json.loads(body)
        print(f"POST {self.path}")
        print(json.dumps(data, indent=2))
        self.send_response(200)
        self.end_headers()

if __name__ == "__main__":
    server = HTTPServer(("0.0.0.0", 8000), CallbackHandler)
    print("Listening on port 8000 ...")
    server.serve_forever()

ACK/NACK and Command Lifecycle

ACK and NACK callbacks are only generated for commands (time-bound work with a nonce). Once a command is acknowledged or times out, it clears and the device falls back to its persistent statics. Understanding this distinction is crucial for designing reliable pick-to-light workflows. See the Commands vs. Statics Quick Reference for complete details on timeout behavior, seconds vs. displaytimeout, and best practices.

Common Patterns

Here are common integration patterns for closed-loop systems:

  • Simple confirmation: Use the transaction ID to mark a pick as complete in your WMS when an "ack" is received.
  • Timeout handling: Re-send the command or escalate to a supervisor when a "nack" is received.
  • Barcode verification: Display a unique barcode on the device for each pick. The picker scans the barcode to confirm, and a kill command is sent to the device. No button push needed!
  • Quantity adjustment: Use multi-button devices to let pickers adjust quantity before acknowledgement. The final quantity is sent along with the ack/nack callback.

Summary Flow

1Your Server → sends command with nonce → www.voodoodevices.com
2www.voodoodevices.com → routes to Turbo → Cloud Display Device lights up
3Picker → presses button (or timeout occurs)
4www.voodoodevices.com → calls your callback URL with ack/nack + nonce
5Your Server → updates inventory/order status accordingly