JavaScript and TypeScript SDK
The official SDK: one client for every API with retries, pagination, typed errors, idempotency and webhook verification. Zero dependencies.
- Package
@passkey-x/sdk- Version
- 1.1.0
- Runtimes
- Node.js 18+, Deno, Bun, Workers, browsers
- Dependencies
- None
- Hosted
https://passkey-x.com/sdk/v1/index.mjs
Features
- Every API, one clientCustomer API (pxu_ keys or a session), Organization API (pxk_ keys) and Agent API (pxa1. tokens), with typed methods for every route.
- Safe retriesBackoff with jitter for 429, 5xx and network errors, honouring Retry-After. Writes carry an Idempotency-Key, so a retried POST never runs twice.
- Pagination built inEvery list has .iterate(): for await over all rows, following next_cursor or the Link rel="next" header for you.
- Typed errorsPasskeyXError with status, the API's error code, the request id for support, and retryAfter.
- Webhook verificationverifyWebhook checks the HMAC-SHA256 signature over the raw body in constant time, rejects replays, and accepts both signatures (and both secrets) while you rotate.
- Headers, read for youpx.rateLimit from RateLimit-Limit / -Remaining / -Reset, responseMeta(body) for X-Request-Id, ETag, Link and Idempotent-Replayed, an ETag cache with If-None-Match, and a warning (or onDeprecation) on Deprecation / Sunset.
- Zero knowledge, zero dependenciesVault items stay ciphertext. No dependencies; runs on Node.js 18+, Deno, Bun, Workers, edge functions and browsers. Full TypeScript types.
Install
# In your project (from the SDK kit or the Passkey-X repository)
npm install ./passkey-x-sdk
# Deno, edge runtimes and scripts: import it straight from Passkey-X
import { PasskeyX } from "https://passkey-x.com/sdk/v1/index.mjs";The kit’s files are listed below. For a self-hosted server, pass your own /api as baseUrl.
Quick start
import { PasskeyX } from "@passkey-x/sdk";
const px = new PasskeyX({ apiKey: process.env.PASSKEY_X_API_KEY }); // pxu_… from the API keys tab
const me = await px.me();
console.log(me.key?.name, me.key?.scopes, me.your_ip);
const vaults = await px.workspaces.list();const org = new PasskeyX({ apiKey: process.env.PASSKEY_X_ORG_KEY }); // pxk_…
// Every audit event, page by page, without handling cursors
for await (const event of org.org.auditEvents.iterate({ limit: 500 })) {
console.log(event.sequence, event.occurred_at, event.action);
}
// Signature requests still waiting
for await (const request of org.org.signatureRequests.iterate({ status: "pending" })) {
console.log(request.label, request.signed, "of", request.total, "signed");
}Options
| Option | Default | Meaning |
|---|---|---|
apiKey / accessToken | (required) | pxu_…, pxk_…, pxa1.… or a session access token |
baseUrl | https://passkey-x.com/api | Your own /api for a self-hosted server |
timeoutMs | 30000 | Timeout for each attempt (0 turns it off) |
maxRetries | 2 | Retries for 408, 425, 429, 5xx and network errors |
publishableKey | none | Sessions only: the public apikey |
fetch | globalThis.fetch | A custom fetch, e.g. for proxies or tests |
headers | none | Extra headers on every request |
onRequest, onResponse | none | Logging and metrics hooks; they never receive the token |
onDeprecation | warn once per route | Called when an answer carries Deprecation / Sunset |
cache | false | Keep GET answers with their ETag and revalidate with If-None-Match (a 304 reuses them) |
Pagination
Every list has .iterate(params): for await over every row, following next_cursor (or the Link rel="next" header when a body has none). px.paginate(fetchPage) does the same for any route. See Pagination and rate limits.
Retries and idempotency
429, 5xx and network errors are retried with exponential backoff and jitter, following Retry-After (maxRetries, default 2). Every write carries an Idempotency-Key, the same on every retry; pass idempotencyKey to make a retry a no-op across processes. See Idempotency.
import { PasskeyX, responseMeta, type Page } from "@passkey-x/sdk";
type Member = { identity_id: string; email: string; role: string; status: string };
const org = new PasskeyX({
apiKey: process.env.PASSKEY_X_ORG_KEY!, // pxk_…
maxRetries: 4, // 429 / 5xx / network: backoff with jitter, honours Retry-After
cache: true, // GETs revalidate with If-None-Match; a 304 reuses the body
onDeprecation: ({ path, sunset }) => console.warn("deprecated", path, "sunset", sunset),
});
// Pagination: every page, following next_cursor (or Link rel="next")
for await (const row of org.org.members.iterate({ limit: 200 })) {
const member = row as unknown as Member;
if (member.status === "suspended") console.log(member.email);
}
// Idempotency: the same key on every retry, and across processes if you pass your own
const page = await org.request<Page<Member>>("POST", "/org/v1/members", {
body: { email: "new.hire@example.com", role: "member" },
idempotencyKey: `invite:new.hire@example.com:2026-10-21`,
});
const meta = responseMeta(page);
console.log(meta?.requestId, meta?.replayed ? "replayed" : "created", org.rateLimit?.remaining, "left");Headers
px.rateLimit{ limit, remaining, reset, policy }fromRateLimit-*(orX-RateLimit-*).responseMeta(body),px.lastResponse{ status, requestId, etag, link, next, deprecation, sunset, replayed }.ifNoneMatch,cache: trueA304returnsnull, or the cached body withcache: true.onDeprecationCalled onDeprecation/Sunset; without it the SDK warns once per route.
Every header is described in HTTP headers.
Errors
PasskeyXError has status, the API’s code, requestId, field, retryAfter and retryable. Codes are listed in Errors.
import { PasskeyXError } from "@passkey-x/sdk";
try {
await px.vaultItems.update(itemId, revision, envelope); // If-Match: someone else's change is never overwritten
} catch (error) {
if (error instanceof PasskeyXError) {
console.error(error.status, error.code, error.requestId); // e.g. 409 conflict, 403 insufficient_scope, 429 rate_limited
}
}Webhooks
verifyWebhook({ body, headers, secrets }) checks the signature over the raw body in constant time, rejects stale timestamps and accepts two secrets while you rotate. parseWebhook also returns the parsed payload. See Webhooks.
import { verifyWebhook } from "@passkey-x/sdk/webhooks";
// While a secret rotates, deliveries carry v1=<new>,v1=<old>: keep both secrets until the overlap ends.
// Next.js route handler (app/api/passkey-x/route.ts); the same works in Express, Workers or Deno.
export async function POST(request: Request) {
const body = await request.text(); // verify the RAW body before parsing it
try {
await verifyWebhook({ body, headers: request.headers, secrets: [process.env.PX_WEBHOOK_SECRET!, process.env.PX_WEBHOOK_SECRET_OLD].filter(Boolean) as string[] });
} catch {
return new Response("invalid signature", { status: 401 });
}
const { events } = JSON.parse(body); // at-least-once and in order: deduplicate on event.sequence
return new Response(null, { status: 204 });
}Methods
Any route that isn’t wrapped yet works through px.request(method, path, init), for example px.request("GET", "/org/v1/storage"). Writes accept { idempotencyKey, signal, timeoutMs, maxRetries, headers }.
| Namespace | Credential | Methods |
|---|---|---|
px | Any | request(method, path, init)paginate(fetchPage)rateLimitlastResponsekind |
px | Customer API | health()me()itemTypes() |
px.apiKeys | Customer API | list()create(body)update(keyId, body)revoke(keyId)activity(keyId, params) |
px.workspaces | Customer API | list()members(workspaceId) |
px.vaultItems | Customer API | list(params)iterate(params)get(itemId)create(envelope)update(itemId, revision, envelope)delete(itemId, revision) |
px.sync | Customer API | changes({ workspace_id, cursor }) |
px.accessRequests | Customer API | list(params)create(body)decide(requestId, body) |
px.org | Organization API | me()logs.iterate(params)auditEvents.iterate(params)itemTimeline(itemId)storage()insights() |
px.org.alerts | Organization API | list(params)iterate(params)update(alertId, body) |
px.org.security | Organization API | summary()overview()incidents.iterate(params)deviceTrust() |
px.org.policies | Organization API | list()set(policyType, body)stop(policyType) |
px.org.members | Organization API | iterate(params)invite(body)suspend(identityId)reactivate(identityId)block(identityId)unblock(identityId)remove(identityId)security.iterate(params) |
px.org.onboarding | Organization API | iterate(params)remind(planId)extend(planId)resume(planId)cancel(planId) |
px.org.handovers | Organization API | iterate(params)preview(params)create(body) |
px.org.externalShares | Organization API | iterate(params)approve(linkId)deny(linkId) |
px.org.documents | Organization API | iterate(params)get(documentId)verify(body)cancel(documentId, body) |
px.org.signatureRequests | Organization API | list(params)iterate(params) |
px.org.webhooks | Organization API | events()get(webhookId)create(body)update(webhookId, body)delete(webhookId)rotateSecret(webhookId, body)test(webhookId)deliveries(webhookId, params)deadLetters(webhookId)replayDeadLetter(webhookId, deadLetterId)replay(webhookId, body) |
px.org.controller | Organization API | get()createJob(body)job(jobId) |
px.org.remote | Organization API | get()endSession(sessionId)invites()createInvite(body)revokeInvite(inviteId) |
px.agent | Agent API | vaults(params) |
@passkey-x/sdk/webhooks | Any | verifyWebhook({ body, headers, secrets })parseWebhook({ body, headers, secrets })WebhookVerificationError |
@passkey-x/sdk | Any | PasskeyXErrorresponseMeta(body)parseLinkHeader(value)readRateLimit(headers)pageRows(page)credentialKind(token) |
SDK kit files
Put these files in a folder named passkey-x-sdk/src (package.json and README.md in passkey-x-sdk), then npm install ./passkey-x-sdk. Or import https://passkey-x.com/sdk/v1/index.mjs directly.
- index.mjsThe client
- index.d.tsTypeScript types
- webhooks.mjsWebhook verification
- webhooks.d.tsIts types
- package.jsonnpm package manifest
- README.mdGuide and options
Changelog
- 1.1.0
- Reads the gateway headers: px.rateLimit from RateLimit-* (or X-RateLimit-*), responseMeta(body) and px.lastResponse with request id, ETag, Link, Deprecation, Sunset and replayed.
- paginate / .iterate() follow next_cursor, or Link rel="next" when a body has none.
- cache: true keeps GET answers with their ETag and revalidates with If-None-Match; onDeprecation is called on Deprecation / Sunset (otherwise one warning per route).
- A 409 idempotency_in_progress is retried; writes keep the same Idempotency-Key on every retry.
- Webhooks 2: rotateSecret with overlap_hours (verifyWebhook accepts two secrets), deadLetters and replayDeadLetter.
- 1.0.0
- One client for the Customer, Organization and Agent APIs, with retries, timeouts, Idempotency-Key on writes, If-Match revisions, typed errors and verifyWebhook.
Need help building on Passkey-X?
Questions about the API, a bug or a missing endpoint? Our team replies by email, usually within one business day.