@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/clientQuick 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 id1. 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-userChoose 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 status1. 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 SDK —
reclaim.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— thereclaimCLI / 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_CLIENTSmaps TypeScript-friendly keys to the stable registry namesbuilder,portals,verifier-app,inapp-sdk,reclaim-browser-extension, andzkfetch.KnownVerificationClientNameis the union of those string values.KnownVerificationClientUrlMapis 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) // → voidevents 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?) // → VerifyResultFullOutcomereceive branches on the delivery:
type ReceivedDelivery =
| { kind: 'result'; result: VerifyResultFullOutcome } // signed, decrypted, verified result
| { kind: 'event'; event: VerificationEvent; eventData: Record<string, unknown> } // plaintext lifecycle eventopts (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(...): NestedProofOutcomeETH 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 lackES256K. - The callback delivery is unauthenticated transport — never trust the raw POST body; trust only the decrypted, signature-verified result that
results.receivereturns.
