@validpay/react-native-sdk
v1.2.0
Published
ValidPay verification SDK for React Native — 8 patented protections for document authenticity
Maintainers
Readme
@validpay/react-native-sdk
Official React Native SDK for the ValidPay document verification API. AES-256-GCM client-side encryption, eight patented protections, and zero runtime dependencies.
The wire format is byte-compatible with the Python SDK and the Node SDK — an intent created with any ValidPay SDK can be verified with any other.
Installation
npm install @validpay/react-native-sdkSetup — pluggable crypto backend
React Native does not include Web Crypto. Apps choose their own AES-256-GCM
implementation (expo-crypto, react-native-quick-crypto, Web Crypto on
RN-future, etc.) and inject it once at startup. The SDK uses your
implementation everywhere — there's no native module bundled.
import { configure, CryptoProvider } from '@validpay/react-native-sdk';
import * as ExpoCrypto from 'expo-crypto';
class ExpoCryptoProvider implements CryptoProvider {
async getRandomBytes(length: number): Promise<Uint8Array> {
return ExpoCrypto.getRandomBytes(length);
}
async encrypt(plaintext: Uint8Array, key: Uint8Array): Promise<string> {
// AES-256-GCM with a fresh 12-byte IV. Return base64 string of:
// iv (12 bytes) || authTag (16 bytes) || ciphertext
// ...consult expo-crypto / react-native-quick-crypto docs for the call.
}
async decrypt(ciphertext: string, key: Uint8Array): Promise<Uint8Array> { /* … */ }
async sha256(data: Uint8Array): Promise<string> {
return ExpoCrypto.digestStringAsync(
ExpoCrypto.CryptoDigestAlgorithm.SHA256,
Buffer.from(data).toString('base64'),
{ encoding: ExpoCrypto.CryptoEncoding.HEX },
);
}
}
configure({ crypto: new ExpoCryptoProvider() });If you use splitKey(), also import a synchronous random-bytes polyfill
once at app entry:
import 'react-native-get-random-values';This provides globalThis.crypto.getRandomValues, which splitKey() needs
synchronously.
Quick start
import { ValidPayClient } from '@validpay/react-native-sdk';
const client = new ValidPayClient({ apiKey: 'vp_live_…' });
// Issue
const result = await client.createIntent('check', {
payee: 'Alice',
amount: 1500,
});
console.log(result.retrievalId, result.key);
// → vp_abc123… (embed in QR)
// → base64 AES key — print on document, never store
// Verify (no API key required)
const verified = await client.verifyIntent(result.retrievalId, result.key);
console.log(verified.payload); // { payee: 'Alice', amount: 1500 }
console.log(verified.integrityVerified); // trueFeatures
Eight patented protections, all live in this SDK:
| Patent | Feature | Method |
| ------ | ----------------------------- | --------------------------------------------------- |
| A | Multi-Hash Verification | computeCommitmentHash, automatic on verify |
| B | Blind Content Escrow | encrypt / decrypt (client-side AES-256-GCM) |
| C | Split-Key Verification | createSplitKeyIntent / verifySplitKeyIntent |
| D | Time-Locked Verification | validFrom / validUntil on all create methods |
| E | Selective Field Disclosure | createSelectiveIntent / verifySelectiveIntent |
| F | Chain-of-Custody Tracking | getRevocationHistory |
| G | Physical Medium Binding | computeBindingHash, createBoundIntent |
| H | Blind Revocation | revokeIntent / reinstateIntent |
API reference
new ValidPayClient(options)
| Option | Type | Default | Notes |
| ----------- | -------- | ----------------------------- | ---------------------------------------- |
| apiKey | string | required | Bearer key from the issuer dashboard. |
| baseUrl | string | https://api.validpay.com | Override for staging. |
| timeoutMs | number | 10000 | Per-request timeout (AbortController). |
Core methods
client.createIntent(documentType, payload, opts?): Promise<CreateIntentResult>
client.createIntentBatch(intents): Promise<CreateIntentResult[]> // ≤100 items
client.verifyIntent(retrievalId, key): Promise<VerifyIntentResult>Split-key (Patent C)
client.createSplitKeyIntent(documentType, payload, opts?): Promise<CreateIntentResult>
client.verifySplitKeyIntent(retrievalId, shareA): Promise<VerifyIntentResult>createSplitKeyIntent returns Share A as the key field — embed it on the
document as you would the regular key. Share B stays on the server and is
fetched at verify time.
Selective disclosure (Patent E)
client.createSelectiveIntent(
documentType,
payload, // { fieldName: value }
policy, // { roleName: ['fieldName', …] }
opts?, // { splitKey?, validFrom?, validUntil? }
): Promise<CreateIntentResult>
client.verifySelectiveIntent(
retrievalId,
key,
role = 'full', // 'full' decrypts everything
): Promise<VerifyIntentResult>A full role with every field key is added to the policy automatically —
that's the issuer view.
Time-locked verification (Patent D)
validFrom / validUntil (ISO-8601 strings) are accepted on every create
method. Every verify result includes timeLockStatus ('valid',
'not_yet_valid', 'expired', or null). The SDK never withholds the
payload — the caller decides whether to surface the status to the user.
const result = await client.createIntent('check', payload, {
validFrom: '2026-06-01T00:00:00Z',
validUntil: '2026-12-31T23:59:59Z',
});
const verified = await client.verifyIntent(result.retrievalId, result.key);
// verified.timeLockStatus → 'valid' | 'not_yet_valid' | 'expired'Revocation (Patent H)
client.revokeIntent(retrievalId, reason?): Promise<…>
client.reinstateIntent(retrievalId, reason?): Promise<…>
client.getRevocationHistory(retrievalId): Promise<Array<…>>Revocation is "blind" — the server flips a status bit and stops returning the ciphertext. It never decrypts the payload.
Physical medium binding (Patent G)
import { computeBindingHash, compareBindingHashes } from '@validpay/react-native-sdk';
// Pre-process the binding-zone image to 8×8 grayscale with your image lib
// (expo-image-manipulator, react-native-image-resizer, etc.) and pass the
// raw bytes — this SDK doesn't bundle a JPEG/PNG decoder.
const imageBytes = new Uint8Array(64); // 8×8 grayscale
const hash = await computeBindingHash(imageBytes);
const result = compareBindingHashes(hashA, hashB, /* threshold */ 10);
result.match; // boolean
result.distance; // Hamming distancecreateBoundIntent issues a binding-aware intent in one call:
const result = await client.createBoundIntent(
'check',
{ payee: 'Alice', amount: 1500 },
bindingZoneImage,
{ threshold: 10 },
);QR placement (buildVerifyUrl + resolveQrRect)
A verifiable document needs a scannable QR — encoding the verify URL — placed on it. This SDK gives you the canonical placement contract, shared verbatim with the Node and Python SDKs and the developer console's "Try it" tool, so a position picked once maps to the exact same spot everywhere.
import { buildVerifyUrl } from '@validpay/react-native-sdk';
import QRCode from 'react-native-qrcode-svg'; // your QR component of choice
const url = buildVerifyUrl(retrievalId, key); // key is base64url'd into the #fragment
<QRCode value={url} size={160} />;To stamp the QR onto a generated PDF, use the placement → points helper and hand
the result to your PDF library (or do it server-side with @validpay/node-sdk's
embedQr, which mints the PDF for you):
import { resolveQrRect, type QrPlacement } from '@validpay/react-native-sdk';
const placement: QrPlacement = { anchor: 'bottom-right', x: 36, y: 36, width: 90 };
const { x, y, size } = resolveQrRect(placement, pageWidthPt, pageHeightPt); // bottom-left pointsWhy no on-device
embedQr? Editing PDF bytes on a phone isn't practical, so React Native ships only the pure contract. Render the QR for display, or seal the PDF where you build it. Keep the QR ≥ 72pt (1in) so it scans once printed (MIN_RECOMMENDED_QR_PT).
QrPlacement fields: anchor (page corner — top-left/top-right/bottom-left/bottom-right, default top-left), x/y (insets from that corner), width (QR side), units (pt/mm/in, default pt), page (1-based).
Errors
Every SDK and API failure is thrown as ValidPayError:
import { ValidPayError } from '@validpay/react-native-sdk';
try {
await client.verifyIntent('vp_missing', 'key');
} catch (e) {
if (e instanceof ValidPayError) {
e.code; // 'not_found' | 'unauthorized' | 'integrity_failure' | …
e.status; // HTTP status, when the error came from the API
e.details; // raw error body / extra context
}
}Cross-SDK compatibility
The wire format is identical across the Python, Node, and React Native SDKs:
base64(iv[12 bytes] || authTag[16 bytes] || ciphertext)An intent created with validpay (Python) can be verified with this SDK,
and vice versa. Tests in __tests__/crypto.test.ts decrypt a hand-crafted
blob in the Python wire layout to enforce this guarantee.
License
MIT — see LICENSE.
