Document-capture package
@emnet-id/document-capture-js is the device-side half of document data capture: it checks a photo, encrypts it on the device to Emnet’s pinned upload key, and uploads only the ciphertext with your publishable key.
The package runs in browsers and React Native apps, so it holds only a publishable key and the capture’s evidence token. Starting a capture and fetching the fields stay on your backend with the Java SDK (startDocumentExtraction, fetchExtractionFields). The Java SDK’s uploadExtractionEvidence only forwards an envelope that was already built on a device; it never encrypts an image.
The package and document data capture are in early access until capture is generally available. The examples show a LIVE key to illustrate the contract.
Install
The package, @emnet-id/document-capture-js, is distributed with your early-access key, together with its install instructions. Request a key by writing to sales@emnetid.com.
The only Emnet device package is @emnet-id/document-capture-js. The emnet-id organisation on npm is Emnet’s; no other scope or name is — do not install a look-alike. During early access, install the package only from the instructions issued with your early-access key.
Commit your lockfile. The upload public keys are pinned in the package version, so the lockfile is what pins them in your app build; when Emnet rotates its upload key you update the package and release your app. The package has no runtime dependencies, is ESM-only and ships TypeScript types.
Usage
Your backend starts the capture and passes the Start response to the page or app. The package never calls Start: browsers cannot (there is no cross-origin access to it), and a native app that starts the capture itself still hands the response in.
import { createUploader, EmnetCaptureError, retakeMessage } from '@emnet-id/document-capture-js'
// start: the 201 body of startDocumentExtraction, from your backend
const uploader = await createUploader({
baseUrl: EMNET_BASE_URL, // the https base URL issued with your key
publishableKey: PUBLISHABLE_KEY, // pk_live_… — never a secret key
environment: 'LIVE', // what this build targets
start,
})
try {
for (const part of uploader.requiredEvidence) {
const jpeg = await capturePhoto(part) // Uint8Array of a JPEG or PNG
await uploader.encryptAndUpload(part, jpeg)
}
await notifyBackend(uploader.extractionHandle) // the backend fetches the fields
} catch (e) {
if (e instanceof EmnetCaptureError && e.retake) {
showMessage('Please take the photo again.')
} else {
throw e
}
}
// A retake hint from your backend's fields call:
showMessage(retakeMessage(hint))What it checks before sending anything
| Code | When |
|---|---|
SECRET_KEY_REFUSED | A secret key was passed. |
ENVIRONMENT_MISMATCH | The environment you declare, the key prefix (pk_test_ / pk_live_) and the Start response disagree. |
NO_PINNED_UPLOAD_KEY | None of the Start response’s uploadKeyThumbprints is pinned in this package version — update the package. Nothing is uploaded. |
LIVE_NOT_PROVISIONED | This package version pins no LIVE upload key yet. |
INVALID_START_RESPONSE | The Start response is malformed. |
CRYPTO_UNAVAILABLE | No usable WebCrypto implementation (see React Native below). |
HEIC_NOT_SUPPORTED | The photo is HEIC/HEIF — export or convert it to JPEG on the device first. |
UNSUPPORTED_IMAGE_FORMAT, IMAGE_TOO_SMALL, IMAGE_TOO_LARGE, IMAGE_OVER_BYTE_CEILING | Not JPEG/PNG, under 200 px or over 6000 px on a side, over 25 megapixels, or over 8 MiB. |
PART_NOT_ACCEPTED, PART_ALREADY_UPLOADED | A part outside acceptedEvidence, or a second upload of one. |
Server answers map to UNAUTHORIZED, NOT_FOUND, EVIDENCE_ALREADY_RECEIVED, PAYLOAD_TOO_LARGE, EVIDENCE_UNREADABLE, RATE_LIMITED, CAPTURE_DISABLED and EXTRACTION_UNAVAILABLE; anything else is REQUEST_FAILED. Error messages are constant and safe to log: they never carry the evidence token, a key, the image or the envelope. validateImage(bytes) runs the photo checks on their own.
uploader.requiredEvidence lists the parts that must be uploaded; uploader.acceptedEvidence lists every part the upload admits, which adds any optional one (the back of a kebele ID, for example). uploader.requiredEvidenceUploaded turns true once this uploader has sent every required part. Treat it as a hint for your UI: your backend’s fields call is what decides whether the capture is complete.
Verification sessions
The same uploader delivers evidence to a document verification session. Pass the session’s Start response (it carries sessionId and a ds_ session handle instead of an extraction handle); the package recognises it and uploads to the session — you never pass a path. uploader.kind is 'CAPTURE' or 'VERIFICATION'. A Start response that mixes the two shapes is refused before anything is sent. If your backend’s completion call answers with a retake, create a new uploader from the same Start response and upload every part again.
How it encrypts
Each part becomes a JWE with ECDH-ES+A256KW on P-256 and A256GCM, a header of exactly alg, enc, kid and epk, and a fresh ephemeral key, content key and IV from the platform’s secure random generator for every part — the evidence envelope. The package encrypts to the first pinned key whose thumbprint is in the Start response’s uploadKeyThumbprints; it never fetches a key and never takes one from a response.
The upload is a single multipart/form-data request built as a string, so it always carries a declared Content-Length (chunked uploads are refused). It never follows a redirect, sends no cookies and no referrer, and accepts only an https base URL.
Browsers
- Register each origin your app uploads from, per environment, with Emnet.
- Serve your app from your own origin and bundle the package into your build. The pin is only as trustworthy as the JavaScript that carries it, so never load the package from a CDN script tag or through Emnet’s edge.
- Browsers have the required WebCrypto support natively; nothing else is needed.
React Native
The package implements no cryptographic primitive itself. React Native needs a WebCrypto implementation backed by the platform’s native crypto, installed as the global crypto or passed as the crypto option:
crypto.subtlewith ECDH on P-256, AES-KW, AES-GCM and SHA-256;crypto.getRandomValuesbacked by the operating system’s secure random generator.
A random-values-only module is not enough, and a pure-JavaScript or Math.random-based polyfill is never acceptable. Before first use the package runs known-answer tests on the provider and refuses to send anything if one fails; those tests prove the provider computes correctly but cannot prove its random generator is secure, which is why it must be a native one. The providers we accept are listed in your onboarding pack.
What it never does
- Store anything — no local storage, IndexedDB, AsyncStorage or cookies.
- Log anything — printing an uploader shows only that a token and a key are present.
- Keep a copy of your photo after the upload returns.
- Hold or use a secret key, or fetch fields.