npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@reclaimprotocol/client

v0.1.29

Published

Typed SDK/HTTP client SDK for the Reclaim Protocol — the programmatic way to use Reclaim Protocol to verify **data points** about your users (a follower count, an account balance, an order). Can be directly integrated by apps to request verification for t

Readme

@reclaimprotocol/client

Use this typed HTTP client and SDK to request verifications and verify data points about your users, such as a follower count, an account balance, or an order. Integrate it directly into your app.

One package gives you:

  • createReclaim(...) — create verification sessions, register callbacks, and receive and verify results with a small declarative client.
  • ReclaimClient — call any operation in the spec with the low-level typed HTTP client (call('OperationId', args)).
  • Crypto helpers — decrypt ECIES results with the organization's ETH key, sign and verify result JWSs, discover JWKS keys, and verify exact legacy proofs.

Use the API-only entry point in browsers and edge runtimes. Use the package root in trusted runtimes that need proof verification, signing, or encryption.

Choose an entry point

Use the narrowest entry point that fits the code:

| Import | Use it for | Includes | | --- | --- | --- | | @reclaimprotocol/client/api | Browser Verification Clients, frontend dashboards, agents, and backend API calls | Browser-safe fetch transport plus OpenAPI-generated operation and schema types | | @reclaimprotocol/client | Consumer backends and other trusted runtimes | API client, session workflows, result verification, proof verification, signing, and encryption | | @reclaimprotocol/client/openapi | Type-only integrations | OpenAPI-generated types only |

Both runtime entry points export the exact same ReclaimClient, ReclaimClientOptions, ProblemError, and OpenAPI-generated types. The package root re-exports /api and adds the trusted-runtime SDK features. It doesn't define another API client or another problem-error type.

The /api dependency graph doesn't include proof, attestor, signing, encryption, or Node-only modules. The shared client doesn't set User-Agent, which browsers don't allow JavaScript to control. Pass an identifying header explicitly from a runtime that permits it.

import {
  isProblemError,
  ReclaimClient,
} from '@reclaimprotocol/client/api'

const builder = new ReclaimClient({
  baseUrl: 'https://build.reclaimprotocol.org',
  // Use a bearer token for consumer or administrative API calls.
  token,
})

try {
  const { data: session } = await builder.call('GetVerificationSession', {
    params: { sessionId },
  })
  console.log(session.status)
} catch (error) {
  if (isProblemError(error)) {
    console.error(error.output.statusCode, error.data.title)
  }
}

ProblemError stores the RFC 9457 document in error.data. It is Boom-compatible: @hapi/boom's isBoom(error) returns true, and error.output.statusCode contains the HTTP status. The API entry point creates this shape without importing the Boom runtime into browser bundles.

A registered Verification Client uses its session-bound UUID header instead of a consumer bearer token:

import { ReclaimClient } from '@reclaimprotocol/client/api'

const builder = new ReclaimClient({
  baseUrl: builderApiUrl,
  headers: { 'x-reclaim-vc-id': verificationClientId },
})

const { data: recipe } = await builder.call('GetVerificationRecipe', {
  params: { sessionId },
})

Treat the Verification Client ID as deployment configuration, not as proof of identity. Builder authorizes it only for the session assigned to that registered client. Never put an organization secret, result-signing private key, or proof-owner private key in browser code. Keep result signing in the Verification Client backend and call verifyResultFull from the consumer backend after receiving a result.

The model in one paragraph

Authentication uses one per-organization secret (rorg_…, the orgSecret) as a bearer credential; it uses no signatures or per-call secrets. An Ethereum (secp256k1) public key identifies the organization. When canEncryptResult is enabled, verification results are encrypted with ECIES (secp256k1 + AES-256-GCM) to the organization's ETH public key. Keep the matching ETH private key (0x-hex, the form any wallet or ethers emits) to decrypt. The Verification Client signs the result JWS with ES256K; verify it against that client's registered trusted JWKS.

                          ┌─ one-time setup ─────────────────────────┐
  reclaim.callbacks.register({ callbackUrl })      ← where results go
  reclaim.keypair.set(orgId, { publicKey,          ← identity + (optional)
                       canEncryptResult: true })      encrypt to your key
                          └──────────────────────────────────────────┘
                          ┌─ per verification ───────────────────────┐
  reclaim.sessions.create(...) → open session.verificationUrl
        … end-user completes it …
  POST → your callbackUrl → reclaim.results.receive(body) → verified payload
                          └──────────────────────────────────────────┘

