Skip to main content

@zyphe-sdk/node

Install the package:

pnpm add @zyphe-sdk/node

1. Webhook Signature Verification​

This package provides the core webhook signature verification logic for Zyphe. It enables secure verification of incoming webhook requests in Node.js using HMAC-SHA256 signatures.

Basic Usage​

import { verifyWebhookSignatureHeader } from '@zyphe-sdk/node'

const secretHex = '<your-hex-encoded-secret>'
const rawBodyString = '...' // The raw request body as a string
const signatureHeader = 't=1234567890,v0=abcdef...' // The signature header from the webhook

const isValid = verifyWebhookSignatureHeader(secretHex, rawBodyString, signatureHeader)
if (isValid) {
// Signature is valid
} else {
// Signature is invalid
}
  • secretHex: The hex-encoded secret key used to sign the webhook.
  • rawBodyString: The raw request body as a string (must match exactly what was sent).
  • signatureHeader: The signature header string, e.g. t=1234567890,v0=abcdef....
  • Returns: true if the signature is valid, or false otherwise.

Advanced Utilities​

  • parseSignatureHeader(signatureHeader), Parse the signature header into timestamp and signature.
  • hexToBytes(hex) / bufferToHex(buffer), Convert between hex and bytes.
  • timingSafeEqual(a, b), Constant-time string comparison.
  • Error codes/messages for robust error handling.

2. Verification Session Management​

You can programmatically create verification sessions and construct user-facing URLs for onboarding flows.

Create a Verification Session​

import { createVerificationSession } from '@zyphe-sdk/node'

const response = await createVerificationSession(
{
// Provide at least one identity field: email or a credential.
email: 'user@example.com', // optional when credentials are provided
credentials: [
{ type: 'EXTERNAL_ID', externalId: 'user_123' },
{ type: 'WALLET', address: '0x123...', chain: 'ETHEREUM' },
], // optional when email is provided
flowId: '<flowId>', // required
isSandbox: false, // required
customData: {
// optional
plan: 'pro',
// ...any extra data you want to pass
},
},
{
// Secret key (zyphe_sk_...) — required for server-side session creation
apiKey: '<your-secret-api-key>',
// production → https://api.zyphe.com + https://verify.zyphe.com
// See Environment setup: /docs/guides/environment-setup
environment: 'production',
handoffBaseUrl: 'https://your-app.com', // optional
// Theming, all optional. Light mode:
backgroundColor: '#ffffff', // page background
accentColor: '#155dfb', // buttons, icons, highlights
cardColor: '#ffffff', // card surface
// Dark mode:
darkBackgroundColor: '#16181d',
darkAccentColor: '#155dfb',
darkCardColor: '#16181d',
borderRadius: '12px', // shared radius for cards, buttons, inputs
// Deprecated (still honored): primaryColor → background, secondaryColor → accent
isFullscreen: true, // optional
},
)
  • email and credentials identify the user. Provide at least one of email, an EXTERNAL_ID credential, or a WALLET credential.
  • credentials (optional): Pass an array of credentials, for example { type: 'EXTERNAL_ID', externalId: 'user_123' } or { type: 'WALLET', address: '0x123...', chain: 'ETHEREUM' }.
  • You can combine identity fields, such as email plus externalId, email plus wallet, or all three.
  • customData (optional): Pass any extra data you want to associate with the verification session. This will be available in the webhook payload and session data.
  • The function returns a Promise resolving to the API response. Handle errors as needed (e.g., try/catch or checking for error fields).

Construct a Verification Session URL​

import { constructVerificationSessionUrl } from '@zyphe-sdk/node'

const url = constructVerificationSessionUrl({
flowParams: {
flowId: '...',
email: 'user@example.com',
credentials: [{ type: 'EXTERNAL_ID', externalId: 'user_123' }],
isSandbox: false,
customData: { plan: 'pro' }, // optional
},
verificationSession: response.data, // from createVerificationSession
opts: {
apiKey: '<your-api-key>',
environment: 'production',
handoffBaseUrl: 'https://your-app.com', // optional
// Theming, all optional. Light mode:
backgroundColor: '#ffffff', // page background
accentColor: '#155dfb', // buttons, icons, highlights
cardColor: '#ffffff', // card surface
// Dark mode:
darkBackgroundColor: '#16181d',
darkAccentColor: '#155dfb',
darkCardColor: '#16181d',
borderRadius: '12px', // shared radius for cards, buttons, inputs
// Deprecated (still honored): primaryColor → background, secondaryColor → accent
isFullscreen: true, // optional
},
})
  • The constructed URL includes all relevant parameters and can be used to embed or redirect users to the verification flow.
  • Optional fields (like handoffBaseUrl, accentColor, borderRadius, etc.) will be appended as query parameters if provided. See Theming for the full list of theme options and how they work.

