Skip to content

Extracting Structured Fields From a Conference Badge Scan Export

9 min read · updated August 11, 2026

Badge scanning looks like the one document problem with no document in it. Then a badge gets reprinted, the code on it resolves to one person and the name printed above it belongs to another, and you discover that your record had been quietly choosing between them all along.

It is an export, not a scan

The artefact that reaches your pipeline after a trade show is almost never an image. It is a file the lead-retrieval vendor produced: a CSV, an XLSX, occasionally a JSON payload from an API, and sometimes — irritatingly — a PDF report laid out for printing. The scanning happened on the show floor and is already over.

That changes what the extraction problem is. There is no reading order to recover and no OCR to run in the common case; the difficulty is that every vendor names its columns differently and every show produces a different subset of them. One export has First Name, Last Name, Company, Scan Date/Time, Rep, Qualifier 1..5. Another has attendee_first, attendee_last, org, scanned_at_utc, device_id, notes. A third exports one row per qualifier answer rather than one row per lead, so the record count is four times the lead count.

A language model is genuinely good at the header-mapping half of this — give it the header row and your target schema and ask for a column mapping, once per export, then apply that mapping in code to all nine thousand rows. Running the model over every row is the expensive way to get a worse answer, since a per-row read can map the same column inconsistently across the file. Extract the mapping; transform the data. Where the vendor only gives a PDF, that is when ordinary PDF table extraction applies, and the rest of this page applies on top of it.

What is actually in the QR code

Badge codes are not standardised across the industry, and the four patterns you will meet behave very differently:

  • An opaque registration identifier — a short string such as R-4471-88. Carries no personal data at all. The scanning app resolves it against the registration system, and every attribute on the lead row came from that lookup rather than from the badge.
  • A URL with a token pointing at the registration provider. Same thing with a redirect in front of it, and the token frequently expires after the show.
  • An embedded vCard with name, organisation, title and email encoded directly. Self-contained, and therefore frozen at the moment the badge was printed.
  • A delimited or base64 blob of the registration provider’s own devising — pipe-separated fields, often with trailing empty positions whose meaning is undocumented.

The distinction that matters is whether the payload is a reference or a copy. A reference is resolved at scan time and reflects the registration record as it stood then. A copy reflects the record as it stood when the badge was printed, which may be weeks earlier. Store which kind you got; it is the only thing that explains a later disagreement.

The reprint, and why the two disagree

Badges are reprinted constantly at registration desks: a misspelled name, a changed job title, a lost badge, or — the case that actually breaks things — a transfer, where one company sends a different employee than the one registered and the desk prints a new badge without creating a new registration record.

After a transfer with a lazy reprint, one physical badge can carry a printed name of one person over a code that still resolves to another. Your booth staffer sees the printed name, may type it into the notes field, and the vendor export carries both:

registration_id      R-4471-88
name_from_lookup     Dana Okonkwo        (synthetic example)
name_as_printed      Priya Raman         (synthetic example)
company              Northwind Analytics
scanned_at           2026-05-13 14:22 (show local time, no offset in the file)

There is no way to determine from the export alone which human stood at your booth. Both values are true statements about different things: one is who the registration belongs to, the other is what the badge said. The mistake is not failing to resolve it — it is unresolvable — the mistake is picking one and discarding the other, because that turns a visible conflict into an invisible wrong answer.

Which source the schema trusts

Make the registration identifier the primary key and everything else a claim with a provenance tag:

{
  "registration_id": "R-4471-88",
  "identity": {
    "value": "Dana Okonkwo",
    "source": "registration_lookup",
    "conflicting": [
      { "value": "Priya Raman", "source": "badge_print" }
    ]
  },
  "payload_kind": "reference",
  "scans": [
    { "at": "2026-05-13T14:22:00-05:00", "rep": "device-07", "qualifiers": ["demo-requested"] }
  ]
}

The registration identifier is authoritative for identity of record because it is the only value that is stable, unique and resolvable — a name is none of those. The printed name is authoritative for who your staffer spoke to, and it is the field a salesperson will recognise. Keeping both, with conflicting populated only when they differ, means the conflict shows up in exactly the records where it is real. Where the only artefact is a photograph of a badge and there is no code at all, the same schema still works: registration_id is null and source is badge_print, which is honest about what you have.

Set the merge rule accordingly: never overwrite an identity of record with a printed name, and never suppress a printed name because it disagrees. This is a specific instance of a general habit worth building, which is that an extraction record should be able to represent disagreement between two sources rather than resolve it prematurely — see designing a schema that survives a multi-entity document.

Duplicates, timestamps and encoding

  • The same person scanned four times. Three booth staff and a re-scan at the theatre session. Dedupe on registration identifier, keep the earliest scan as first contact, and take the union of qualifier codes and the concatenation of notes — collapsing to the last scan throws away the qualifier that the first staffer recorded. Deduplicating on name instead will merge two genuinely different attendees who share a common name and split one attendee whose name was captured with different punctuation.
  • Timestamps with no offset. Most exports write show local time as a naked string. A three-day show in one city is fine once you attach the venue’s zone; a show that spans a daylight-saving transition produces an hour that is either ambiguous or nonexistent, and a sort by scan time silently reorders across it. Store the naked string as-is, plus the zone you applied, plus the derived instant. Three fields, and only the first is data you were given.
  • Mojibake. Vendor exports are frequently Windows-1252 or Latin-1 with no declaration, so a name containing an accented character arrives as a two-character sequence when read as UTF-8. It is worth detecting at ingestion, because once the corrupted form is in your CRM it is indistinguishable from a genuinely odd spelling, and no amount of model quality downstream will fix a byte that was decoded wrong at the door.
  • Qualifier codes with no legend. Columns of Q1, Q2, Q3 containing values like A and C. The legend is in the show’s configuration, not in the export, and it changes per show. Store the raw codes and the show identifier rather than mapping to meanings you cannot verify.
  • The lawful basis travels with the record. A badge scan is a transfer of personal data from the organiser to you under terms the attendee agreed to at registration, and those terms differ by show and by jurisdiction. Keep the show identifier and the organiser’s consent artefact attached to the leads so retention and objection handling can operate on them later; the record-of-processing page covers what that documentation needs to contain.