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.39

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:

  • ReclaimVerification.create(...) — create verification sessions and receive and verify results.
  • 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, verify Builder-signed result JWSs, discover JWKS keys, and verify exact proofs. Result signing is Builder-managed in api=2.

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.

Contents

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, legacy 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 to call the bootstrap endpoint:

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

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

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

Builder also returns an immutable, session-scoped feature configuration on VerificationSession.runtimeConfig. Read only keys the Verification Client implements, and use a safe fallback because older sessions can omit the snapshot:

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

const { data: bootstrap } = await builder.call('BootstrapVerificationClient', {
  params: { sessionId },
})
const useNewPicker = getBooleanFeatureFlag(
  bootstrap.session.runtimeConfig,
  'portal.newRequestPicker',
  false,
)

The /api and package-root entry points export the same browser-safe flag readers. Verification Clients never call the admin feature-flag endpoints or fetch live values during a session. See Verification Client feature flags.

Treat the Verification Client ID as deployment configuration, not as proof of identity. Builder requires it to match the client assigned to the session, but the public UUID doesn't authenticate the caller. Never put an organization secret, result-signing private key, or proof-owner private key in browser code. Builder owns result signing in Builder mode; claimant-facing Verification Clients receive no signing key. 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. Builder signs the result JWS with its managed ES256K key; verify it against the Builder's trusted issuer and well-known JWKS.

                          ┌─ one-time setup ─────────────────────────┐
  Configure callback in dashboard or raw API       ← where results go
  SetOrgKeypair via dashboard or raw API            ← optional encryption
                          └──────────────────────────────────────────┘
                          ┌─ 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 { ReclaimVerification } from '@reclaimprotocol/client'

const reclaim = ReclaimVerification.create({
  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.

The high-level verification client intentionally excludes callback management. Use the dashboard, the MCP create_org_callback_subscription tool, or call the generated CreateOrgCallbackSubscription operation with a human-authenticated ReclaimClient when automating organization setup. The consumer orgSecret does not authenticate this management operation.

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.api('SetOrgKeypair', {
  params: { orgId },
  body: {
    publicKey: publicKeyHex,
    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.
  orgEthPrivateKey,
})
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 { VerificationClient } from '@reclaimprotocol/client'

const session = await reclaim.sessions.create({
  providers: [
    // Blank/omitted → latest; 1.2.2 → exact; ^1.2.2 → >=1.2.2 <2.0.0.
    { providerId: 'prov_…', version: '^1.2.0' },
  ],
  context: { userId: 'abc', orderId: 'order-123' },
  verificationClientUrl: VerificationClient.portals,
})

// 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 resolves each provider independently and returns its exact resolvedVersion. Verification Clients execute that resolved recipe; they don't resolve versions themselves.

For imported legacy providers, pass consumer values in context. Builder-mode clients support {{context.key}}, then {{context_key}}, then {{key}} as compatibility aliases. Explicit request or provider-script parameters take precedence.

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. Imported recipes can fall back to {{context_key}} and then {{key}}. A lower-priority alias is used only when the recipe doesn't use a higher-priority spelling, and an explicit parameter always wins. Clients apply this to URLs, headers, request bodies, response matches, response redactions, provider scripts, and interceptor-created claims. This 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 declarative URLs for these intended uses. Builder owns terminal-result signing and encryption; claimant-facing clients receive no signing key, issuer, JWKS URL, or callback key:

| Registry name | Builder v2 use | | --- | --- | | builder | Builder's hosted claimant page at /v/{sessionId}. | | portals | Interactive web portal. The default when you omit the URL. | | verifier-app | Reclaim Verifier mobile app identifier. | | inapp-sdk | In-app SDK or add-to-app module identifier. | | reclaim-browser-extension | Browser extension identifier. | | zkfetch | Programmatic client in the separate ZKFETCH quota pool. |

Use VerificationClient to select a built-in client or supply a registered custom URL:

VerificationClient.builder
VerificationClient.portals
VerificationClient.verifierApp
VerificationClient.inAppSdk
VerificationClient.reclaimBrowserExtension
VerificationClient.zkFetch
VerificationClient.custom('https://verify.example.com')

Administrators can also register additional clients with their own URL.

Examples for the currently supported interactive clients:

import { VerificationClient } from '@reclaimprotocol/client'

// 1. Default (Portal) — omit verificationClientUrl.
const byDefault = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
})

// 2. Portal, selected explicitly.
const portal = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
  verificationClientUrl: VerificationClient.portals,
})

// 3. Reclaim Verifier app.
const verifierApp = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
  verificationClientUrl: VerificationClient.verifierApp,
})

// 4. In-app SDK or add-to-app host.
const inApp = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
  verificationClientUrl: VerificationClient.inAppSdk,
})

