DIRECT MAIL AdRails 0.8.0

Postcard QR codes and per-piece tracking codes

Each postcard gets its own tracking code and a QR code that points at your landing page with that code attached, so a scan or a phone call traces back to one mail piece. Generate the set, check the QR symbols, then download a manifest CSV, a print-exact Avery 5160 label sheet or a page of QR SVGs.

Last reviewed 2026-11-06 with AdRails 0.8.0

TOOL

Generate postcard tracking codes and QR codes

Codes avoid 0, 1, I, L, O and U and carry a check character. Tracking URLs carry the campaign name. Nothing leaves your browser.

Must start with https://. Every QR and tracking URL points here with ?c=CODE appended.
Added to the tracking URL, so a code alone identifies the piece and the run.
1 to 500. Ignored when a mailing list CSV is loaded.
Higher survives more scuffs but holds fewer characters. Long URLs may overflow at H.
First row must be headers. Name and address columns are printed on the label sheet.
Ready.

Runs entirely in your browser. Nothing you enter is sent anywhere.

METHOD

How do I track who responds to a postcard?

One code per piece, one URL per code, and a scan count that ignores bots.

Assign one code per piece

Every piece in the run gets its own short code from the mail alphabet, plus a tracking URL built from your landing page, that code and the campaign name. The code is printed on the label; the QR symbol encodes the URL. A caller who reads the code back and a scanner who lands on the page resolve to the same single piece.

Turn a scan into a piece

AdRails reads your own web-server access logs and counts unique human scans per code. Bots and link previewers (Slack, Facebook, WhatsApp and the rest) are dropped, along with your own IPs and CIDRs across IPv4 and IPv6, unknown codes, and any hit that arrived before that piece was mailed. Visitor ids are stored hashed, never as raw IPs, and re-running the count is idempotent. selftest: mail.scans

Attribute the lead

Attribution runs in a fixed order: tracking code first, then UTM, then what the lead said, then referrer. A code read back over the phone therefore beats a UTM that a shared office browser overwrote. Cost per lead and per client then sits on one ruler with Google Ads, lead vendors and referrals. selftest: mail.cac

A scan and a phone call are both single-piece signals. Neither one proves the postcard was read; each proves one piece was answered. Treat response counts as a floor, never a census.

WHY PER PIECE

Why give every postcard its own code?

A shared code tells you the campaign worked. A per-piece code tells you which list, batch and design did the work.

What per-piece codes buy you

  • Batches compare cleanly. Response rates by any column come back with 95% Wilson intervals and a verdict that says "too early" or "no signal yet: could still be up to X%" instead of a flattering number from a sample of nine. selftest: mail.response
  • Two designs get a real test. mail_compare runs a two-proportion test with a difference interval, so "B beat A" arrives with an interval attached rather than a hunch. selftest: mail.response
  • Small runs get sized honestly. mail_holdout_plan returns the number of pieces and the no-mail holdout you need to prove the channel at 95% confidence and 80% power, within 3% of the textbook formula. selftest: mail.response

What a single shared code costs you

  • You cannot separate a good list from a good postcard. One code collapses list, offer, design and postcode band into a single number that cannot be split afterwards.
  • A bad batch stays invisible. With one code, a segment that was mailed the wrong address block looks exactly like a segment that ignored you.
  • Some responses never register. A piece binned without a scan and a phone call nobody logged produce no row at all. Codes lower the floor of what you miss; they do not remove it.
CODES

Why these characters — and what does the check character catch?

Drop the lookalikes, weight each position, reduce modulo a prime, and a code survives being read down a phone line.

30 characters, no lookalikes, one checksum

  • Nothing ambiguous to mistype. The alphabet excludes 0, 1, I, L, O and U, so a code has no 0/O clash and no 1/I/L clash. A lookalike is never guessed: an invalid code is not quietly mapped to a near neighbour. selftest: mail.codes
  • Every single wrong character is caught. A position-weighted checksum modulo the prime 31 was measured at 0 misses in 4,060,000 substitutions. selftest: mail.codes
  • Every swapped adjacent pair is caught. The same test caught all adjacent transpositions; standard Luhn mod 30 missed 47 adjacent swaps on the same data. selftest: mail.codes

Why modulo a prime beats a plain sum

A checksum that returns the same value for two different mistakes is worse than no checksum, because it fails quietly. Weighting each character by its position and reducing modulo a prime pushes adjacent transpositions — AB becoming BA — onto different residues, which is exactly the case a plain unweighted sum misses.

The practical result is a code you can read to a caller and confirm in one step: short, unambiguous, and carrying one check character that has to agree. selftest: mail.codes

Alphabet and checksum are the same in this page and in the MCP tool, so a code printed from the browser reads back correctly to an agent running mail_codes_assign.

QR

Do QR codes on postcards actually work?

The symbol is verifiable. The scan rate for your list is a different question, and this page does not claim one.

The symbol decodes, and that is tested

AdRails' encoder was checked against an independent decoder (OpenCV): 557 of 557 test symbols decoded to the exact input, and the harness's control read 40 of 40 codes from an established encoder. Versions 1 to 10, error correction L, M, Q and H, all eight masks. selftest: mail.qr

Print-exact beats approximately right