Types​

  • InitializeZypheFlowParams: Parameters for initializing a verification flow.
  • SDKOptions: Configuration for the SDK.
  • SdkCreateVerificationRequestResponse: API response type for session creation.

Error Handling​

  • If session creation fails, the function may throw or return an error object depending on usage context. Always check for errors and handle them appropriately.
  • Example:
try {
const response = await createVerificationSession(...);
if (response.error) {
// Handle error
console.error(response.error);
} else {
// Success
}
} catch (err) {
// Handle thrown errors (e.g., network issues)
console.error(err);
}
Email aliases not supported in production

The email field in flowParams does not accept email aliases (e.g. user+tag@example.com) in production environments. Use a plain email address without +alias suffixes when isSandbox is false.


3. Complete Verification Steps​

The Node SDK also includes helpers for completing document selection, document verification, and liveness steps from your backend.

Typical flow:

  1. Create a verification session with createVerificationSession.
  2. Check the current step with getNextStepDetails if you need to branch dynamically.
  3. Complete document selection with completeDocumentSelectionStep when the next step is document-selection.
  4. Upload document images with completeDvStep when the next step is a document verification step.
  5. Complete liveness with completeLivenessStep when the flow requires liveness.

Inspect the Next Step​

import { getNextStepDetails, type SDKOptions } from '@zyphe-sdk/node'

const opts: SDKOptions = {
apiKey: '<your-secret-api-key>',
environment: 'production',
}

const { error, data: nextStep } = await getNextStepDetails(
{
flowId: '<flow-id>',
email: 'user@example.com',
credential: { type: 'EXTERNAL_ID', externalId: 'user_123' },
isSandbox: true,
},
opts,
)

if (error) {
console.error(error)
} else {
console.log('Next step:', nextStep)
}
  • Use getNextStepDetails to decide whether to ask for document selection, document images, liveness capture, or another step.
  • Provide at least one identity field: email, an EXTERNAL_ID credential, or a WALLET credential.
  • This helper calls the sdk_get_next_step endpoint documented in docs/openapi/sdk-get-next-step.api.mdx.

Complete Document Selection and Document Verification​

import { completeDocumentSelectionStep, completeDvStep, createVerificationSession, DocumentType, type SDKOptions } from '@zyphe-sdk/node'
import { readFile } from 'node:fs/promises'

async function readBase64File(filePath: string) {
return (await readFile(filePath)).toString('base64')
}

const opts: SDKOptions = {
apiKey: '<your-secret-api-key>',
environment: 'production',
}

const verificationSessionResponse = await createVerificationSession(
{
email: 'user@example.com',
flowId: '<flow-id>',
isSandbox: true,
},
opts,
)

if (verificationSessionResponse.error || !verificationSessionResponse.data) {
throw new Error('Failed to create verification session')
}

await completeDocumentSelectionStep(
{
verificationSession: verificationSessionResponse.data,
payload: {
country: 'IT',
documentType: DocumentType.IDENTITY_CARD,
},
},
opts,
)

const dvResult = await completeDvStep(
{
verificationSession: verificationSessionResponse.data,
countryCode: 'IT',
images: {
type: 'DOUBLE_SIDED_DOCUMENT',
frontImage: await readBase64File('./front.jpg'),
backImage: await readBase64File('./back.jpg'),
},
},
opts,
)

console.log('DV step completed')
console.log(dvResult)
  • This example follows ../zyphe-sdk/examples/node-complete-step-dv/src/index.ts.
  • Use DocumentType.IDENTITY_CARD, DocumentType.DRIVING_LICENSE, or DocumentType.PASSPORT when selecting the document type.
  • completeDvStep accepts either:
    • type: 'DOUBLE_SIDED_DOCUMENT' with frontImage and backImage
    • type: 'SINGLE_SIDED_DOCUMENT' with a single image
  • Image values must be base64-encoded strings.
  • The SDK returns the API response for the submitted document job, including the verification request ID and job ID.