Install

npm install @reclaimprotocol/client

Quick start (consumer)

Configure the client once with your organization secret. In the example, orgId identifies the organization that owns the secret.

import { createReclaim } from '@reclaimprotocol/client'

const reclaim = createReclaim({
  orgSecret: process.env.RECLAIM_ORG_SECRET!,  // rorg_… (issued by an org owner)
})
const orgId = process.env.RECLAIM_ORG_ID!      // your org's id

1. Register a callback (one-time)

Register the endpoint where Builder posts verification results. Complete this one-time setup with the dashboard or the agent too; see Three ways to set up.

await reclaim.callbacks.register({ callbackUrl: 'https://my.app/reclaim/callback' })

2. Enable encryption (optional, one-time)

By default, the Builder delivers plaintext signed JWSs. To have it encrypt results so only you can read them, register your organization's ETH public key with canEncryptResult: true. Use any ETH key (the 0x-hex form wallets and ethers emit), or mint one with the SDK:

import { generateEthKeypair } from '@reclaimprotocol/client'
// keep privateKeyHex secret — it decrypts results
const { privateKeyHex, publicKeyHex } = generateEthKeypair()
// (also doable from the dashboard, which can mint the keypair in-browser)
await reclaim.keypair.set(orgId, {
  publicKey: publicKeyHex,   // 0x04+128 hex (or pass publicKeyJwk)
  canEncryptResult: true,
})

3. Create a session (per verification)

const session = await reclaim.sessions.create({
  providers: [{ providerId: 'prov_…' }],   // one or more, proved in order → one combined result
  context: { userId: 'abc' },
  // Optional: snapshot this active organization theme onto the session.
  themeId: '550e8400-e29b-41d4-a716-446655440000',
  // Optional: derive and bind the legacy TEE nonce locally after creation.
  teeAttestation: { appSecret: organizationVerificationPrivateKey },
})
console.log('open this:', session.verificationUrl)   // hand to the end-user

Choose a verification client

sessions.create always creates a Builder v2 session. Select the claimant-facing Verification Client with its registered base URL:

import {
  KNOWN_VERIFICATION_CLIENTS,
  type KnownVerificationClientUrlMap,
} from '@reclaimprotocol/client'

const verificationClientUrls = {
  [KNOWN_VERIFICATION_CLIENTS.portals]:
    process.env.RECLAIM_PORTAL_URL,
  [KNOWN_VERIFICATION_CLIENTS.verifierApp]:
    process.env.RECLAIM_VERIFIER_APP_URL,
  [KNOWN_VERIFICATION_CLIENTS.inAppSdk]:
    process.env.RECLAIM_INAPP_URL,
  [KNOWN_VERIFICATION_CLIENTS.reclaimBrowserExtension]:
    process.env.RECLAIM_BROWSER_EXTENSION_URL,
} satisfies KnownVerificationClientUrlMap

const clientName = KNOWN_VERIFICATION_CLIENTS.portals
const verificationClientUrl = verificationClientUrls[clientName]
if (!verificationClientUrl) throw new Error(`${clientName} URL isn't configured`)

const session = await reclaim.sessions.create({
  providers: [
    // Blank/omitted version → latest; exact and npm semver ranges also work.
    { providerId: 'prov_…', version: '^1.2.0' },
  ],
  context: { userId: 'abc', orderId: 'order-123' },
  verificationClientUrl,
})

// Builder resolves the URL to a registered client, pins its UUID on the
// session, and adds the session id plus api=2. Share this returned URL.
console.log(session.verificationUrl)

Builder snapshots the selected theme at session creation. Omit themeId to use the organization's most recently updated active theme. If the organization has no active theme, the Verification Client uses its default presentation. A theme's optional localized consent block controls whether the claimant sees a consent screen before the first provider starts.

For a deliberate troubleshooting session, append diag=1 to the returned URL before opening it:

const verificationUrl = new URL(session.verificationUrl)
verificationUrl.searchParams.set('diag', '1')
window.location.assign(verificationUrl)

