@reclaimprotocol/client
v0.1.11
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
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 their users using Reclaim Protocol and verify the results.
One package gives you:
createReclaim(...)— a small, declarative client for the consumer happy path: create verification sessions, register callbacks, and receive + verify results.ReclaimClient— a low-level typed HTTP client (call('OperationId', args)) for any operation in the spec.- Crypto helpers — ECIES decryption with the org's eth key, result-JWS signing/verification, JWKS discovery, and nested attestor-proof verification.
Works in Node, Deno, browsers, and edge runtimes (it uses WebCrypto + @noble, no native deps).
The model in one paragraph
Authentication is a single per-org secret (rorg_…, the orgSecret) — a bearer credential; no signatures, no per-call secrets. The org is identified by an Ethereum (secp256k1) public key, set as its keypair. When that keypair's canEncryptResult is enabled, verification results are encrypted with ECIES (secp256k1 + AES-256-GCM) to the org's eth public key, and you keep the matching eth private key (0x-hex — the form any wallet/ethers emits) to decrypt. Ethereum also signs the result JWS with ES256K, which you verify against the Builder's 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 once with your org secret. orgId below is your organization's id (the org the secret belongs to).
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)
Tell the Builder where to POST verification results. This is one-time setup and can also be done from the dashboard or the agent — see Three ways to set up.
await reclaim.callbacks.register({ callbackUrl: 'https://my.app/reclaim/callback' })2. Enable encryption (optional, one-time)
By default results are delivered as plaintext signed JWS (still verified). To have the Builder encrypt results so only you can read them, register your org's eth public key with canEncryptResult: true. Use any eth key (the 0x-hex form wallets/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' },
})
console.log('open this:', session.verificationUrl) // hand to the end-userIt's a plain authenticated POST, so you don't need the SDK — curl works too (the org secret is the only auth):
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/v/{sessionId}"
}'
# → 201 { "id": "…", "verificationUrl": "…", "status": "pending", "mode": "…", … }4. Receive & verify the result
When the end-user finishes, the Builder POSTs to your callbackUrl. Pass the request body to results.receive. It decrypts (if encrypted) and verifies the signature + nested proofs, and throws if the result is invalid — so reaching the result branch means it's verified.
// in your callback HTTP handler:
const outcome = await reclaim.results.receive(requestBody, {
credential: process.env.RECLAIM_ETH_PRIVATE_KEY, // 0x-hex eth key; omit for plaintext
expectedReclaimSessionId: session.id,
})
if (outcome.kind === 'result') {
console.log('verified!', outcome.result.payload)
} else {
console.log('progress event:', outcome.event) // non-terminal notification
}Three ways to set up
The one-time setup (registering the callback subscription + the encryption keypair) can be done however suits you — they all hit the same API:
- This SDK —
reclaim.callbacks.register(...)+reclaim.keypair.set(...), as above (good for scripting/CI). - The dashboard — the org'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.
Whichever you use, at runtime your backend needs only the org secret (to authenticate) and the eth private key (to decrypt) — the results.receive(...) step is unchanged.
API reference
createReclaim(config)
Returns a Reclaim with four namespaces (sessions, callbacks, keypair, 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 config. Everything else is passed where it's used: the orgId to keypair.set/get, and the eth private key (0x-hex) to results.receive/decrypt.
reclaim.sessions
reclaim.sessions.create({ providers, context, verificationClientUrl? })
// providers: { providerId, version? }[] — proved in order, one combined result; all required.
// → VerificationSession (has `id`, `verificationUrl`, `status`, `mode`, …)
// verificationClientUrl defaults to the Builder's own /v/{sessionId} page.
reclaim.sessions.get(sessionId) // → VerificationSession
reclaim.sessions.listEvents(sessionId) // → VerificationEventRecord[]reclaim.callbacks
reclaim.callbacks.register({ callbackUrl, events? }) // events defaults to the 3 terminal events
// → CallbackSubscription. Org-scoped; no address/publicKey/secret.
reclaim.callbacks.list() // → CallbackSubscription[]
reclaim.callbacks.remove(subscriptionId) // → voidevents is any subset of VerificationEvent. The terminal events (verification_success / verification_rejected / verification_error, exported as TERMINAL_EVENTS) deliver the signed result; non-terminal events deliver a plaintext progress notification.
reclaim.keypair (the org'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 org's eth uncompressed public key (0x04 + 128 hex — the form generateEthKeypair()/ethers produce); alternatively pass publicKeyJwk (a secp256k1 JWK). set stores the JWK + derived eth address. There is exactly one keypair per org; 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 } // terminal: decrypted + verified
| { kind: 'event'; event: VerificationEvent; eventData: Record<string, unknown> } // non-terminal: plaintextopts (ReceiveOptions) is VerifyResultFullOptions (expectedReclaimSessionId nonce check, expectedAud, attestor allowlists, fetchImpl, now) plus an optional credential — the eth decryption key as a 0x-hex private key string (the key content, not a path or env name). All three methods are async; pass credential only when the delivery is encrypted.
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). So the result branch is 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) // e.g. '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 & 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 + attestor proofs (throws if invalid)
verifyJwsSignature(jws, signerPublicKey), decodeJwsPayload(jws)
// JWKS (signature-key discovery / publishing)
fetchJwks(url), jwksFromPrivateKey(ethPrivHex), selectKey(...), jwkToUncompressedPublicKey(jwk)
// Nested attestor-proof verification
verifyNestedProof(...): NestedProofOutcomeEth keypair utils (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 (used by content-hash binding).
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 ProblemError (RFC 9457). 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 org secret is a bearer credential — treat it like a password; rotate it from the dashboard (
POST /orgs/{orgId}/token, which invalidates the prior one). The server stores only an HMAC of it. - Encryption is ECIES over secp256k1 + AES-256-GCM (via
eciesjs/@noble) — runs in browsers, Node, 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.