Complete a Liveness Step​

import { completeLivenessStep, createVerificationSession, type SDKOptions } from '@zyphe-sdk/node'
import { readFile } from 'node:fs/promises'

async function readBinaryFile(filePath: string) {
return new Uint8Array(await readFile(filePath))
}

const opts: SDKOptions = {
apiKey: '<your-secret-api-key>',
environment: 'production',
}

const verificationSessionResponse = await createVerificationSession(
{
email: 'user@example.com',
flowId: '<flow-id>',
isSandbox: true,
},
opts,
)

if (verificationSessionResponse.error || !verificationSessionResponse.data) {
throw new Error('Failed to create verification session')
}

const calibrationImage = await readBinaryFile('./calibration.jpg')
const video = await readBinaryFile('./liveness.mp4')
const movementImages = await Promise.all(['./movement-1.jpg', './movement-2.jpg'].map((filePath) => readBinaryFile(filePath)))

const livenessResult = await completeLivenessStep(
{
verificationSession: verificationSessionResponse.data,
frontImage: Buffer.from(calibrationImage),
video: Buffer.from(video),
movementImages: movementImages.map((image) => Buffer.from(image)),
},
opts,
)

console.log('Liveness step completed')
console.log(livenessResult)
  • This example follows ../zyphe-sdk/examples/node-complete-step-liveness/src/index.ts.
  • completeLivenessStep first fetches the challenge configuration, uploads the required assets to the provided presigned URLs, and then calls the final liveness processing endpoint.
  • For active liveness challenges, you must provide a video and the same number of movementImages requested by the challenge.
  • frontImage is the calibration image uploaded before the final liveness submission.

4. Flow Results​

You can use the Node SDK to list flow results for a flow and fetch a single flow result by ID.

List Flow Results​

import { listFlowResults, type SDKOptions } from '@zyphe-sdk/node'

const opts: SDKOptions = {
apiKey: '<your-secret-api-key>',
environment: 'production',
}

const { error, data } = await listFlowResults(
{
organizationId: '<organization-id>',
flowSlug: '<flow-slug>',
isSandbox: true,
pagination: {
take: 10,
skip: 0,
},
},
opts,
)

if (error) {
console.error(error)
} else {
console.log('Total flow results:', data?.totalCount ?? 0)
console.log('Current page:', data?.page ?? [])
}
  • organizationId: The organization that owns the flow results.
  • flowSlug: The flow slug used to scope the results.
  • isSandbox: Set to true for sandbox data and false for production data.
  • pagination (optional): Use take, skip, sort, and filter to control paging.
  • locale (optional): Localize fields that support translated values.
  • Returns: A Promise resolving to a paginated response with totalCount and a page array of flow results.

Get a Flow Result​

import { getFlowResult, type SDKOptions } from '@zyphe-sdk/node'

const opts: SDKOptions = {
apiKey: '<your-secret-api-key>',
environment: 'production',
}

const { error, data: flowResult } = await getFlowResult(
{
organizationId: '<organization-id>',
flowResultId: '<flow-result-id>',
isSandbox: true,
},
opts,
)

if (error) {
console.error(error)
} else {
console.log('Flow result:', flowResult)
}
  • flowResultId: The ID of the specific flow result you want to retrieve.
  • Returns: A Promise resolving to the full flow result payload, including result collections such as dvResults, poaResults, and amlResults when available.

getFlowResult and getDvResult are Export API helpers. They require a secret key and an organization with the Export API product enabled. getDvResult returns decoded document fields and images from the protobuf stream. See Export API.


5. AML Screening​

screenAml runs a live AML screening for a person or a company, outside any verification flow. It queries the configured screening provider and returns its raw matches: nothing is scored or stored.

import { screenAml, type SDKOptions } from '@zyphe-sdk/node'

const opts: SDKOptions = {
apiKey: '<your-secret-api-key>',
environment: 'production',
}

const { error, data } = await screenAml(
{
organizationId: '<organization-id>',
isSandbox: true,
subjectType: 'PERSON', // or 'COMPANY'
query: {
name: 'Jane Doe',
birthDate: '1980-01-01',
country: 'nl',
},
},
opts,
)