diag=1 is honored only with Builder mode's existing api=2 selector. It can enable logs containing personal data, so don't add it to normal production links. Redirect parameters are also client-owned: append them to the returned URL instead of putting them in the session request.

Use {{context.key}} in new Builder recipes for values supplied in context; Builder validates those through the provider's requiredContext. For an old DevTools recipe that still uses bare {{key}}, Builder-mode Verification Clients seed a missing parameters.key from scalar context.key. An explicit parameter wins. This compatibility alias applies only to api=2; it doesn't change legacy sessions. Builder-owned reclaimSessionId and attestationNonce remain context-only.

The value must exactly equal that deployment's registered verification_clients.uri, including its scheme, host, path, trailing slash, query, and optional {sessionId} placeholder. Do not substitute the placeholder yourself. Builder either substitutes it or adds a sessionId query parameter, then adds api=2. An unregistered or slightly different URL is rejected with HTTP 400.

The SDK exposes stable registry names for these intended uses. A standard deployment seeds all six rows, but an administrator must configure each row's URL, issuer, JWKS URL, and signing key before consumers select it:

| Registry name | How the consumer selects it | Builder v2 use | | --- | --- | --- | | builder | Omit verificationClientUrl. | Built-in /v/{sessionId} client; available whenever Builder is running. | | portals | Pass the exact registered portal URL. | Interactive web portal. Its deployment must configure the bridge URL and registered Client UUID. | | verifier-app | Pass the exact registered HTTPS app/universal-link URL. | Reclaim Verifier mobile app. Its build must configure the bridge URL and registered Client UUID. | | inapp-sdk | Pass the exact registered URL owned by the host app. | In-app SDK or add-to-app module. The host app must configure Builder transport and the registered Client UUID. | | reclaim-browser-extension | Pass the exact registered extension launch URL. | Browser extension. Use a build that supports Builder mode (api=2). Its existing legacy oprf-raw support is independent of Builder-mode hardening. | | zkfetch | Don't use as an interactive claimant client unless the deployment explicitly supports it. | Separate programmatic ZKFETCH quota pool, not one of the current portal/mobile Builder flows. |

KNOWN_VERIFICATION_CLIENTS is the public string map for these stable registry names:

{
  builder: 'builder',
  portals: 'portals',
  verifierApp: 'verifier-app',
  inAppSdk: 'inapp-sdk',
  reclaimBrowserExtension: 'reclaim-browser-extension',
  zkFetch: 'zkfetch',
}

Use KnownVerificationClientName when accepting a client choice in your own API and KnownVerificationClientUrlMap for deployment configuration. Validate untrusted input against the map's values. The names are stable; the URLs are not.

Client URLs are deployment configuration, not package constants. A seeded registry row can be only an identifier until an administrator replaces its URI with the real deployed client URL and configures its issuer, JWKS, bridge, and signing key. Get the exact value from the Builder administrator or the admin-only /admin/verification-clients registry; never accept it from a claimant or derive it from a signed result. An organization token cannot list the admin registry. Administrators can also register additional clients with their own URL, issuer, JWKS URL, and signing key.

Examples for the currently supported interactive clients:

// Replace these examples with exact values from your Builder registry.
const registeredClients = {
  portal: 'https://portal.your-deployment.example/',
  verifierApp: 'https://verify.your-deployment.example/app',
  inApp: 'https://app.your-deployment.example/reclaim',
  browserExtension: 'https://extension.your-deployment.example/launch',
}

// 1. Built-in Builder client — omit verificationClientUrl.
const builtIn = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
})

// 2. Portal — the URL must be the exact registered portals.uri.
const portal = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
  verificationClientUrl: registeredClients.portal,
})

// 3. Reclaim Verifier app — use its registered HTTPS app link.
const verifierApp = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
  verificationClientUrl: registeredClients.verifierApp,
})

// 4. In-app SDK/add-to-app host — use the host app's registered link.
const inApp = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
  verificationClientUrl: registeredClients.inApp,
})

// 5. Browser extension — use a deployed build with Builder-mode support.
const browserExtension = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
  verificationClientUrl: registeredClients.browserExtension,
})

