Server SDK Quickstart

The Server SDK handles the server-side steps of the Relay flow. It abstracts away Privacy Pass cryptography and handles retries automatically.

Looking for full API documentation? See the Server SDK Reference.

Installation

$npm install @persona/relay-sdk-node

Setup

1import Persona from '@persona/relay-sdk-node';
2
3const persona = new Persona({ apiKey: <your_api_key> });

Relay methods are available on the persona.relays namespace.

Creating a Relay session

Call persona.relays.create() before rendering the widget. This returns a session access token to pass to your frontend, along with a relayToken and relaySecret that you must store securely on your server — never expose these to the client.

Recommended: Encrypt your claim payload

We recommend generating an asymmetric key pair so that the claim payload is encrypted and only decryptable by your server.

1const { relayToken, relaySecret, relaySessionAccessToken } = await persona.relays.create({
2 claimType: 'age_over18_united_kingdom',
3 encryptionKeyPem: '<your_public_key_pem>', // pass null to opt out of encryption
4});
5
6// Return only the access token to your frontend
7return { accessToken: relaySessionAccessToken };

Once your frontend has the access token, initialize the widget and wait for onComplete before proceeding. See Widget usage.

Issuing a Privacy Pass

A Privacy Pass is the billing unit for Relay — it is billed on creation and is what gets redeemed to fetch the claim result. Since issuance uses your API key, Persona knows your platform identity at this point for billing purposes. Redemption remains fully anonymous — Persona does not know who redeems the claim. Each Privacy Pass can only be redeemed once, so we recommend storing the privacyPassToken on your end and mapping it to the corresponding relay.

persona.relays.issuePrivacyPass requires a relay to exist — the challenge is obtained through the relay’s endpoint — but can be called at any point after relay creation and before redemption. Issuing immediately after creating the relay, before the widget completes, is recommended: it makes it harder for Persona to correlate your platform identity to a relay via timing. One Privacy Pass must exist before you can redeem any relay.

This step requires your Persona API key — you can find it in the Persona Dashboard under API Keys.

1const { privacyPassToken } = await persona.relays.issuePrivacyPass({
2 relayToken,
3 relaySecret,
4});

Redeeming the claim

onComplete should trigger a call to your server to run the following steps. The SDK handles the full blind RSA protocol internally.

The Privacy Pass is redeemed only on a successful claim. Since each pass can only be redeemed once, retrying a successful request with the same already-spent token — for example, after a network drop where your server never received the response — would normally result in a double-spend error. Idempotency is handled automatically by the SDK.

1// Redeem the Privacy Pass and retrieve the claim result
2const { claimPayload, tokenConsumed } = await persona.relays.generateClaim({
3 relayToken,
4 relaySecret,
5 privacyPassToken,
6});

Parsing the claim payload

If you opted out of encryption (encryptionKeyPem: null), parse claimPayload directly:

1const claim = JSON.parse(claimPayload);
2
3console.log(claim.claim_type); // e.g. 'age_over_18_united_kingdom'
4console.log(claim.claim_result); // 'passed' or 'failed'
5console.log(claim.methodology); // array of MethodologyCalculation, if not hidden

If you provided an encryptionKeyPem, decrypt the payload with your private key first:

1import crypto from 'crypto';
2
3const decrypted = crypto.privateDecrypt(
4 {
5 key: '<your_private_key_pem>',
6 padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
7 },
8 Buffer.from(claimPayload, 'base64')
9);
10
11const claim = JSON.parse(decrypted.toString('utf8'));
12
13console.log(claim.claim_type); // e.g. 'age_over_18_united_kingdom'
14console.log(claim.claim_result); // 'passed' or 'failed'

See the claim payload schema for the full type definition.

Full example

1import Persona from '@persona/relay-sdk-node';
2import crypto from 'crypto';
3
4const persona = new Persona({ apiKey: <your_api_key> });
5
6// Step 1 — Create a session and return the access token to your frontend
7const { relayToken, relaySecret, relaySessionAccessToken } = await persona.relays.create({
8 claimType: 'age_over18_united_kingdom',
9 encryptionKeyPem: '<your_public_key_pem>',
10});
11return { accessToken: relaySessionAccessToken };
12
13// Step 2 — Issue a Privacy Pass (any time after relay creation, before redemption)
14const { privacyPassToken } = await persona.relays.issuePrivacyPass({
15 relayToken,
16 relaySecret,
17});
18
19// ---- Widget handles user verification on the frontend ----
20// Initialize the Relay widget with the access token above.
21// onComplete should trigger Step 3 below. See: Widget usage
22
23// Step 3 — Redeem the Privacy Pass and retrieve the claim result
24const { claimPayload, tokenConsumed } = await persona.relays.generateClaim({
25 relayToken,
26 relaySecret,
27 privacyPassToken,
28});
29
30// Step 4 — Decrypt and parse the claim payload
31const decrypted = crypto.privateDecrypt(
32 {
33 key: '<your_private_key_pem>',
34 padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
35 },
36 Buffer.from(claimPayload, 'base64')
37);
38
39const claim = JSON.parse(decrypted.toString('utf8'));
40
41console.log(claim.claim_type); // e.g. 'age_over_18_united_kingdom'
42console.log(claim.claim_result); // 'passed' or 'failed'