Build a business card scanner that writes to your CRM
Front and back of a card in one job returns E.164 phones, ISO country codes and a decoded QR. Then one POST to your CRM. Here is the whole pipeline.
5 min readA business card is a badly designed data entry form. The phone number has spaces in it, or dots, or a leading zero and no country code. The email is set in a font where a lowercase L and a capital I are the same glyph. Half of them now carry a QR code holding all of this in machine readable form, and almost nobody reads it.
Getting a card into a CRM is one extraction job and one POST. Most of the cleanup happens before the result reaches you.
This closes a short series on consumer apps that are mostly one API call: a meal photo calorie tracker, a receipt scanner and bill splitter, and a barcode pantry tracker.
The pipeline
- One job. Front and back of the card in a single request, typed contact fields back.
- Your CRM. One authenticated POST to whichever API you already use.
Two photos, one job
sources is an array and everything in it is read as one document set, so the back of the card is not a second job. Send both sides and the fields are filled from whichever side carries them.
Notice what you did not have to write.
The phone is E.164. A card printed in Lisbon says 21 345 67 89 and means +351213456789. Completing that needs the country, which is not in the number, so the extraction reads it off the card itself: the address, the language, the domain suffix. That is a perception task, which is the model's job. The formatting that follows is mechanical, and it runs in code.
The country is ISO 3166-1 alpha-2. PT, not "Portugal" and not "PRT".
The email is canonical lowercase, so Ana.Silva@Cordoval.PT and ana.silva@cordoval.pt do not become two contacts.
The QR is decoded, not read. Where a card carries a vCard or a LinkedIn URL in a QR code, a real decoder resolves it before extraction and the payload lands in card_qr byte for byte. If there is no code, the field is null rather than a plausible looking URL.
One rule sits behind all of those, and it decides how far you can trust the output: resolve or leave alone, never fabricate. A phone number that cannot be completed to E.164 comes back exactly as printed. A country word with no mapping stays the word. Anything that arrives raw is visible in your data and can be routed to a human, while a quietly guessed value cannot.
The same holds for absence. A card with no address gives you null, which is why required in that schema has exactly one entry. Everything you mark required is pressure to produce a value, so reserve it for what is genuinely always there.
The POST
Whatever CRM you use, this is the boring half. HubSpot as an example:
That name split is deliberately naive, and it is wrong for compound surnames, for patronymics and for most of the world. Splitting a name is a policy decision rather than a parsing one, so either ask the schema for given_name and family_name separately, or show the split on a confirmation screen and let the person fix it.
What you still have to build
Deduplication. Everyone at a conference hands out the same card twice. Match on canonical email first, then on phone, and merge rather than insert. This is the feature that decides whether the CRM stays usable.
A confirmation screen. One card, one glance, editable fields. Nobody trusts silent writes into their own CRM, and nobody should.
The CRM auth. OAuth, token refresh, per user connections, rate limits on their side. This will take longer than the extraction did.
The empty paths. Job failed, an unreadable photo, a card that is genuinely a logo and a first name. A failed job refunds its hold in full, so the retry costs you nothing, but the user still needs to be told something.
Cost and shape
POST /v1/jobs/create returns immediately with an id and status: "pending", and the result arrives on your webhook_url or through GET /v1/jobs/:jobId/get. Two images in sources is still one job.
The number we publish is measured: $5.63 per 1,000 pages on vera-1.0-high across the full 1,651 page OmniDocBench run. That is whole page transcription, the heaviest output there is. A business card against ten fields is nowhere near it, because output size is what drives the bill.
Start here
The quickstart runs the first card in a few minutes, schemas and formats is the full format table, and verified values covers what gets checked rather than merely formatted.