A warehouse asks you for shelf labels. Marketing wants a QR code on every product insert. Finance wants asset tags for 3,000 laptops. Each request sounds like "call the generator in a loop", and each one goes wrong in a different way if that is all you do: the warehouse scanners cannot read QR, the insert codes point at a URL that will be dead in a year, and the asset tags get regenerated (and paid for) every time the script is rerun.
The Fundamentals project generated one signed QR code per event ticket. This lesson is about the decisions that come before the loop and the checks that come after it.
Pick the symbology before you write any code
The symbology is decided by who scans it and with what, not by what looks modern. Get this wrong and no amount of code fixes it, because the labels are already printed.
| QR code | Code 128 | Code 39 | |
|---|---|---|---|
| Shape | 2D square | 1D linear | 1D linear |
| Read by | Phone cameras, 2D imagers | Any 1D laser or imager | Any 1D laser or imager, including very old ones |
| Characters | Any text, including UTF-8 | All 128 ASCII characters | Uppercase A-Z, 0-9, space and - . $ / + % |
| Practical length | Hundreds of characters | Roughly 20-30 before the label gets too wide | Roughly 10-15; it is the least dense |
| Typical home | Marketing, tickets, contact cards, Wi-Fi | Shipping, inventory, asset tags | Legacy industrial and government systems |
Three rules come out of that table.
Laser scanners cannot read QR codes. A handheld laser scanner sweeps one line across the label. A QR code has no meaningful single line. If the warehouse has laser guns, the answer is Code 128, however much you like QR.
Phones are bad at linear barcodes. Camera apps on most phones will open a URL from a QR code without any extra app, but treat a 1D barcode as text at best. If a consumer is meant to scan it, use QR.
Code 39 is for compatibility, never for preference. It exists because some older systems only understand Code 39. Its character set has no lowercase letters and no underscore. When we sent part_7781 with type: code39, the API refused it with "Invalid barcode data or type". When we sent part-7781 it succeeded, but Code 39 cannot represent lowercase, so whatever the scanner gives back will not be the lowercase string you stored. Normalise identifiers to uppercase before you generate Code 39, and store them that way.
One more boundary: the Barcode Generator produces code128 and code39 only. Retail product codes (EAN-13, UPC-A) require a number assigned by GS1 to your company, and the generator rejects ean13 as a type. The Barcode Reader, on the other hand, decodes EAN and UPC, which matters when you are receiving goods rather than labelling them.
A warehouse uses handheld laser scanners on its picking line. You need a label that encodes bin location IDs like A-12-07. Which symbology should you generate?
Answer the check above to continue.
Read the codes back before they go to print
A code you have not scanned is a code you are hoping about. The QR Code Reader and Barcode Reader each take an image and return the decoded text. The Barcode Reader also returns format (the symbology it recognised) and isRetail. Both accept the image as a multipart file upload in a field named image; let your HTTP client build the multipart body, because the Content-Type header has to carry a boundary you cannot write by hand.
import { readFile } from 'node:fs/promises';
export async function readCode(file, kind) {
const api = kind === 'qr' ? 'qrcodereader' : 'barcodereader';
const form = new FormData();
form.append('image', new Blob([await readFile(file)], { type: 'image/png' }), 'code.png');
const res = await fetch(`https://api.apiverve.com/v1/${api}`, {
method: 'POST',
headers: { 'x-api-key': process.env.APIVERVE_API_KEY }, // no Content-Type: fetch adds the boundary
body: form,
signal: AbortSignal.timeout(30000),
});
const json = await res.json();
if (json.status !== 'ok') throw new Error(json.error || `HTTP ${res.status}`);
return json.data.text;
}
export async function verify(file, kind, expected) {
const text = await readCode(file, kind);
const want = kind === 'code39' ? expected.toUpperCase() : expected;
return { file, ok: text === want, text };
}import os
import sys
import requests
def read_code(path, kind):
api = "qrcodereader" if kind == "qr" else "barcodereader"
with open(path, "rb") as f:
res = requests.post(
f"https://api.apiverve.com/v1/{api}",
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]},
files={"image": ("code.png", f, "image/png")},
timeout=30,
)
body = res.json()
if body.get("status") != "ok":
raise RuntimeError(body.get("error") or f"HTTP {res.status_code}")
return body["data"]["text"]
if __name__ == "__main__":
path, kind, expected = sys.argv[1:4]
text = read_code(path, kind)
want = expected.upper() if kind == "code39" else expected
print("OK" if text == want else f"MISMATCH: got {text!r}")Uploads on Free and Hobby plans are capped at 400 KB, which a generated PNG fits comfortably; a phone photo of a printed label may not, so resize photos before sending them.
What to verify, and how much
Reading back costs another 10 credits per code, which doubles the cost of the batch. Whether that is worth it depends on what a bad code costs you.
- Verify every code when the payload is built from data you do not fully control (customer-entered names in vCards, free-text part numbers), or when a bad label means a reprint of thousands.
- Verify a sample when every payload comes from one template. If the template is right for 50 random rows, it is right for the rest; check the first, the last, the longest payload and a random handful.
- Verify the printed output, not just the file, for anything going to a commercial printer. Photograph a proof and run it through the reader. Scaling, ink spread and low-contrast brand colours break codes that were perfect as PNGs.
A mismatch tells you something specific. For QR, it usually means an escaping bug in the payload builder. For Code 39, it usually means case. Either way you found it before the print run, which is the whole point.
You generated a Code 39 label for the identifier 'part-7781' and the Barcode Reader returns 'PART-7781'. What is the correct conclusion?
Production notes
Keep the payload table, not just the images. The manifest written by the batch maps every payload to its file. Keep it with the label job, so a reprint a year from now reuses the same images instead of guessing which file was which.
Don't put secrets or personal data in a code you can't take back. Anything encoded is readable by anyone who can photograph it. A parcel label with a customer's phone number in a QR code is a privacy leak on every doorstep. Encode an opaque ID or a signed URL and look the details up server-side.
Download inside the expiry window, every time. The batch saves each image in the same step that generates it. Splitting generation and download into separate jobs is how a queue ends up full of dead links, especially for barcodes with their shorter lifetime.
Next you will put money on paper: Generate invoices covers numbering, tax lines and storing a PDF you may have to produce for an auditor years from now.