The Consumer supplies a URL, not verificationClientId or x-reclaim-vc-id. Builder resolves and stores the UUID. The selected Verification Client reads its own configured UUID and sends it only on its session-bound Builder requests.

To add client-owned redirect data, modify only the returned URL after session creation:

const claimantUrl = new URL(portal.verificationUrl)
claimantUrl.searchParams.set(
  'redirectUrl',
  'https://merchant.example/verification-complete',
)
// `redirectUrl` is a portal convention. Another client can use another name.
openForClaimant(claimantUrl.toString())

Do not replace api or sessionId. Builder doesn't store or execute these redirect values, and callbacks remain organization-scoped subscriptions rather than per-session request fields.

Complete web application flow

Keep session creation and result verification on your backend. Give the frontend only the returned launch URL. Treat a client redirect as navigation, not as evidence that verification succeeded.

sequenceDiagram
  participant Backend as Consumer backend
  participant Frontend as Consumer frontend
  participant Client as Verification Client
  participant Builder
  Backend->>Builder: Create verification session
  Builder-->>Backend: Return session ID and launch URL
  Backend-->>Frontend: Return session ID and launch URL
  Frontend->>Client: Open launch URL
  Client->>Builder: Complete Builder-mode session
  Builder->>Backend: Deliver signed result callback
  Backend->>Backend: Verify every proof and save trusted claims
  Frontend->>Backend: Read saved verification status
  Backend-->>Frontend: Return verified status

1. Create the request on your backend. Keep RECLAIM_ORG_SECRET private, validate the requested client name against KNOWN_VERIFICATION_CLIENTS, map it to your deployment's registered URL, and save the returned session ID with your user or order.

import {
  type CallbackBody,
  createReclaim,
  KNOWN_VERIFICATION_CLIENTS,
  type KnownVerificationClientName,
  type KnownVerificationClientUrlMap,
} from '@reclaimprotocol/client'

const reclaim = createReclaim({
  orgSecret: process.env.RECLAIM_ORG_SECRET!,
})

const clientUrls: KnownVerificationClientUrlMap = {
  [KNOWN_VERIFICATION_CLIENTS.portals]: process.env.RECLAIM_PORTAL_URL!,
  [KNOWN_VERIFICATION_CLIENTS.verifierApp]:
    process.env.RECLAIM_VERIFIER_APP_URL!,
  [KNOWN_VERIFICATION_CLIENTS.inAppSdk]: process.env.RECLAIM_INAPP_URL!,
  [KNOWN_VERIFICATION_CLIENTS.reclaimBrowserExtension]:
    process.env.RECLAIM_BROWSER_EXTENSION_URL!,
}

async function createVerificationRequest(
  userId: string,
  clientName: KnownVerificationClientName,
) {
  const verificationClientUrl = clientName === KNOWN_VERIFICATION_CLIENTS.builder
    ? undefined
    : clientUrls[clientName]
  if (clientName !== KNOWN_VERIFICATION_CLIENTS.builder && !verificationClientUrl) {
    throw new Error(`${clientName} URL isn't configured`)
  }

  const session = await reclaim.sessions.create({
    providers: [{ providerId: 'prov_…', version: '^1.2.0' }],
    context: { userId },
    ...(verificationClientUrl ? { verificationClientUrl } : {}),
  })
  await savePendingVerification({
    sessionId: session.id,
    userId,
    orgId: session.orgId,
  })
  return { sessionId: session.id, verificationUrl: session.verificationUrl }
}

2. Launch it from your frontend. Open exactly the returned URL. Builder has already added sessionId and api=2. You may append a redirect parameter that the selected Verification Client supports, but don't replace either Builder parameter.

const response = await fetch('/api/verifications', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ client: 'portals' }),
})
const { sessionId, verificationUrl } = await response.json()

const claimantUrl = new URL(verificationUrl)
claimantUrl.searchParams.set(
  'redirectUrl',
  `${location.origin}/verification-returned?sessionId=${encodeURIComponent(sessionId)}`,
)
location.assign(claimantUrl.toString())

On the return page, ask your backend for the saved session status. Don't read a proof or trusted claim from the redirect URL.

3. Receive and verify on your backend. Register a terminal callback once. The {{sessionId}} placeholder is filled from Builder's session record and URL encoded. Persist the delivery before returning a successful HTTP status so a process crash can't lose it.

