@selfxyz/enterprise-sdk
v0.4.1
Published
Node/TS SDK for Self.xyz enterprise verification: session creation, webhook signature verification, typed payloads.
Downloads
2,323
Readme
@selfxyz/enterprise-sdk
Official Node/TypeScript SDK for the Self.xyz Enterprise API. Create verification sessions and verify webhooks with typed payloads.
Pre-1.0 stability: this SDK is under active development. Minor versions may include breaking changes until
1.0.0. Pin to an exact version in production.
Requirements
- Node.js ≥ 20
- ESM-only (the package is
"type": "module"; use it from ESM consumers, or via dynamicimport()from CommonJS)
Install
pnpm add @selfxyz/enterprise-sdk
# or
npm install @selfxyz/enterprise-sdk
# or
yarn add @selfxyz/enterprise-sdkQuickstart
Create a verification session and hand the returned URL to your end user:
import { SelfClient } from '@selfxyz/enterprise-sdk';
const self = new SelfClient({ apiKey: process.env.SELF_API_KEY! });
const session = await self.sessions.create({
flowId: '<your-flow-uuid>',
externalUuid: '<user-uuid>',
});
// Redirect the end user here:
console.log(session.verificationUrl);To look up a session later:
const detail = await self.sessions.get(session.id);
console.log(detail.status); // 'pending' | 'valid' | 'invalid' | 'error' | 'expired'Webhook verification
Use SelfWebhooks.verify to validate an inbound webhook signature and return a typed event:
import { SelfWebhooks } from '@selfxyz/enterprise-sdk';
app.post('/webhooks/self', (req, res) => {
try {
const event = SelfWebhooks.verify(
req.body, // raw string or Buffer (do NOT pre-parse)
req.headers, // must include the signature headers as received
process.env.SELF_WEBHOOK_SECRET!, // whsec_... from the Self dashboard
);
console.log(event.type, event.verification_id);
res.status(200).end();
} catch (err) {
res.status(400).end();
}
});The raw request body must be passed exactly as received. Middleware that JSON-parses the body before signature verification will cause it to fail.
Uniquely identifying a user
The verification.completed payload includes a nullifier: a privacy-preserving identifier derived from the verified document. It is stable for the same person within your scope and reveals nothing about their identity, so it is the value to key on for Sybil resistance or one-account-per-person checks. It is present when event.status is 'valid' and null otherwise. Note that event.external_uuid is the identifier you supplied when creating the session, not a per-person value.
Proof re-verification
verifyProof lets you independently re-verify a proof you received from Self.
Persist event.proof, event.proof_attributes, and event.environment from the verification.completed payload — those three fields are everything verifyProof needs to re-run offline whenever you want.
import { verifyProof } from '@selfxyz/enterprise-sdk';
// `verificationData` is whatever you stored when the
// `verification.completed` webhook arrived — keep `event.proof`,
// `event.proof_attributes`, and `event.environment` so this call
// can run offline at any time.
const { isValid } = await verifyProof({
orgId: process.env.SELF_ORG_ID!,
proof: verificationData.proof,
proofAttributes: verificationData.proof_attributes,
environment: verificationData.environment,
});isValid is true only when the Groth16 proof verifies, the minimum-age predicate is satisfied (if configured), and the user is not on an OFAC list (if OFAC checking is configured).
orgId
verifyProof requires your organization UUID — it's needed to verify the proof. You can find it on the dashboard under any product's Deploy tab → Re-verify proofs step.
Error handling
The SDK throws three distinct error types:
SelfValidationError: bad arguments. Raised before any network call when an SDK input (e.g.flowId) fails the schema check, or bySelfWebhooks.verifywhen a webhook payload doesn't match a known event shape.SelfApiError: non-2xx response from the API.WebhookVerificationError: invalid or missing webhook signature.
Network-level failures (DNS errors, connection refused, request aborts) propagate as the underlying TypeError/AbortError from fetch. They are not wrapped in SelfApiError. Handle them separately if you need to distinguish network problems from API responses.
import {
SelfApiError,
SelfValidationError,
WebhookVerificationError,
} from '@selfxyz/enterprise-sdk';
try {
await self.sessions.create({ flowId, externalUuid });
} catch (err) {
if (err instanceof SelfValidationError) {
err.message; // 'Invalid sessions.create input: flowId (Invalid uuid)'
err.issues; // ZodIssue[] (raw issues for programmatic handling)
} else if (err instanceof SelfApiError) {
err.statusCode; // number (HTTP status)
err.code; // string (machine-readable error code; see table below)
err.message; // string (human-readable)
err.details; // Record<string, unknown> | undefined (see "When details is populated")
err.requestId; // string | undefined (X-Request-Id from the response)
}
throw err;
}Error codes
SelfApiError.code carries a machine-readable error code so consumers can branch on the failure mode:
| Code | HTTP status | Meaning |
| -------------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
| validation_failed | 400 | Request body failed server-side validation. |
| unauthenticated | 401 | Missing, invalid, or revoked API key. |
| unauthenticated | 402 | Insufficient credits / billing gate triggered. Disambiguate from 401 via statusCode and check details. |
| forbidden | 403 | API key lacks permission for the resource. |
| not_found | 404 | Flow or session not found. |
| conflict | 409 | Flow has no published version, or other state conflict. |
| rate_limited | 429 | Rate limit exceeded. Back off and retry. |
| internal_error | 500 | Unhandled server error. |
| vendor_unavailable | 503 | Upstream dependency is degraded (e.g. circuit breaker open). |
| unknown_error | any | API response didn't match the expected envelope. The message field includes a body preview. |
The set of codes may grow over time. Always include a default branch in any switch (err.code) so future codes don't fall through silently.
try {
await self.sessions.create({ flowId, externalUuid });
} catch (err) {
if (!(err instanceof SelfApiError)) throw err;
switch (err.code) {
case 'rate_limited':
// back off and retry
break;
case 'unauthenticated':
if (err.statusCode === 402) {
// billing gate. err.details is { balance, required, planTier, creditGateMode }
} else {
// bad API key (401)
}
break;
case 'not_found':
// flow or session doesn't exist
break;
default:
// unknown_error or any future code. Log err.requestId and err.message
console.error('Self API error', { code: err.code, requestId: err.requestId });
}
}When details is populated
SelfApiError.details is the server's optional context object. It's set on the following errors:
unauthenticated + statusCode: 402 (billing gate): emitted when your organization's credit balance is below what the requested verification costs:
{
balance: number; // spendable credits at request time
required: number; // credits needed for this request
planTier: 'free' | 'starter' | 'enterprise';
creditGateMode: 'hard'; // always 'hard' (soft-gate orgs do not see this error)
}validation_failed + statusCode: 400: { issues: ZodIssue[] }. The SDK pre-validates request bodies and throws SelfValidationError before sending, so this server-side variant is normally caught client-side. It can surface if the SDK and server schemas fall out of sync; pin an exact SDK version in production to avoid this.
For every other code, details is undefined.
License
Licensed under the Business Source License 1.1. © 2025 Socialconnect Labs, Inc.
On 2029-06-11 the license converts automatically to the Apache License, Version 2.0.
