The official Didit TypeScript and JavaScript library. Identity verification (KYC), business verification (KYB), AML screening, biometrics, transaction monitoring, Travel Rule and customer authentication, from your backend.
- Every server endpoint of the Didit API, with full types, generated from the OpenAPI contract.
- Webhook verification with typed events, and Didit Authentication token verification.
- No runtime dependencies. Runs on Node.js 18 or later, Bun, Deno and edge runtimes with
fetchand Web Crypto (Cloudflare Workers, Vercel Edge). - ESM and CommonJS, TypeScript strict.
npm install @didit-protocol/sdk-node
# or: pnpm add @didit-protocol/sdk-node / yarn add @didit-protocol/sdk-node / bun add @didit-protocol/sdk-nodeCreate a verification session, send the user to its URL, read the decision, and verify the webhook Didit sends when the status changes.
import Didit from "@didit-protocol/sdk-node";
const didit = new Didit(); // reads DIDIT_API_KEY
// 1. Create a session and send the user to session.url.
const session = await didit.sessions.create({
workflow_id: "11111111-2222-3333-4444-555555555555",
vendor_data: "user-42",
callback: "https://example.com/verification/done",
});
console.log(session.url);
// 2. Later, read the decision.
const decision = await didit.sessions.retrieveDecision(session.session_id);
console.log(decision.status, decision.created_at); // "Approved", a Date// 3. Verify the webhook (Express). Pass the raw body, not parsed JSON.
import express from "express";
import { webhooks, WebhookVerificationError } from "@didit-protocol/sdk-node";
const app = express();
app.post("/webhooks/didit", express.raw({ type: "application/json" }), async (req, res) => {
// express.raw leaves req.body empty for other content types.
if (!Buffer.isBuffer(req.body)) return res.status(400).send("expected a JSON body");
try {
const event = await webhooks.verify(req.body, req.headers, process.env.DIDIT_WEBHOOK_SECRET!);
if (event.webhook_type === "status.updated") {
console.log(event.session_id, event.status);
}
res.sendStatus(200);
} catch (error) {
if (error instanceof WebhookVerificationError) return res.status(400).send(error.reason);
throw error;
}
});More runnable examples, including webhook receivers for Express, Fastify, Hono, Next.js, Cloudflare Workers, Bun and Deno, are in examples/.
const didit = new Didit({
apiKey: "...", // default: DIDIT_API_KEY
organizationId: "...", // default: DIDIT_ORGANIZATION_ID
applicationId: "...", // default: DIDIT_APPLICATION_ID
timeout: 60_000, // milliseconds per attempt
maxRetries: 2,
});| Option | Default | Notes |
|---|---|---|
apiKey |
DIDIT_API_KEY |
Sent as x-api-key, only to baseURL. A client without a key can still use didit.account and verify webhooks. |
baseURL |
https://verification.didit.me |
|
organizationId, applicationId |
DIDIT_ORGANIZATION_ID, DIDIT_APPLICATION_ID |
Needed by application-scoped methods (cases, networks, ongoing monitoring and others; see api.md). |
accountBaseURL |
https://apx.didit.me/auth/v2 |
The account management API (didit.account). |
accountToken |
none | The access_token from didit.account.login(...). |
timeout |
60_000 |
Milliseconds, per attempt, including reading the body. |
maxRetries |
2 |
See Retries. |
defaultHeaders |
none | Added to every request. They cannot replace the credentials (x-api-key, Authorization). |
fetch |
global fetch |
Your own fetch (proxies, instrumentation, tests). |
fetchOptions |
none | Extra fetch options for every request, such as an undici dispatcher. A signal here cancels every request. |
logLevel, logger |
DIDIT_LOG, console |
debug logs one line per request. |
The environment variables are read on Node.js, Bun and Deno (with --allow-env). Edge runtimes such as Cloudflare Workers pass configuration to the handler instead, so give it to the client: new Didit({ apiKey: env.DIDIT_API_KEY }).
Create one client and reuse it. didit.withOptions({ ... }) returns a new client with some options changed. Every method also takes request options as its last argument:
await didit.sessions.create(
{ workflow_id },
{
timeout: 10_000,
maxRetries: 0,
idempotencyKey: "order-123",
headers: { "X-Trace": "abc" },
signal,
},
);signal cancels the request, its retries and the wait between them; a signal in fetchOptions does the same.
Resources and methods follow the API: didit.<resource>.<method>(...), with nested resources such as didit.sessions.imports or didit.cases.notes.
Path parameters are positional; everything else goes in one params object whose fields keep the API's names (snake_case).
api.md lists every method with its HTTP route and types, generated from openapi/.
import { fileFromPath } from "@didit-protocol/sdk-node";
await didit.idVerification.verify({ front_image: await fileFromPath("id-front.jpg") });
await didit.aml.screen({ full_name: "Ada Lovelace", date_of_birth: "1815-12-10" });
await didit.users.updateStatus("user-42", { status: "FLAGGED" });
await didit.cases.notes.create(caseUuid, { comment: "Checked with the customer" });Every request and response has a type, exported from the package:
import type {
SessionsCreateParams,
SessionsCreateResponse,
TransactionDetail,
} from "@didit-protocol/sdk-node";Enums are open ("Approved" | "Declined" | (string & {})): a value added to the API later never breaks your build.
Fields the API adds later are kept on the objects you receive.
Optional fields are ?:; fields the API can send as null include | null.
Date-time fields are Date objects (a date-time the API sends without a time zone is read as UTC); in parameters you can pass a Date or an ISO 8601 string.
The account management API (organizations and applications) is under didit.account:
const { access_token } = await didit.account.login({ email, password });
const account = didit.withOptions({ accountToken: access_token });
const organizations = await account.account.organizations.list();Paginated list methods (a "page" in api.md) return a page with count, next, previous and results. Iterate with for await to walk every item of every page:
for await (const session of didit.sessions.list({ limit: 100 })) {
console.log(session.session_id);
}
const page = await didit.sessions.list({ limit: 100 });
if (page.hasNextPage()) {
const next = await page.getNextPage();
}The SDK follows the API's next links and refuses a link to another origin, so your API key never leaves the API.
Upload parameters accept a Blob or File, a Uint8Array or ArrayBuffer, a web ReadableStream, a Node.js stream such as fs.createReadStream("id.jpg"), or { data, filename, contentType }.
fileFromPath(path) reads a file on Node.js, Bun and Deno; toFile(bytes, filename) names bytes.
An upload without a name is sent as file plus the extension its content type or first bytes give (file.jpg, file.pdf).
A request with a file is sent as multipart form data: objects in it travel as JSON text, and null fields are left out (multipart has no null).
Binary responses (session and user PDFs, network reports, wallet screening PDFs, the import template, case attachments) return a Blob:
import { writeFile } from "node:fs/promises";
const pdf = await didit.sessions.generatePdf(sessionId);
await writeFile("report.pdf", new Uint8Array(await pdf.arrayBuffer()));| Error | When |
|---|---|
DiditError |
Base class of every error below. |
APIConnectionError / APITimeoutError |
No response (network failure, timeout). |
APIStatusError |
Any non-2xx answer, with status, body (parsed JSON when possible), headers and requestId. |
BadRequestError 400, AuthenticationError 401, PermissionDeniedError 403, NotFoundError 404, ConflictError 409, UnprocessableEntityError 422, RateLimitError 429, InternalServerError 5xx |
Subclasses of APIStatusError. |
APIResponseValidationError |
A 2xx answer the SDK cannot use (not JSON, a pagination link to another origin). |
WebhookVerificationError |
A webhook failed verification; reason says why. |
TokenVerificationError |
An Authentication token failed verification; reason says why. |
ConfigurationError |
Missing API key, organization or application id, an empty webhook secret, an invalid option. Thrown before anything is sent. |
import { APIStatusError, NotFoundError } from "@didit-protocol/sdk-node";
try {
await didit.sessions.retrieveDecision(sessionId);
} catch (error) {
if (error instanceof NotFoundError) {
// ...
} else if (error instanceof APIStatusError) {
console.error(error.status, error.body, error.requestId);
} else {
throw error;
}
}The API key never appears in an error message, a log line or the logged client.
Failed requests are retried up to maxRetries times (default 2) with exponential backoff from 0.5 s, full jitter, capped at 8 s, honouring Retry-After (capped at 60 s).
GET, HEAD, PUT and DELETE are retried on connection errors, timeouts, 408, 429 and 5xx.
POST and PATCH are never retried once the request may have reached the server: only on 429 or when the connection failed before anything was sent.
const { data, response, requestId } = await didit.sessions.create({ workflow_id }).withResponse();
const raw: Response = await didit.sessions.generatePdf(sessionId).asResponse(); // unread, for streamingwebhooks.verify(rawBody, headers, secret, options?) checks the signature and the timestamp and returns a typed event; it throws WebhookVerificationError otherwise.
It is also available as didit.webhooks.verify(...).
rawBody: the exact body bytes (aBuffer,Uint8Array,ArrayBufferor string). Configure your framework to give you the raw body.headers: aHeadersobject, Node.jsreq.headers, or[name, value]pairs.secret: the destination's secret, or a list of secrets during a rotation.options.tolerance: seconds between the delivery's timestamp and now (default 300).
Didit signs three ways: X-Signature (the raw body), X-Signature-V2 (a canonical JSON form that survives middleware that re-encodes JSON) and the deprecated X-Signature-Simple; during a secret rotation the -Previous variants are signed with the old secret. Any of the first two, with either secret, is accepted.
const event = await webhooks.verify(rawBody, headers, [currentSecret, previousSecret]);
switch (event.webhook_type) {
case "status.updated":
case "data.updated":
console.log(event.session_id, event.status, event.decision?.status);
break;
case "transaction.status.updated":
console.log(event.transaction_id, event.status, event.score);
break;
default:
console.log("unhandled event", event.webhook_type);
}Events are a union discriminated by webhook_type: session (status.updated, data.updated), user, business, transaction and Travel Rule events, activity.created, workflow.compliance_changed, kyb.registry_search.resolved, and UnknownEvent for any other type.
Each event also has event.verifiedBy (which signature matched) and event.bodyAuthenticated.
The event is the body as sent, except that the session decision is the generated decision type, so its date-times are Date objects.
The KYB registry search callback (kyb.registry_search.resolved) is delivered unsigned today (X-Didit-Unsigned-Callback: true), so verify rejects it with missing_signature. Do not trust its body: read the result from the API by its request_id instead.
If your framework only gives you the parsed JSON body, webhooks.verifyParsed(payload, headers, secret) checks X-Signature-V2 only, and fails for integers above 2^53 that JSON.parse has already rounded. Prefer the raw body.
X-Signature-Simple signs the envelope only, not the body; it is accepted with { allowSimple: true }, and then event.bodyAuthenticated is false: re-fetch the record from the API before acting on it.
const txn = await didit.transactions.create({
transaction_id: "txn-1001",
transaction_category: "finance",
transaction_details: { direction: "outbound", amount: 2500, currency: "EUR" },
subject: { vendor_data: "user-42", full_name: "Ada Lovelace" },
});
console.log(txn.status, txn.score); // "APPROVED" | "IN_REVIEW" | "DECLINED" | ...
const screening = await didit.walletScreening.screen({
wallet_address: "0x...",
blockchain: "ETH",
});
for await (const rule of didit.transactions.rules.list()) console.log(rule.title, rule.mode);
const settings = await didit.travelRule.retrieveSettings();
const vasps = await didit.travelRule.listVasps({ limit: 20 });The transaction.created, transaction.status.updated and travel_rule.status.updated webhooks are typed events.
Didit Authentication gives each application its own OpenID Connect issuer for its end users. The Authentication helper verifies the ES256 tokens it signs and calls its token, session and step-up endpoints from your backend. It never uses your API key.
import { Authentication, TokenVerificationError } from "@didit-protocol/sdk-node";
const auth = Authentication.fromPublishableKey(process.env.DIDIT_PUBLISHABLE_KEY!, {
clientId: process.env.DIDIT_APPLICATION_ID!,
});
// or: new Authentication({ issuer: "https://acme.auth.didit.me", clientId: "<application id>" })
try {
const claims = await auth.verifyAccessToken(accessToken);
console.log(claims.sub, claims.org?.role);
} catch (error) {
if (error instanceof TokenVerificationError)
console.log(error.reason); // e.g. "expired"
else throw error;
}- Options:
issuer(or the publishable key),clientId(the application id: the default audience, and sent asclient_idto the token endpoint),leeway(seconds, default 60),timeout(milliseconds per issuer request, default 10 000),fetch,logLevel,logger. verifyAccessToken(token, { audience? })andverifyIdToken(token, { nonce, audience? }): ES256 only; issuer, audience, expiry, not-before and issued-at are checked with 60 s leeway. Keys come from the issuer's JWKS, cached and refreshed when a new key appears. Passnonce: nullwhen no nonce was sent (an ID token fromrefresh); the token must then carry none. An access token an agent uses for a person also verifies: itsactclaim names the agent.createPkce(),createState()andcreateNonce()are standalone functions (import { createPkce } from "@didit-protocol/sdk-node") for the authorization code flow; thenauth.exchangeCode({ code, codeVerifier, redirectUri }).refresh(refreshToken): serialise refreshes for one session; reusing a consumed refresh token revokes the session.getSession(accessToken)for an instant revocation check,revokeSession(accessToken).createStepUp(accessToken, { action, amount?, currency?, returnTo? })andretrieveStepUp(accessToken, id): act only on statusapproved. The issuer currently requiresreturnToand answers 400 without it.
Authentication is in early access and reaches production with the 2026-11-01 Didit release.
Set DIDIT_LOG=debug (or logLevel: "debug") to log one line per HTTP request: method, path, status, duration and request id. Bodies, headers, query strings and credentials are never logged.
| Runtime | Versions |
|---|---|
| Node.js | 18, 20, 22, 24 |
| Bun | 1.x |
| Deno | 2.x |
| Edge runtimes | Cloudflare Workers, Vercel Edge and other runtimes with fetch, Web Crypto and Web Streams |
CI runs the test suite on every Node.js version and a smoke test of the built package on Node.js 18, Bun, Deno and workerd (the Cloudflare Workers runtime).
The SDK refuses to send your API key anywhere but the API, but it is a server-side library: never ship your API key to a browser.
Choices this SDK makes where the shared design leaves them to each language:
- Names. Resources and methods are camelCase (
didit.idVerification.verify,retrieveDecision); request and response fields keep the API's snake_case names. A query parameter named like a body field takes a_querysuffix in the params object (node_id_query). Fields the SDK adds are camelCase:requestIdon errors and raw responses,verifiedByandbodyAuthenticatedon webhook events. - Options. Options are objects, not positional arguments:
new Didit({ apiKey }),new Authentication({ issuer }),verifyIdToken(token, { nonce, audience }),exchangeCode({ code, codeVerifier, redirectUri }),createStepUp(token, { action, ... }). The design'shttp_clientis thefetchoption (plusfetchOptions);logLevelandloggerchoose where debug lines go. On edge runtimes, pass the API key as an option (environment variables are read only on Node.js, Bun and Deno). - Errors.
APITimeoutErrorextendsAPIConnectionError.TokenVerificationError(Authentication) andAPIResponseValidationError(a 2xx answer the SDK cannot use) complete the design's table. - Times.
timeoutis in milliseconds; webhooktoleranceand Authenticationleewayare in seconds, like the timestamps they compare. - Dates. Response date-times are
Dateobjects, which keep millisecond precision (the API sends microseconds); one without a time zone is read as UTC. Date-only fields (format: date) stayYYYY-MM-DDstrings. Webhook events and Authentication claims are the JSON payloads as sent, so their timestamps stay numbers or strings, except a session event'sdecision, which is the generated decision type withDateobjects. - Async. Every API method,
webhooks.verifyand the Authentication helpers return promises (Web Crypto is asynchronous).createPkce,createStateandcreateNonceare asynchronous for the same reason. - Results. Methods return an
APIPromise: await it for the data, or call.withResponse()/.asResponse(). - Absent and null. Optional fields are
?:; nullable fields add| null; a field you leaveundefinedis not sent. - Large integers. Webhook signatures are checked on the raw body with a parser that keeps every number's digits, so integers above 2^53 are verified exactly. The typed event you get back is parsed with
JSON.parse, like every response. - CommonJS.
const { Didit } = require("@didit-protocol/sdk-node"); the ESM default export is alsorequire(...).default. - Files. Paths are read with
fileFromPath(path); everything else is passed as bytes, a Blob or a stream. - Client header.
User-Agent: didit-node/<version> (<runtime> <version>; <os>)andX-Didit-Client: node/<version>, after the repository name, on every runtime.
This package follows semantic versioning. A breaking change to the public SDK surface is a major version (a minor version while the version is 0.x).
Releases and CHANGELOG.md are produced by release-please from conventional commits.
When the API removes or renames an endpoint, the SDK keeps the old method as deprecated for one more minor version.
- API reference and guides: https://docs.didit.me
- Design contract shared by every Didit server SDK (Python, TypeScript, Go, Java, PHP, Ruby, .NET): docs/DESIGN.md
- Contributing (regenerate, test, release): CONTRIBUTING.md
- Security: SECURITY.md
MIT