@selfxyz/enterprise-sdk
v0.2.0
Published
Node/TS SDK for Self.xyz enterprise verification: session creation, webhook signature verification, typed payloads.
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.
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 (re-exported fromsvix).
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.