await reclaim.callbacks.register({
  callbackUrl: 'https://my.app/reclaim/callback/{{sessionId}}',
})

// HTTP callback handler. `sessionId` comes from the route path.
async function acceptCallback(sessionId: string, requestBody: CallbackBody) {
  const deliveryId = await saveCallbackDelivery({ sessionId, requestBody })
  await enqueueCallbackVerification(deliveryId)
  return new Response(null, { status: 202 })
}

Verify the durable delivery in a backend worker. Load the expected session and organization from your database; don't take either expected value from the callback payload. results.receive decrypts when needed, validates the registered Verification Client issuer and JWKS, verifies the outer result JWS, checks the session/audience and any legacy result expiry, and runs the package's legacy-compatible verifyProof checks for every proof.

async function verifyCallbackDelivery(deliveryId: string) {
  const delivery = await loadCallbackDelivery(deliveryId)
  const expected = await loadPendingVerification(delivery.sessionId)
  const outcome = await reclaim.results.receive(delivery.requestBody, {
    credential: process.env.RECLAIM_ETH_PRIVATE_KEY,
    expectedReclaimSessionId: expected.sessionId,
    expectedAud: expected.orgId,
  })

  if (outcome.kind !== 'result') return // optional progress subscription

  const trustedClaims = outcome.result.proofs?.flatMap((proof) => (
    proof.data ? [proof.data] : []
  )) ?? []
  await markVerificationComplete({
    sessionId: expected.sessionId,
    trustedClaims,
  })
}

results.receive throws ResultVerificationError for an invalid signature, issuer, audience, session binding, legacy result expiry, proof signature, proof hash, TEE, or attestor trust check. Mark a verification complete only after this call succeeds. Make callback storage idempotent because Builder can retry delivery.

Removing api=2 does not turn this request into a valid legacy request. To create a legacy verification, continue using the legacy JS SDK/backend request format. URLs without api=2 stay on each client's unchanged legacy parser.

With teeAttestation, the SDK creates the session, derives the selected exact legacy nonce, signs the Builder binding with EIP-191, and registers it at PUT /verifications/sessions/{sessionId}/tee-nonce. The private key is never sent to Builder. The binding is idempotent for identical values and immutable afterward; bind before proof generation starts. The default nonceMode: 'hash' uses RECLAIM_TEE_NONCE_V1. Set nonceMode: 'signature' for the older 65-byte EIP-191 nonce that legacy verifyProof also accepts.

Because this endpoint is an authenticated POST, you can use curl without the SDK. The organization secret is the only credential:

curl -X POST "$BUILDER_URL/verifications/sessions" \
  -H "Authorization: Bearer $RECLAIM_ORG_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
        "providers": [{ "providerId": "prov_…" }],
        "context": { "userId": "abc" },
        "verificationClientUrl": "https://your.app/"
      }'
# → 201 { "id": "…", "verificationUrl": "…&api=2", "status": "pending", "mode": "…", … }

For a TEE-bound session, a raw HTTP client must derive the legacy hash nonce with its organization verification private key, sign the exact RECLAIM_BUILDER_TEE_BINDING_V1:{organizationId}:{sessionId}:{applicationId}:{timestamp}:{attestationVersion}:{attestationNonce} payload with EIP-191, then call PUT /verifications/sessions/{sessionId}/tee-nonce with the same OrgToken. Never send the private key in either request.

4. Receive and verify the result

When the user finishes, Builder sends a POST request to your callbackUrl. Pass the request body to results.receive. It decrypts encrypted results, verifies the outer signature, and verifies every exact legacy proof. It throws if the result is invalid, so reaching the result branch means the result is verified.

// Load these expected values from your database, not from the callback body.
const expected = await loadPendingVerification(callbackRouteSessionId)
const outcome = await reclaim.results.receive(requestBody, {
  credential: process.env.RECLAIM_ETH_PRIVATE_KEY,  // 0x-hex ETH key; omit for plaintext
  expectedReclaimSessionId: expected.sessionId,
  expectedAud: expected.orgId,
})
if (outcome.kind === 'result') {
  // These values came from verifyProof. Don't trust diagnostic copies from
  // the outer payload for application decisions.
  const trustedClaims = outcome.result.proofs?.flatMap((proof) => (
    proof.data ? [proof.data] : []
  )) ?? []
  useVerifiedClaims(trustedClaims)
} else {
  console.log('progress event:', outcome.event)          // non-terminal notification
}

