Passkey-X
Build

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

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

Your first call (personal key)
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();
Organization API: audit events and signatures
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

OptionDefaultMeaning
apiKey / accessToken(required)pxu_…, pxk_…, pxa1.… or a session access token
baseUrlhttps://passkey-x.com/apiYour own /api for a self-hosted server
timeoutMs30000Timeout for each attempt (0 turns it off)
maxRetries2Retries for 408, 425, 429, 5xx and network errors
publishableKeynoneSessions only: the public apikey
fetchglobalThis.fetchA custom fetch, e.g. for proxies or tests
headersnoneExtra headers on every request
onRequest, onResponsenoneLogging and metrics hooks; they never receive the token
onDeprecationwarn once per routeCalled when an answer carries Deprecation / Sunset
cachefalseKeep 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.

Typed client: retries, idempotency, pagination and headers
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 } from RateLimit-* (or X-RateLimit-*).
  • responseMeta(body), px.lastResponse{ status, requestId, etag, link, next, deprecation, sunset, replayed }.
  • ifNoneMatch, cache: trueA 304 returns null, or the cached body with cache: true.
  • onDeprecationCalled on Deprecation / 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.

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.

Receive 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 }.

NamespaceCredentialMethods
pxAnyrequest(method, path, init)paginate(fetchPage)rateLimitlastResponsekind
pxCustomer APIhealth()me()itemTypes()
px.apiKeysCustomer APIlist()create(body)update(keyId, body)revoke(keyId)activity(keyId, params)
px.workspacesCustomer APIlist()members(workspaceId)
px.vaultItemsCustomer APIlist(params)iterate(params)get(itemId)create(envelope)update(itemId, revision, envelope)delete(itemId, revision)
px.syncCustomer APIchanges({ workspace_id, cursor })
px.accessRequestsCustomer APIlist(params)create(body)decide(requestId, body)
px.orgOrganization APIme()logs.iterate(params)auditEvents.iterate(params)itemTimeline(itemId)storage()insights()
px.org.alertsOrganization APIlist(params)iterate(params)update(alertId, body)
px.org.securityOrganization APIsummary()overview()incidents.iterate(params)deviceTrust()
px.org.policiesOrganization APIlist()set(policyType, body)stop(policyType)
px.org.membersOrganization APIiterate(params)invite(body)suspend(identityId)reactivate(identityId)block(identityId)unblock(identityId)remove(identityId)security.iterate(params)
px.org.onboardingOrganization APIiterate(params)remind(planId)extend(planId)resume(planId)cancel(planId)
px.org.handoversOrganization APIiterate(params)preview(params)create(body)
px.org.externalSharesOrganization APIiterate(params)approve(linkId)deny(linkId)
px.org.documentsOrganization APIiterate(params)get(documentId)verify(body)cancel(documentId, body)
px.org.signatureRequestsOrganization APIlist(params)iterate(params)
px.org.webhooksOrganization APIevents()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.controllerOrganization APIget()createJob(body)job(jobId)
px.org.remoteOrganization APIget()endSession(sessionId)invites()createInvite(body)revokeInvite(inviteId)
px.agentAgent APIvaults(params)
@passkey-x/sdk/webhooksAnyverifyWebhook({ body, headers, secrets })parseWebhook({ body, headers, secrets })WebhookVerificationError
@passkey-x/sdkAnyPasskeyXErrorresponseMeta(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.

Changelog

  1. 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.
  2. 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.