npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kyciris/core

v1.1.1

Published

UI-agnostic client for the KYCiris identity verification API

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/core

Quickstart

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, never NEXT_PUBLIC_* / EXPO_PUBLIC_*). The SDK throws if it reaches a client.
  • [ ] externalId comes 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, not status (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, advances

Failures 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 already

Only 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.