if (error) {
console.error(error)
} else {
for (const entity of data.entities) {
console.log(entity.name, entity.score)
}
}
  • query accepts the person or company attributes you have, such as name, firstName, lastName, birthDate, country, nationality, registrationNumber, or passportNumber. The more you send, the fewer false positives.
  • The response lists the raw provider matches in entities (each with a match score), with the provider and entity format used.
  • AML screening is billed per call. For how screening works inside a flow, see AML.

6. KYB Cases​

Every KYB result is handled as a case: its status, due-diligence level, rating, officers, and decision. The case helpers address it by caseId, the ID of the KYB case (the same value as the KYB result ID).

All KYB helpers require a secret key.

Read a Case​

import { getKybCase, getKybCaseOfficers, type SDKOptions } from '@zyphe-sdk/node'

const opts: SDKOptions = {
apiKey: '<your-secret-api-key>',
environment: 'production',
}

const params = { organizationId: '<organization-id>', caseId: '<case-id>', isSandbox: true }

const { data: kybCase } = await getKybCase(params, opts)
console.log(kybCase?.status, kybCase?.dueDiligenceLevel, kybCase?.rating)

const { data: officers } = await getKybCaseOfficers(params, opts)
console.log(officers?.ubos.length, 'UBOs,', officers?.directors.length, 'directors')
  • getKybCase returns the case as the dashboard reads it: status (OPEN, PROCESSING, WAITING_INPUT, ACTION_REQUIRED, READY_FOR_REVIEW, APPROVED, REJECTED, CANCELLED), dueDiligenceLevel, rating, assignment, what the case is waiting on, and each attempt at deciding it.
  • getKybCaseOfficers returns the case's ubos and directors. Each person carries the stable id the officer helpers below use.

Download the Case Report​

import { writeFile } from 'node:fs/promises'
import { getKybCaseReport } from '@zyphe-sdk/node'

const { data: pdf, etag, fileName, error } = await getKybCaseReport(params, opts)
if (pdf) await writeFile(fileName ?? 'kyb_report.pdf', pdf)

// Later: download it again only if something in the report has changed
const { notModified } = await getKybCaseReport({ ...params, ifNoneMatch: etag }, opts)
  • getKybCaseReport returns the case's KYB report as a PDF (data is a Uint8Array): company and register data, directors, ownership and beneficial owners, screening, adverse media, documents and data sources, as the case stands when you call it. Every report has the same sections; a section the case has no data for says so.
  • The report is identified by a hash of its content, returned as etag. Pass it back as ifNoneMatch: when nothing in the report has changed the call answers notModified: true with no body.
  • Each download is recorded on the organization's audit log.

Manage UBOs and Directors​

import { addKybCaseUbos, removeKybCaseDirector, sendKybCaseUboReminder } from '@zyphe-sdk/node'

const { data: officers } = await addKybCaseUbos(
{
...params,
persons: [
{
firstName: 'Jane',
lastName: 'Doe',
dateOfBirth: '1980-01-01',
country: 'nl',
gender: 'F',
// With an email the person is invited to complete full KYC;
// without one they are screened by name only (AML)
email: 'jane.doe@example.com',
},
],
},
opts,
)

// Invite the UBO just added to complete their KYC
await sendKybCaseUboReminder({ ...params, recipientIndex: officers!.ubos.length - 1 }, opts)

await removeKybCaseDirector({ ...params, personId: '<director-id>' }, opts)
  • addKybCaseUbos / addKybCaseDirectors add one or more people; removeKybCaseUbo / removeKybCaseDirector remove one by personId. All four answer with the case's officers as they stand after the change ({ caseId, ubos, directors }).
  • sendKybCaseUboReminder sends (or re-sends) the KYC invitation to the UBO at recipientIndex in the ubos list. Only a UBO with an email can be invited.

Request a Document​

import { requestKybCaseOnDemandDocument } from '@zyphe-sdk/node'

const { data: documentRequest } = await requestKybCaseOnDemandDocument(
{
...params,
documentKey: '<on-demand-document-key>',
recipientEmail: 'compliance@company.com', // optional
subject: 'One more document', // optional
body: 'Please upload the requested document.', // optional
},
opts,
)
  • documentKey must name a document the flow marks as on demand. A document the flow requires upfront is rejected.

Decide a Case​

