@kyciris/core
v1.1.1
Published
UI-agnostic client for the KYCiris identity verification API
Maintainers
Readme
@kyciris/core
The UI-agnostic client for the KYCiris identity
verification API. Zero runtime dependencies — it uses the platform's own
fetch, FormData and Blob, so it runs unchanged on Node 18+, in browsers,
and in React Native.
pnpm add @kyciris/coreQuickstart
Point everything at the sandbox gateway while you build — the URL is in your
onboarding details. Sandbox and production behave identically; only the data is
throwaway. (The interactive /docs reference is served by a gateway running in
development mode only, so it is off on the hosted sandbox — ask us for the
endpoint reference if you need more than this page.)
1. A token route on your backend
This is the one piece you have to write, and the reason is worth a sentence: your API key can reach every identity in the project, so it stays on your server. What the client gets is a token scoped to one end user.
// Express
import express from 'express';
import { createKycirisClient } from '@kyciris/core';
const kyciris = createKycirisClient({
baseUrl: process.env.KYCIRIS_BASE_URL, // your sandbox gateway
apiKey: process.env.KYCIRIS_API_KEY, // server-side only
});
app.post('/api/kyc/token', requireYourOwnAuth, async (req, res) => {
// `externalId` is YOUR id for this user. Take it from the session, never
// from the request body -- otherwise anyone can mint a token for anyone.
const { token } = await kyciris.verifications.createToken(req.user.id);
res.json({ token });
});// Next.js app router — app/api/kyc/token/route.ts
import { createKycirisClient } from '@kyciris/core';
const kyciris = createKycirisClient({
baseUrl: process.env.KYCIRIS_BASE_URL,
apiKey: process.env.KYCIRIS_API_KEY,
});
export async function POST() {
const user = await requireYourOwnAuth();
const { token } = await kyciris.verifications.createToken(user.id);
return Response.json({ token });
}2. The flow, in your app
// React (@kyciris/web) — the same shape in React Native (@kyciris/mobile)
import { KycirisVerification } from '@kyciris/web';
<KycirisVerification
baseUrl={process.env.NEXT_PUBLIC_KYCIRIS_BASE_URL}
verificationToken={() =>
fetch('/api/kyc/token')
.then((r) => r.json())
.then((b) => b.token)
}
country="AO"
documentType="ID_CARD"
levelName="basic-kyc"
onComplete={(state) => console.log(state.status, state.outcome)}
/>;Passing a function rather than a string means an expired token is re-fetched instead of failing every request from then on.
3. Learn the result from a webhook, not from polling
The component tells the user what happened. Your backend should hear it from a webhook, because a verification sent to a human reviewer can sit for hours:
await kyciris.webhooks.create({
url: 'https://your-app.example.com/hooks/kyciris',
eventTypes: ['*'], // every event, including ones added later
});The URL must be https with a public hostname — the gateway resolves it and
refuses anything private.
Checklist before you go live
- [ ] The API key is only ever read on the server (
process.env, neverNEXT_PUBLIC_*/EXPO_PUBLIC_*). The SDK throws if it reaches a client. - [ ]
externalIdcomes from your session, not from the request. - [ ] Your browser origin is in the gateway's
ALLOWED_ORIGINS, or every request fails CORS before it is even authenticated. - [ ] You branch on
outcome, notstatus(see below). - [ ] A webhook endpoint is registered and reachable.
Two credentials, two places
This is the part worth getting right.
| | apiKey | verificationToken |
| -------- | ---------------------------------------- | ---------------------------------- |
| Scope | The whole project — every identity in it | One end user's verification |
| Lives | On your server | In the app or page |
| Header | X-API-Key | x-verification-token |
| Lifetime | Until revoked | 10m by default; the gateway decides |
Constructing a client with apiKey inside a browser or React Native app
throws (CREDENTIAL_MISUSE). A key in a shipped bundle is a key the end
user has.
import { createKycirisClient } from '@kyciris/core';
// --- On your server
const server = createKycirisClient({
baseUrl: 'https://api.kyciris.com',
apiKey: process.env.KYCIRIS_API_KEY,
});
const { token } = await server.verifications.createToken('user-123');
// hand `token` to the client
// --- In the app
const client = createKycirisClient({
baseUrl: 'https://api.kyciris.com',
verificationToken: token,
});Pass a function as verificationToken to have it re-fetched when needed,
rather than failing every request once it expires:
createKycirisClient({
baseUrl,
verificationToken: () =>
fetch('/api/kyc/token')
.then((r) => r.json())
.then((b) => b.token),
});Running a verification
levelName is required and must name an active verification level of your
project: the level says which documents are accepted and whether a selfie is
needed, and the gateway has no default. Create one once, from your backend:
await client.levels.create({
name: 'basic-kyc',
definition: {
requiredSteps: [
{ step: 'IDENTITY_DOCUMENT', required: true, documents: [{ country: 'AO', types: ['ID_CARD'] }] },
{ step: 'SELFIE', required: true },
],
},
});const { verificationId } = await client.verifications.start({
country: 'AO',
documentType: 'ID_CARD',
levelName: 'basic-kyc',
});
await client.verifications.uploadDocument(verificationId, 'front', frontImage);
await client.verifications.uploadDocument(verificationId, 'back', backImage);
await client.verifications.uploadSelfie(verificationId, selfieImage);
const result = await client.verifications.waitForResult(verificationId);
console.log(result.status, result.outcome);Images can be a Blob/File, raw bytes ({ data: Uint8Array }), a React
Native { uri }, a data: URI, or bare base64. The bytes are checked against
the same magic-byte test the gateway applies, so a PDF renamed .jpg fails
before the upload rather than after it.
Both document sides are required
All three catalog documents are two-sided, and the gateway only queues OCR once
both documentFrontPath and documentBackPath exist. A flow that collects
only the front leaves the verification sitting in PENDING with nothing
running.
The flow controller
VerificationFlow is the sequencing above as a state machine — which step is
next, what a resumed flow skips, when to poll. It is what @kyciris/mobile and
@kyciris/web are built on.
import { VerificationFlow } from '@kyciris/core';
const flow = new VerificationFlow(client, {
country: 'AO',
documentType: 'ID_CARD',
levelName: 'basic-kyc',
});
flow.subscribe((state) => render(state));
await flow.start(); // or resumes what is in storage
await flow.submit(image); // uploads state.currentStep, advancesFailures land in state.error rather than rejecting, so a button handler needs
no try/catch. flow.cancel() stops in-flight work on unmount.
Resuming an interrupted flow
Give the client a storage adapter and it remembers which verification is in
progress:
import { createKycirisClient, createMemoryStorage } from '@kyciris/core';
const client = createKycirisClient({ baseUrl, verificationToken, storage });Progress itself is never trusted to local state — it is read back from the
files the gateway actually holds (GET /v1/verification/:id/media), so it is
right after a reinstall, on a second device, and when an upload succeeded but
its response never arrived.
const progress = await client.verifications.getProgress();
// { missingSteps: ['document_back', 'selfie'], isComplete: false, ... }
await client.verifications.uploadStep({ step: 'document_back', file });
// skipped: true, if it turns out to be there alreadyOnly identifiers are stored. The verification token is never written to storage — it is a bearer credential, and where it lives is your decision.
Server-side
With an API key, the client also reaches the review and operations endpoints:
const page = await server.verifications.list({ outcome: 'NEEDS_REVIEW' });
const summary = await server.verifications.summary();
const history = await server.verifications.events(verificationId);
await server.verifications.decide(verificationId, {
decision: 'REJECTED',
reason: 'Document photo does not match the selfie.',
});Plus identities, levels, media, ocr, webhooks and analytics.
Images come out through a two-step, single-use flow — a 60-second token for one file, then the bytes:
const response = await server.media.download(verificationId, 'document-front');Status, and what it means
status is the pipeline's state machine. outcome is what a finished
verification means, and it exists because REJECTED is written for two
different events:
| outcome | Meaning |
| ------------------ | ------------------------------------------------------------ |
| APPROVED | Passed. |
| REJECTED | A real decision. Retrying will not change it. |
| FAILED_TECHNICAL | Our pipeline broke. Not the user's fault — reanalyze() it. |
| NEEDS_REVIEW | A person should look at the images. |
Branch on outcome, not on status, when deciding what to tell a user.
For anything longer than a spinner, use the webhooks rather than polling: a
REVIEW sits with a human, potentially for hours.
Errors
Everything thrown is a KycirisError with a code you can branch on, the
gateway's requestId, and nothing from the request — the headers hold the
credentials, and an error object is what gets logged whole.
import { isKycirisError } from '@kyciris/core';
try {
await client.verifications.start({
country: 'AO',
documentType: 'ID_CARD',
levelName: 'basic-kyc',
});
} catch (error) {
if (isKycirisError(error) && error.code === 'UNAUTHORIZED') {
// the token expired — fetch a new one
}
}Transient failures (429, 5xx, network) are retried with jittered backoff. Uploads are not, because a POST that timed out may well have landed; a 429 is, because the gateway rejected it before doing any work.
Configuration
| Option | Default | |
| ------------------------------ | ------- | -------------------------------------------------------------------- |
| baseUrl | — | Origin of the gateway. Plaintext http to a remote host is refused. |
| apiKey / verificationToken | — | One is required. |
| apiVersion | v1 | URI version segment. |
| timeoutMs | 30000 | Per attempt. |
| maxRetries | 2 | Retries of a failed attempt. |
| storage | — | Enables resuming. |
| fetch | global | A custom implementation, for proxies or tests. |
| headers | — | Added to every request; cannot override the credentials. |
| allowInsecureBaseUrl | false | Permits http to a private network. |
| allowApiKeyInUntrustedClient | false | Only for a trusted first-party console. |
Supported documents
AO/ID_CARD, AO/DRIVING_LICENSE, MZ/ID_CARD. An unsupported pair is
refused client-side, naming what the country does support, rather than costing
a round trip.