// 5. Browser extension.
const browserExtension = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
  verificationClientUrl: VerificationClient.reclaimBrowserExtension,
})

// 6. Builder's hosted claimant page.
const hosted = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
  verificationClientUrl: VerificationClient.builder,
})

// 7. Separately registered client or another Builder deployment.
const custom = await reclaim.sessions.create({
  providers: [{ providerId }],
  context,
  verificationClientUrl: VerificationClient.custom(
    'https://verify.example.com',
  ),
})

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 and save the returned session ID with your user or order. The session opens in the Portal unless you pass a VerificationClient URL.

import {
  type CallbackBody,
  ReclaimVerification,
} from '@reclaimprotocol/client'

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

async function createVerificationRequest(
  userId: string,
) {
  const session = await reclaim.sessions.create({
    providers: [{ providerId: 'prov_…', version: '^1.2.0' }],
    context: { userId },
  })
  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.

// One-time setup as the signed-in organization member, not with orgSecret.
import { ReclaimClient } from '@reclaimprotocol/client/api'
const setupClient = new ReclaimClient({ token: humanAccessToken })
await setupClient.call('CreateOrgCallbackSubscription', {
  params: { orgId },
  body: {
    callbackUrl: 'https://my.app/reclaim/callback/{{sessionId}}',
    events: [
      'verification_success',
      'verification_rejected',
      'verification_error',
      'verification_cancelled',
      'session_expired',
    ],
  },
})

// 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 Builder issuer and JWKS, verifies the outer result JWS, checks the session/audience and any legacy result expiry, and runs the package's verifyRequestProof 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, {
    orgEthPrivateKey: process.env.RECLAIM_ETH_PRIVATE_KEY,
    reclaimSessionId: 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 orgEthPrivateKey, 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. Use teeNonceMode: 'signature' on sessions.create for this compatibility mode.

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 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, {
  orgEthPrivateKey: process.env.RECLAIM_ETH_PRIVATE_KEY,
  reclaimSessionId: expected.sessionId,
  expectedAud: expected.orgId,
})
if (outcome.kind === 'result') {
  // These values came from verifyRequestProof. 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:

  • Raw OpenAPI client — use a human-authenticated ReclaimClient for CreateOrgCallbackSubscription. Use SetOrgKeypair through reclaim.api when configuring result encryption, which is useful for scripting or 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

ReclaimVerification.create(config)

Call ReclaimVerification.create(config) to return a client with sessions and results namespaces, 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 the optional ETH private key directly to sessions.create, results.receive, or results.decrypt. The SDK doesn't cache or serialize that key.

Use reclaim.export() to serialize non-secret configuration. Restore it with runtime credentials:

const saved = reclaim.export()
const restored = ReclaimVerification.import(saved, { orgSecret })

The JSON contains only a format version and Builder base URL. It never contains the organization secret, ETH private key, headers, or custom fetch function.

Verification clients

import { VerificationClient } from '@reclaimprotocol/client'
  • VerificationClient.builder, .portals, .verifierApp, .inAppSdk, .reclaimBrowserExtension, and .zkFetch contain production URLs.
  • VerificationClient.custom(uri) returns a separately registered client URL unchanged.

The helper doesn't make a URL trusted or register it in Builder. Your backend chooses an already registered URL, and sessions.create({ verificationClientUrl }) asks Builder to resolve that URL to its registered client UUID. Omit the URL to select portals.

reclaim.sessions

reclaim.sessions.create({ providers, context, verificationClientUrl?, orgEthPrivateKey? })
  // 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;
  // omitted, it defaults to the Portal.
  // orgEthPrivateKey optionally creates the post-create TEE binding locally.
  // The returned verificationUrl contains sessionId and api=2.
reclaim.sessions.bindTeeNonce(reclaimSessionId, { orgEthPrivateKey, timestamp? })
  // Explicit two-step/retry form. Reads the session, derives locally, and binds.
reclaim.sessions.get(reclaimSessionId)         // → VerificationSession
reclaim.sessions.listEvents(reclaimSessionId)  // → 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.results

reclaim.results.receive(body, opts?)        // → ReceivedDelivery  ← the 90% path
reclaim.results.decrypt(body, orgEthPrivateKey?) // → Jws
reclaim.results.verify(jws, opts?)          // → VerifyResultFullOutcome

Each verified result.proofs entry includes the exact proof received from the Verification Client, its provider and request identity, all attestor addresses, derived proof hashes, trusted data, unverified publicData, and the claimant/session and attestor TEE verification outcomes. This preserves the complete proof for storage or on-chain use while keeping trusted extracted parameters separate from unverified convenience data.

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

ReceiveOptions includes the reclaimSessionId check, expectedAud, trusted issuer configuration, attestor allowlists, and an optional orgEthPrivateKey. The private key decrypts encrypted results and verifies TEE proof binding. All three methods are async.

verifyResultFull is the recommended server-side API. Configure the canonical Builder issuer and well-known JWKS URL (or keep those values in your own server-side trust configuration):

await reclaim.results.verify(jws, {
  reclaimSessionId: session.id,
  expectedAud: orgId,
  trustedIssuers: ['https://build.reclaimprotocol.org'],
  jwksUrl: 'https://build.reclaimprotocol.org/.well-known/jwks.json',
})

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 Builder deployment inputs. Do not construct the JWKS URL from the JWS payload.

verifyResultFull calls this package's verifyRequestProof implementation for every exact Proof object in the signed result. It does not base64-decode, wrap, reconstruct, or otherwise transform those proofs. When you provide reclaimSessionId, every proof must also contain that session ID. This prevents a valid proof from an older session from being placed in a newly signed result envelope. For an older deployment that produced unbound proofs, set proofValidation.requireSessionBinding to false explicitly during its migration. A different embedded session ID is always rejected. By default, it uses each result's provider_id and exact resolved_version to resolve the provider hash requirements. You can instead pass hashes, proofValidation, or proofValidationByProviderId. These options mirror verifyRequestProof, including hasNoPii, verifier TEE (teeAttestation.appSecret), and attestor-TEE checks. The configured Builder JWKS URL supplies the purpose-bound result keys, its x-reclaim-attestors signer list, and its x-reclaim-attestor-image-digests Confidential Space image allowlist. When the JWKS publishes image digests, verifyResultFull verifies any attestor TEE claimAttestation a witness carries — its attestor address and claim signature, and its attestation_report pinned to those digests when present. Neither is required: like @reclaimprotocol/js-sdk, a witness is trusted by its signer being in x-reclaim-attestors. A portal TEE proof without a bound nonce has no claimAttestation, and production attestors sign without returning a report. When nothing was attested, isAttestorTeeAttestationVerified comes back false. Set proofValidation.attestorTeeAttestation to require a claimAttestation on every witness. If its expectedImageDigests is also set, the SDK uses the intersection: caller policy can narrow the published trust list, but can't broaden it. TEE settings preserve the same 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, {
    orgEthPrivateKey,
    reclaimSessionId,
  })
  // 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', … }.