A QR that prints 4% small still scans on a phone held close. A label that prints 4% off the pitch does not fit the die-cut. AdRails writes Avery 5160/8160, 5163, 5164 and 5167 sheets as HTML that prints to PDF from any browser, with an optional QR on every label and a vendor CSV behind a CSV-injection guard. The SAMPLE watermark is on by default; a real print run is written to the audit chain. selftest: mail.labels

Where QR goes wrong

  • Error correction is a trade. Level H survives more damage and holds far fewer characters than L. A long tracking URL with UTM tags can overflow the encoder at H, so this tool reports the failure instead of printing an unscannable symbol.
  • A screen preview is not a label. Scan the first print run with the phones you actually expect, on paper, before the full run goes out.
  • Scans are not readers. A scan proves a phone opened the URL, not that a household read the card.

The label sheet this page downloads places 30 labels per US Letter page at 2.625 by 1 inch, top 0.5 inch, left 0.1875 inch, horizontal pitch 2.75 inch, with a 0.8 inch QR on the right of each label.

LIMITS

What does this page not do?

It generates codes, URLs, symbols and printable sheets. Everything after the printer belongs to other tools.

Three things it leaves alone

  • It does not clean the list. USPS abbreviations, ZIP+4, foreign and incomplete addresses, institutional owners, your own properties, clients and do-not-mail lists are handled by mail_list_clean, and labels wrap rather than truncate a long address. selftest: mail.hygiene
  • It does not count scans. Scan counts come from your own access logs via mail_scans, which also drops hits logged before a piece was mailed. selftest: mail.scans
  • It does not send anything. Nothing in the direct-mail tools sends mail, buys postage or contacts anyone.

What stays in the browser

Up to 500 pieces per run. Codes are generated locally, so closing the tab discards them; download the manifest before you leave. Uploaded CSVs are parsed in memory only, and a column that is empty or unrecognised simply produces a shorter address block on the label rather than a guessed one.

AGENTS

Can an agent run this for a whole campaign?

Yes, through the AdRails MCP server, which runs on your machine with your credentials.

The same job, on the server

  • mail_codes_assign. Unique codes from the same alphabet and check character, plus tracking URLs with UTM tags, written out as a CSV ready for a mail house. selftest: mail.codes
  • mail_labels. Print-exact Avery sheets as HTML with an optional QR per label, a vendor CSV, a SAMPLE watermark by default and an audit-chain entry for a real run. selftest: mail.labels
  • mail_scans. Unique human scans per code from your own logs, first-scan delay, and scan rate by batch or segment. selftest: mail.scans

Where it fits

Direct mail is one channel on the same ruler as paid search: the same caps file, the same audit chain, the same plan-before-apply rails. Read the direct mail overview for the full 10-tool category, or browse all 40 tools on the server.

AdRails is Apache-2.0 and runs locally over Python 3.11+ with the standard library; nothing is proxied through a hosted service.

PRIVACY

Does anything leave my browser?

No upload, no server round trip, no stored copy.

Local by construction

Codes, tracking URLs, QR symbols, the manifest CSV, the Avery 5160 sheet and the QR page are all built with JavaScript in this tab. Nothing is posted anywhere, and nothing survives closing it. The same rule runs through the direct-mail tools: visitor ids are hashed rather than stored as raw IPs, PII is hashed locally before any upload file is built, and no tool sends mail, buys postage or contacts anyone.

Check the code you run

AdRails ships a CycloneDX 1.5 SBOM and a SHA-256 manifest of every package file, so the build you audit is the build you execute. selftest: sbom Source is at github.com/antonioac1/adrails.

FAQ

Straight answers.

How do I track who responds to a postcard?

Print a unique code on every piece and point its QR code at your landing page with ?c=CODE appended. A scan, or a code read back over the phone, resolves to exactly one mail piece. AdRails counts unique human scans from your own web-server logs and drops bots, link previewers, your own IPs, unknown codes and any hit logged before that piece was mailed.

Why do the tracking codes skip 0, 1, I, L, O and U?

Those six characters are the ones people confuse when reading a code aloud or copying it from a label: 0 with O, 1 with I and L. Leaving them out removes the ambiguity before it happens. A position-weighted check character then catches the remaining mistakes: every single wrong character and every swapped adjacent pair in a 4,060,000-substitution test.

Do QR codes on postcards work?

The symbol side is proven: AdRails' encoder was verified by an independent decoder, with 557 of 557 test symbols decoding to the exact input and a control reading 40 of 40 codes from an established encoder. Whether your list scans them is a separate question with no honest universal answer, so test-scan your first print run with the phones you expect.

What label sheet does this page produce?

Avery 5160, 30 labels per US Letter page. Each label is 2.625 by 1 inch, the first sits 0.5 inch from the top and 0.1875 inch from the left, and the horizontal pitch is 2.75 inches. The download is an HTML file with @page size letter and zero margin, so any browser prints it to PDF at exact size.

Is anything uploaded when I generate codes?

No. Codes, tracking URLs, QR symbols, the manifest CSV, the label sheet and the QR page are all built in your browser with JavaScript. Nothing is sent to AdRails or anyone else, and nothing is kept after you close the tab. AdRails' direct-mail tools follow the same rule: nothing sends mail, buys postage or contacts anyone on your list.

Do I need the AdRails MCP server to use this page?

No, the page runs on its own. The server adds the parts a browser cannot do: cleaning the list with mail_list_clean, assigning codes with the same alphabet and check character in mail_codes_assign, print-exact label sheets with an audit record in mail_labels, and real scan counts from your access logs in mail_scans.