A CSV or Excel file with one row per attendee. Headers get matched automatically; fix anything it guesses wrong below.
Each mapped column becomes a field on the contact card. Leave a field unset to keep it off the badge entirely.
Load a file to map its columns.
Nothing loaded yet.
What actually gets printed. Point a phone camera at it to test.
A zip of PNGs plus a manifest is what a badge printer needs for a variable-data merge.
Every attendee's code. Click one to load it into the proof.
The honest version, because it decides the architecture.
A direct vCard QR cannot be tracked. The phone decodes the card locally and never touches a network. No server sees it, so there is nothing to count. That mode is fast and works with no infrastructure — but it is invisible.
A link QR is trackable because every scan is an HTTP request you own. That is the only reliable way to get a number. It costs you a domain, a tiny endpoint, and a dependency on the venue's connectivity.
The numbers below are a live demo of the data model. In link mode your endpoint writes these rows; here you can seed them to see the shape. Simulated rows are marked.
Sorted by scans. In a real event this is the sponsor-facing number.
Most recent 100 events.
Everything in the prototype except the scan endpoint runs in the browser. The endpoint is the only piece you have to host, and it is small.
One Worker, two routes: log the view and show a card page; log the save and hand over the contact. The whole user experience lives in how that second response is shaped, and it differs by platform.
// iPhone: serve text/vcard INLINE. Safari renders a contact sheet
// with "Create New Contact" — one tap. Do NOT add Content-Disposition.
return new Response(rec.vcard, {
headers: { "content-type": "text/vcard; charset=utf-8", "cache-control": "no-store" }
});
// Android Chrome: skip the file entirely. An intent: URL opens the
// Contacts app with the fields already filled in — one tap to Save.
const href = "intent:#Intent;action=android.intent.action.INSERT;"
+ "type=vnd.android.cursor.dir/contact;"
+ "S.name=" + encodeURIComponent(rec.fn) + ";"
+ "S.phone=" + encodeURIComponent(rec.phone) + ";"
+ "S.email=" + encodeURIComponent(rec.email) + ";"
+ "S.company=" + encodeURIComponent(rec.org) + ";"
+ "S.browser_fallback_url=" + encodeURIComponent("/" + slug + ".vcf?dl=1")
+ ";end";
// Everything else: Web Share API with the .vcf, so Contacts shows up
// as a share target. Plain download is the last resort.
The mistake that produces a bad experience: setting Content-Disposition: attachment on the vCard route. That turns iOS's one-tap contact sheet into a file the person has to hunt down in Downloads. The complete Worker with all three paths, KV tracking and a stats page ships alongside this kit; load it with Export for Worker on the Codes tab.
Sequential ids let anyone walk the whole roster by incrementing a number, which turns 300 badges into a scrapeable contact database. Use a random slug per attendee — this prototype generates 6-character ones. Keep the mapping server-side and never put the attendee's email in the URL.