Respond to a callback delivery

Builder retries a delivery until your callback route answers 2xx, and a delivery that never succeeds holds back that session's later events. If retrying the same body can't help, answer 2xx. If the failure is temporary, answer 5xx.

ResultVerificationError.retryable tells you which case you're in. It's true only when a temporary condition caused the failure: Builder's JWKS or provider recipe couldn't be fetched because of a network error, a timeout, an HTTP 5xx, or rate limiting (jwks-fetch-failed, session-resolution-failed, or an invalid-proof whose proofs all failed this way). It's false for everything about the result itself, including bad-signature, issuer-not-trusted, aud-mismatch, reclaimSessionId-mismatch, expired, and an invalid-proof with any genuinely invalid proof.

app.post('/reclaim/callback', async (req, res) => {
  try {
    const { result } = await reclaim.results.receive(req.body, {
      orgEthPrivateKey,
      reclaimSessionId,
    })
    await markVerificationComplete(result)
    res.sendStatus(200)
  } catch (err) {
    if (err instanceof ResultVerificationError) {
      await storeFailure(err.reason, err.outcome)
      // Retrying a bad result can't help, so only ask for a retry when the
      // failure is temporary.
      res.sendStatus(err.retryable ? 503 : 200)
      return
    }
    throw err
  }
})

results.receive can also throw an ordinary Error instead of ResultVerificationError, for example when orgEthPrivateKey is missing or doesn't match the key the result was encrypted to. Treat it as a configuration problem: log it and answer 5xx, so the delivery succeeds once you fix the key. Builder holds an undelivered result for 7 days.

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 (Builder encrypts terminal results; consumers only decrypt)
encryptForEthPublicKey(publicJwk, bytes): Promise<string>  // → ECIES ciphertext (base64url)
decryptCallback(decryptionKey, body): Promise<Jws>          // ECIES/plaintext → JWS

// Result JWS (Builder-managed ES256K)
verifyResult(jws, opts): VerifyResultOutcome                   // offline signature/nonce check (throws if invalid)
verifyResultFull(jws, opts): Promise<VerifyResultFullOutcome>  // + JWKS discovery + exact proof verification (throws if invalid)
verifyJwsSignature(jws, signerPublicKey), decodeJwsPayload(jws)

// JWKS (signature-key discovery)
fetchJwks(url), 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.