Three ways to set up

Choose any of these one-time setup methods; each uses the same API:

  • This SDKreclaim.callbacks.register(...) + reclaim.keypair.set(...), as above (good for scripting/CI).
  • The dashboard — the organization's web console has a callback-subscription form and a "Set encryption keypair" dialog that can generate the ETH keypair in your browser and download the private key for you.
  • The agent (@reclaimprotocol/agent — the reclaim CLI / MCP server) — handy while building and testing a provider locally.

At runtime, your backend needs only the organization secret for authentication and the ETH private key for decryption. The results.receive(...) step is unchanged.


API reference

createReclaim(config)

Call createReclaim(config) to return a Reclaim with four namespaces — sessions, callbacks, keypair, and results — plus an api escape hatch.

| ReclaimConfig field | type | required | notes | |---|---|---|---| | orgSecret | string | yes | The org secret rorg_…. Sent as Authorization: Bearer; identifies the org server-side. | | baseUrl | string | no | Defaults to the production Builder. | | fetch | typeof fetch | no | Custom fetch (tests, edge runtimes). | | headers | Record<string,string> | no | Extra headers merged into every request. |

orgSecret is the only required configuration value. Pass each other value to the method that uses it: orgId to keypair.set/get, the optional TEE private key (0x-hex) to sessions.create({ teeAttestation: { appSecret } }), and the ETH private key to results.receive/decrypt.

Known verification client names

import {
  KNOWN_VERIFICATION_CLIENTS,
  type KnownVerificationClientName,
  type KnownVerificationClientUrlMap,
} from '@reclaimprotocol/client'
  • KNOWN_VERIFICATION_CLIENTS maps TypeScript-friendly keys to the stable registry names builder, portals, verifier-app, inapp-sdk, reclaim-browser-extension, and zkfetch.
  • KnownVerificationClientName is the union of those string values.
  • KnownVerificationClientUrlMap is a partial map from those names to the exact URLs registered in one Builder deployment.

The map doesn't make a URL trusted and doesn't select a client by name in the Builder API. Your backend chooses a configured URL, and sessions.create({ verificationClientUrl }) asks Builder to resolve that URL to its registered client UUID. Omit the URL to select builder.

reclaim.sessions

reclaim.sessions.create({ providers, context, verificationClientUrl?, teeAttestation? })
  // providers: { providerId, version? }[] — proved in order, one combined result; all required.
  // → VerificationSession (has `id`, `verificationUrl`, `status`, `mode`, …)
  // verificationClientUrl selects a registered Verification Client by URL.
  // teeAttestation: { appSecret } — optional org verification private key;
  //   kept local while the SDK derives and registers the post-create TEE binding.
  // The returned verificationUrl contains sessionId and api=2.
reclaim.sessions.bindTeeNonce(sessionId, { appSecret, timestamp? })
  // Explicit two-step/retry form. Reads the session, derives locally, and binds.
reclaim.sessions.get(sessionId)         // → VerificationSession
reclaim.sessions.listEvents(sessionId)  // → VerificationEventRecord[]

The Builder resolves verificationClientUrl to the session's registered verificationClientId. It does not accept a per-session callback or redirect URL. After creation, the consumer may append redirect query parameters that the selected Verification Client understands; the Builder does not interpret or execute them. A URL without api=2 is handled by the selected client's legacy path.

reclaim.callbacks

reclaim.callbacks.register({ callbackUrl, events? })  // the dashboard preselects 5 terminal events
  // → CallbackSubscription. Org-scoped; no address/publicKey/secret.
reclaim.callbacks.list()                  // → CallbackSubscription[]
reclaim.callbacks.remove(subscriptionId)  // → void

events is any subset of VerificationEvent. Builder terminal events are verification_success, verification_rejected, verification_error, verification_cancelled, and session_expired. The dashboard preselects all five. Terminal result deliveries contain the signed result when one is available. Treat cancellation and expiry as terminal outcomes. Non-terminal events deliver plaintext progress notifications.

