Pricing Docs

Sign in to your dashboard to get a pre-filled config block with your licence key, public key, and analytics endpoint.

Go to dashboard →

Parselo — Quickstart

Scan Canadian driver’s licences and ID cards in your Capacitor app. The SDK decodes the PDF417 barcode on the back of the card on-device and returns the parsed fields (name, date of birth, document number, address, expiry). No document data leaves the device, and it works offline.

This takes about five minutes. Any Capacitor app works — Vue, React, Angular, or vanilla.

Before you start

You’ll need:

  • A Capacitor app targeting iOS and/or Android.
  • Your integration config from the dashboard — a license key, a public key, and an analytics endpoint. The dashboard’s Integration panel gives you a ready-to-paste block with all three filled in.
  • Your app’s bundle id (iOS bundle identifier / Android applicationId). Your key is bound to it — enter it when you request the key.

1. Install

npm install @parselo/scanner-core @parselo/capacitor-pdf417 @parselo/capacitor-mrz \
  @capacitor/camera @capacitor/preferences @capacitor/app
npx cap sync

What each package is for: scanner-core (parsing + licensing), capacitor-pdf417 (native barcode decode), capacitor-mrz (native MRZ text recognition), @capacitor/camera (capture), @capacitor/preferences (offline analytics buffer), @capacitor/app (reads your bundle id at runtime). npx cap sync compiles the native plugins into your iOS and Android projects.

If you only need driver’s licence scanning and not passport, you can omit @parselo/capacitor-mrz — the barcode path is independent.

2. Add camera permission

  • iOS — add to ios/App/App/Info.plist:
    <key>NSCameraUsageDescription</key>
    <string>Used to scan the barcode on your ID.</string>
    
  • Android — the camera permission is included by the plugin; nothing to add for a standard setup.

3. Create the scanner

Paste the token, publicKeys, and ingestUrl from your dashboard’s Integration panel into the marked spots. Everything else is ready as-is.

import { Scanner } from "@parselo/scanner-core";
import { Pdf417 } from "@parselo/capacitor-pdf417";
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
import { Preferences } from "@capacitor/preferences";
import { App } from "@capacitor/app";

export async function createScanner() {
  const { id: bundleId } = await App.getInfo();   // your app's real bundle id

  return new Scanner({
    license: {
      // — paste from your dashboard ———————————————————————————————————————
      token: "{{LICENSE_KEY}}",
      publicKeys: { "{{KID}}": "{{PUBLIC_KEY}}" },
      // ——————————————————————————————————————————————————————————————————
      bundleId,                       // must match the key's bundle id
    },
    analytics: {
      ingestUrl: "{{INGEST_URL}}",   // from your dashboard
      store: {
        get: async (k) => (await Preferences.get({ key: k })).value,
        set: async (k, v) => { await Preferences.set({ key: k, value: v }); },
      },
    },
    native: {
      captureAndDecodePdf417: async () => {
        const photo = await Camera.getPhoto({
          resultType: CameraResultType.Uri,
          source: CameraSource.Camera,
          quality: 100,               // PDF417 is dense — keep the photo sharp
        });
        const { raw } = await Pdf417.decodePdf417({ image: photo.path! });
        return raw;                   // null if no barcode was found
      },
    },
  });
}

The token is yours; publicKeys and ingestUrl are the same for every customer (they point at Parselo’s verification key and analytics endpoint).

4. Verify the licence, then scan

Driver’s licence / ID card

const scanner = await createScanner();

const license = await scanner.init();    // verifies your key offline, once
if (!license.valid) {
  // license.reason: "expired" | "bundle_mismatch" | "bad_signature"
  //               | "unknown_key" | "malformed"
  console.error("Licence not valid:", license.reason);
  return;
}

const result = await scanner.scan();      // opens the camera, decodes, parses
if (result.ok && result.document) {
  const f = result.document.fields;
  console.log("Name:", f.firstName?.value, f.lastName?.value);
  console.log("DOB:", f.dateOfBirth?.value);          // YYYY-MM-DD
  console.log("Doc #:", f.documentNumber?.value);
  console.log("Expires:", f.expiryDate?.value);       // YYYY-MM-DD
  console.log("Jurisdiction:", result.document.jurisdiction);  // e.g. "CA-QC"
} else {
  // result.error: "no_barcode" | "not_aamva" | "license_expired"
  //             | "license_bundle_mismatch" | "license_bad_signature"
  //             | "license_unknown_key" | "license_malformed"
  console.warn("Scan failed:", result.error);
}

That’s the barcode integration. init() once, then scan() whenever you need a scan.

Passport / travel document (MRZ)

Add @parselo/capacitor-mrz to your scanner config to unlock passport scanning. The same Scanner instance handles both document types — only the method you call changes.

import { Mrz } from "@parselo/capacitor-mrz";

// Add mrzNative alongside native when constructing the scanner:
return new Scanner({
  license: { … },
  analytics: { … },
  native: { captureAndDecodePdf417: … },   // barcode path unchanged
  mrzNative: {
    captureAndRecognizeMrz: async () => {
      // Capture a full camera frame and pass it to the MRZ plugin.
      // The plugin runs Vision / ML Kit on the entire image and returns
      // every recognised text observation as an array of strings.
      const { lines } = await Mrz.recognizeText({ image: dataUrl });
      return lines;
    },
  },
});

