Scanning from your own hardware
If you are not using a Skuflo tablet or phone, POST /v1/scan is the one endpoint your hardware needs. A scan station token (see Authentication) is locked to one event, produce, stock or dispatch, so a request can never claim to be something it is not.
The scan endpoint#
barcode is required. location is only read on a stock station (a rack bay, for example A3), and falls back to the station’s configured default location when left out. client_scan_id is for offline clients: see below.
Full parameter and response reference: POST /v1/scan, where you can also send a real scan with Try it.
Every outcome#
The response is always { ok, outcome, barcode, item, order, ... }. ok and the HTTP status tell you at a glance whether to show a green tick or a red cross; outcome says exactly why.
| Outcome | Status | Meaning |
|---|---|---|
ok | 200 | Accepted. The piece moved to its next state. |
duplicate | 200 | This exact scan already happened (same piece, same event), or a replayed client_scan_id. |
test | 200 | The printed hardware test label. Nothing was touched: this just proves the scanner and the station can talk to each other. |
not_found | 409 | The barcode does not match a label in this factory. |
not_produced | 409 | A stock or dispatch scan arrived before the piece was ever produced. |
already_dispatched | 409 | The piece has already been loaded and cannot move again. On a stock station, can_log_return: true means it has no open return: send it again with unexpected_return and return_condition to log one. |
return_condition_required | 409 | A stock station scanned a loaded piece that is on an open return. Nothing is recorded yet: return names the return and suggested_bay a returns bay. Send the scan again (with a new client_scan_id) with return_condition. |
return_received | 200 | The returned piece is received, with its condition and, if location was given, the returns bay. The retailer is emailed and return.received fires. |
bad_condition | 409 | return_condition must be new, needs_repair or unrepairable. |
cancelled | 409 | The order this piece belongs to was cancelled. |
voided | 409 | This label was voided. The response field replacement_barcode names the piece that replaced it, if any. |
location_required | 409 | A stock scan needs a rack bay: pass location, or set a default location on the station. |
bad_location | 409 | The rack bay given does not exist, or is out of range for that rack. |
Returned pieces#
A retailer asks to send pieces back from their portal, and the factory receives each one by scanning its original label at a stock station. A loaded piece scanned there is a return, not a move into stock, and takes two requests:
- The first scan answers
return_condition_requiredwith the return (return.ref, retailer, reason) and asuggested_bay: the first bay of a rack whose zone is called Returns. Nothing is recorded yet. - Ask the person what condition it came back in and send the scan again, with a new
client_scan_id, addingreturn_condition(new,needs_repairorunrepairable),locationfor the returns bay if you want one, andreturn_noteif useful. The answer isreturn_received.
A loaded piece with no open return answers already_dispatched with can_log_return: true. Sending it again with unexpected_return: { reason } and a return_condition logs a return for its retailer and receives it in one go. What happens next (back into stock, repaired, written off) is done in the app or through the Returns endpoints, and the return.requested, return.received and return.resolved webhooks follow each step.
Offline replay#
A phone or handheld that queues scans while it has no signal should generate its own client_scan_id for every scan (a UUID is fine) and send the same id if that queued scan is ever replayed. The first attempt is recorded normally; every later attempt with the same id comes back as outcome duplicate, with replay: true, so the piece is only ever counted once no matter how many times the network makes you retry.
Barcode normalisation#
Whatever a scanner or a keyboard wedge setting actually sends is cleaned up before it is matched against a label, so your hardware does not need to get this exactly right:
- Control characters (carriage return, tab, the FNC1/GS prefix some scanners add) are stripped from anywhere in the payload.
- A leading AIM symbology identifier (for example
]C0or]Q6) is removed. - Our own QR link (
https://skuflo.io/i/<code>orskuflo://i/<code>) is unwrapped to the plain barcode underneath, and URL decoded. - Internal whitespace is collapsed to a single space, and the result is uppercased.
- If a scanner drops the space between the label prefix and the code (for example
OAK7F3K9QZ2), it is reattached when the shape is unambiguous.
In short: send whatever the scanner gives you as the barcode field, raw. There is no need to clean it up on the device first.
The hardware test label#
GET /v1/hardware/test-label returns a printable PDF or ZPL label with a fixed code. Scanning it at any station always returns outcome test and never touches an order, so it is safe to use while you are getting a new scanner or a new build of your app talking to Skuflo for the first time.