import { decideKybCase } from '@zyphe-sdk/node'

const { data: decision, response } = await decideKybCase(
{
...params,
outcome: 'APPROVED', // or 'REJECTED'
reason: 'Checks complete', // mandatory on a rejection
dueDiligenceLevel: 'standard', // optional, approvals only
},
opts,
)

if (response.status === 202) {
// The organization's approval matrix asks for a second signature
console.log('Awaiting:', decision?.decisionProposal)
} else {
console.log('Decided:', decision?.outcome, decision?.statusAfter)
}
  • A decision is final: a decided case cannot be decided again.
  • An approval needs the case in READY_FOR_REVIEW. A rejection is also accepted from ACTION_REQUIRED, and always needs a reason. Add a reasonCode (for example SANCTIONS_CONFIRMED or OUTSIDE_RISK_APPETITE) to classify it.
  • When the organization's approval matrix requires two signatures, the call answers 202 with a decisionProposal listing the roles it awaits. An API key can take the first signature only: the case closes when a person holding one of those roles confirms the proposal in the dashboard. Sending confirmingProposalId with an API key is refused with kyb_case_second_signature_required.

Deprecated KYB Helpers​

The helpers below address the case by kybResultId and call endpoints that are deprecated. They keep working, but new integrations should use the case helpers:

DeprecatedUse instead
addKybUbosaddKybCaseUbos
removeKybUboremoveKybCaseUbo
addKybDirectorsaddKybCaseDirectors
removeKybDirectorremoveKybCaseDirector
requestKybOnDemandDocumentrequestKybCaseOnDemandDocument
sendKybUboRemindersendKybCaseUboReminder

When migrating, pass the same ID as caseId. The officer helpers now answer with { caseId, ubos, directors } instead of the whole KYB result, so read ubos / directors instead of uboDefinitions / directorDefinitions. The migration guide walks through every change.


7. Transaction Monitoring​

Stream payment events from your backend and get the synchronous Allow / Review / Block verdict. See Transaction Monitoring for how rules, lists, and thresholds produce it.

import { getTransaction, ingestTransaction, updateTransactionStatus, type SDKOptions } from '@zyphe-sdk/node'

const opts: SDKOptions = {
apiKey: '<your-secret-api-key>',
environment: 'production',
}

const { error, data: verdict } = await ingestTransaction(
{
organizationId: '<organization-id>',
isSandbox: true,
transaction: {
externalId: 'txn_123', // your ID, also the idempotency key
identityId: '<verified-identity-id>',
direction: 'WITHDRAWAL', // 'DEPOSIT' | 'WITHDRAWAL' | 'TRANSFER'
amountMinor: 125000, // integer minor units: 1,250.00 EUR
currency: 'EUR',
occurredAt: new Date().toISOString(),
channel: 'sepa', // optional
description: 'Invoice 2026-042', // optional, screened for keywords
counterparty: { kind: 'PERSON', name: 'John Smith', address: 'NL91ABNA0417164300' }, // optional
},
},
opts,
)

if (error) {
console.error(error)
} else {
console.log(verdict.decision, verdict.riskProbability, verdict.firedRules, verdict.alerts)
}

// Read it back, then report the payment lifecycle
const { data: transaction } = await getTransaction(
{ organizationId: '<organization-id>', transactionId: verdict!.transactionId, isSandbox: true },
opts,
)

await updateTransactionStatus(
{ organizationId: '<organization-id>', transactionId: verdict!.transactionId, status: 'SETTLED', isSandbox: true },
opts,
)
  • ingestTransaction is idempotent on externalId: sending the same event again returns the original verdict with replay: true, without re-evaluating or billing it twice. Retrying with the same externalId is the safe recovery after a timeout or a 5xx.
  • amountMinor is an integer in minor units (cents); there is no float money.
  • updateTransactionStatus sets the payment status: PENDING, SETTLED, FAILED, CANCELED, or REVERSED. Moving a transaction to FAILED, CANCELED, or REVERSED removes it from the velocity counters once; retrying the same update is safe.
  • Transaction ingestion is billed per ingested transaction (replays are not billed).

8. Types and Configuration​

  • InitializeZypheFlowParams, SDKOptions, SdkCreateVerificationRequestResponse, for type-safe integration.
  • Environments, EnvironmentConfigs, API_KEY_HEADER, for environment and API configuration.