Then call scanPassport() instead of scan():

const result = await scanner.scanPassport();
if (result.ok && result.document) {
  const f = result.document.fields;
  console.log("Name:", f.firstName?.value, f.lastName?.value);
  console.log("DOB:", f.dateOfBirth?.value);           // YYYY-MM-DD
  console.log("Expiry:", f.expiryDate?.value);         // YYYY-MM-DD
  console.log("Country:", f.country?.value);           // ICAO 3-char, e.g. "CAN", "MEX"
  console.log("Jurisdiction:", result.document.jurisdiction);  // e.g. "CA", "MEX"
} else {
  // result.error: "no_mrz" if no valid MRZ was found
}

Supported document types: standard passports (P), emergency travel documents (PU), permanent resident travel documents (PR), and foreign passports of any country.

How name extraction works: The OCR-B MRZ font uses < as a word separator, which Vision and ML Kit frequently misread as C, S, K, etc. The SDK automatically cross-references Vision’s biographical-zone lines (the human-readable area above the MRZ) to recover the correct first and last names, even when the MRZ separator characters are corrupted.

Camera UI: Point the camera at the full data page of the passport (the page with the photo). The MRZ zone — two rows of monospace characters — is at the bottom. Hold the document flat, well-lit, and filling the frame. The SDK samples frames at 800 ms intervals and pre-validates each frame before accepting it, so brief focus wobble doesn’t produce a false result.

What you get back

scan() resolves to { ok, document?, error? }. On success, document is a CanonicalDocument:

PropertyDescription
documentType"drivers_license", "id_card", "passport", or "unknown"
jurisdictionIssuing place, e.g. "CA-QC", "CA-ON"
fieldsThe parsed fields (below). Each is { value, confidence, source }
rawEvery raw AAMVA element by 3-letter code, for advanced use

Available fields (each optional, present when the card carries it): firstName, lastName, middleName, dateOfBirth, expiryDate, issueDate, documentNumber, sex, addressStreet, addressCity, addressRegion, addressPostalCode, country, vehicleClass.

Notes: dates are ISO YYYY-MM-DD. confidence is ~1.0 for barcode data (it’s error-corrected, so it’s exact, not a guess). source is "barcode".

Usage analytics

The SDK records one PII-free event per scan — counts and metadata only, never document data — buffers them locally, and flushes in batches. They power the usage view in your dashboard. To push promptly (e.g. when the app goes to the background), call:

await scanner.flushAnalytics();

Because events buffer offline and sync when there’s a connection, dashboard usage can lag slightly behind the device.

The one thing to get right: the bundle id

Your key is bound to your app’s bundle id. The SDK reads the running app’s bundle id at runtime (App.getInfo().id), so the only way to hit bundle_mismatch is if the key was issued for a different id than the app you’re running. If that happens, check that the bundle id you entered in the dashboard matches your app exactly. Shipping more than one app? Add each bundle id in the dashboard and you’ll get a key valid for all of them.

Test on a real device

A simulator or emulator has no camera and can’t scan a physical card — test on real hardware. Photograph the back of the licence (the PDF417 barcode, not the photo side), held flat, well-lit, and filling the frame. Almost every decode failure on a genuine card is photo quality, not the card.

Troubleshooting

You seeWhyFix
init()bundle_mismatchApp’s bundle id isn’t on the keyMatch the bundle id, or add it in the dashboard
init()expiredTrial or key has expiredIssue a fresh key in the dashboard, or upgrade
init()bad_signature / unknown_keytoken/publicKeys don’t matchRe-copy the config block from the dashboard
scan()no_barcodeNo PDF417 found in the photoShoot the back of the card, sharper / flatter / closer
scan()not_aamvaA barcode was read, but it isn’t an IDScan a Canadian/US driver’s licence or ID card
scanPassport()no_mrzNo valid MRZ found after camera sessionPoint at the data page (photo side), well-lit, flat, not at an angle
MRZ plugin unrecognised (no Xcode logs)ObjC bridge file missing from build targetVerify MrzPlugin.m (or Pdf417Plugin.m) is in Xcode Compile Sources; re-run pod install
Names show garbled characters on passportMRZ < fill chars misread by OCRUsually self-corrects via VIZ cross-reference; if persistent, re-scan with better lighting
recognizeText / decodePdf417 throws “native-only”Running in a browserBoth plugins are native-only — run on a real device
Plugin not found after installNative projects not syncedRe-run npx cap sync then rebuild in Xcode / Android Studio

Going to production

When your trial ends, upgrade in the dashboard and you’ll get a production key — just swap the token, the code is identical. Add more bundle ids for additional apps; billing is based on total scan volume across them.

Privacy

Parsing happens entirely on the device, and the analytics this SDK sends contain only counts and metadata — never names, dates of birth, document numbers, or addresses. That keeps document data off the wire and supports your PIPEDA / Law 25 posture.