reclaim.keypair (the organization's single ETH encryption key)

await reclaim.keypair.set(orgId, { publicKey, canEncryptResult?: true, label?: 'prod' })  // → OrgKeypair
await reclaim.keypair.get(orgId)  // → OrgKeypair | undefined (undefined ⇒ keyless / plaintext delivery)

publicKey is the organization's ETH uncompressed public key (0x04 + 128 hex — the form generateEthKeypair() and ethers produce). Alternatively, pass publicKeyJwk (a secp256k1 JWK). set stores the JWK and derived ETH address. There is exactly one keypair per organization; set replaces it (rotation). canEncryptResult toggles whether results are encrypted to it (default false ⇒ plaintext). Keep the matching ETH private key (0x-hex) and pass it as the decryption key to results.receive/decrypt.

reclaim.results

reclaim.results.receive(body, opts?)        // → ReceivedDelivery  ← the 90% path
reclaim.results.decrypt(body, credential?)  // → Jws   (ECIES or plaintext → signed JWS)
reclaim.results.verify(jws, opts?)          // → VerifyResultFullOutcome

receive branches on the delivery:

type ReceivedDelivery =
  | { kind: 'result'; result: VerifyResultFullOutcome }  // signed, decrypted, verified result
  | { kind: 'event'; event: VerificationEvent; eventData: Record<string, unknown> } // plaintext lifecycle event

opts (ReceiveOptions) is VerifyResultFullOptions (expectedReclaimSessionId nonce check, expectedAud, trusted issuer configuration, attestor allowlists, fetchImpl, now, ignoreExpiration) plus an optional credential — the ETH decryption key as a 0x-hex private key string (the key content, not a path or environment variable name). All three methods are async; pass credential only when the delivery is encrypted.

verifyResultFull is the recommended server-side API. Configure either an explicit issuer/trustedIssuers list, or resolve the registered issuer for the signed session's verificationClientId:

await reclaim.results.verify(jws, {
  expectedReclaimSessionId: session.id,
  expectedAud: orgId,
  resolveIssuers: (verificationClientId) => registry.issuersFor(verificationClientId),
  resolveJwksUrl: (verificationClientId) => registry.jwksUrlFor(verificationClientId),
})

The SDK compares the signed header iss with that trusted set before it fetches JWKS. It never uses an untrusted iss as a network destination. Keep issuer resolution server-side and treat a missing or changed issuer as a verification failure.

Issuer trust and key discovery are separate inputs: resolve the registered verificationClientIssuer and verificationClientJwksUrl independently (or pass trustedIssuers and jwksUrl). Do not construct the JWKS URL from the JWS payload.

verifyResultFull calls this package's legacy-compatible verifyProof implementation for every exact Proof object in the signed result. It does not base64-decode, wrap, reconstruct, or otherwise transform those proofs. By default, it uses each result's provider_id and exact resolved_version to resolve the legacy provider hash requirements. You can instead pass hashes, proofValidation, or proofValidationByProviderId. These options mirror the legacy verifier, including hasNoPii, verifier TEE (teeAttestation.appSecret), and attestor-TEE checks. The same registered Builder JWKS URL supplies the purpose-bound result keys and its x-reclaim-attestors trust list. TEE settings preserve the legacy configuration objects; verifyResultFull does not accept injected verification hooks. The surrounding issuer, session, and context checks are stateless; persist accepted session IDs in your application to prevent replay.

For proof-hash comparison only, the verifier canonicalizes the equivalent redaction transport spellings oprf, oprf-mpc, and oprf-raw to _oprf. This keeps extension-produced proofs compatible without changing the proof, its signed claim parameters, or the redaction mode used to create it.

New Builder result JWS payloads don't contain exp; a verified callback is a durable result. Older Builder payloads can still contain exp. Those payloads use the producer-selected Unix timestamp and fail after that time by default. Set ignoreExpiration: true only when intentionally accepting a stored legacy result after its signed deadline. This option doesn't bypass Builder Session expiry, issuer trust, session binding, proof verification, or application-level replay prevention.

The result's extracted_parameters and content_hash fields are signed diagnostics. Use the verified data returned for each proof in result.proofs[].data; do not trust the diagnostic copies for an authorization decision.

Invalid results throw. receive and verify (and the underlying verifyResult / verifyResultFull) never return a failed verdict. They throw a ResultVerificationError carrying the failure reason and the full outcome (payload, reclaimSessionId, per-proof detail). The result branch is therefore always verified; wrap the call to inspect failures:

import { ResultVerificationError } from '@reclaimprotocol/client'
try {
  const { result } = await reclaim.results.receive(body, { credential })
  // result.payload is verified
} catch (err) {
  if (err instanceof ResultVerificationError) {
    console.error(err.reason, err.outcome)  // 'expired', 'bad-signature', 'invalid-proof', …
  } else {
    throw err
  }
}

A non-terminal event delivery is not a result and never throws. It returns { kind: 'event', … }.

reclaim.api — escape hatch

// fully typed against the OpenAPI spec — for any operation the namespaces don't wrap
const { data } = await reclaim.api('GetVerificationSession', { params: { sessionId } })

Crypto and key helpers

// Eth encryption keys (consumer side)
generateEthKeypair(): { privateKeyHex, publicKeyHex, publicJwk, ethAddress }  // mint an org keypair
loadDecryptionKey(hex): DecryptionKey            // 0x-hex eth private key → raw scalar
uncompressedPubkeyToJwk(hex): Jwk                // 0x04+128 hex → secp256k1 JWK
pubkeyJwkToEthAddress(jwk): string               // secp256k1 JWK → 0x eth address

// ECIES (encryptForEthPublicKey is used by the first-party Verification Client; consumers only decrypt)
encryptForEthPublicKey(publicJwk, bytes): Promise<string>  // → ECIES ciphertext (base64url)
decryptCallback(decryptionKey, body): Promise<Jws>          // ECIES/plaintext → JWS

// Result JWS (eth ES256K)
signResultJws(ethPrivHex, payload, issuer): Jws                // VC-side
verifyResult(jws, opts): VerifyResultOutcome                   // offline signature/nonce check (throws if invalid)
verifyResultFull(jws, opts): Promise<VerifyResultFullOutcome>  // + JWKS discovery + exact legacy-proof verification (throws if invalid)
verifyJwsSignature(jws, signerPublicKey), decodeJwsPayload(jws)

// JWKS (signature-key discovery / publishing)
fetchJwks(url), jwksFromPrivateKey(ethPrivHex), selectKey(...), jwkToUncompressedPublicKey(jwk)

// Legacy compatibility helper. verifyResultFull does not use an inner proof JWS.
verifyNestedProof(...): NestedProofOutcome

ETH keypair utilities (low-level)

generateKeypair(), deriveFromPrivateKey(hex), addressFromPublicKey(...), plus the secp instance and addressFromUncompressedPubkey(...), are exported for the Verification Client and the agent (proof-owner signing). A consumer that enables encryption holds an ETH private key (the same key family) to decrypt; see the encryption-key helpers above. canonicalJson (RFC 8785) is also exported for interoperable diagnostic hashes.

ReclaimClient (low-level)

import { ReclaimClient } from '@reclaimprotocol/client'
const client = new ReclaimClient({ baseUrl, token, fetch, headers })
const { data, response } = await client.call('GetVerificationSession', { params: { sessionId } })

Errors are thrown as the same Boom-compatible ProblemError exported by @reclaimprotocol/client/api. throwProblemError, DEFAULT_BASE_URL, USER_AGENT, and encoding helpers (b64url, base64urlToBytes, bytesToHex, hexToBytes) are also exported. The call method is fully typed against the generated OpenAPI operations.

Security notes

  • The organization secret is a bearer credential. Treat it like a password and rotate it from the dashboard (POST /orgs/{orgId}/token, which invalidates the previous secret). The server stores only an HMAC of it.
  • Encryption uses ECIES over secp256k1 + AES-256-GCM (via eciesjs / @noble). It runs in browsers, Node.js, Deno, and edge runtimes and uses the same ETH key family as signing.
  • Signature verification stays secp256k1 (not routed through jose) so it works on WebCrypto-only runtimes that lack ES256K.
  • The callback delivery is unauthenticated transport — never trust the raw POST body; trust only the decrypted, signature-verified result that results.receive returns.