> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.withpersona.com/2020-05-18/relay-sdk-quickstart/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.withpersona.com/_mcp/server. # Server SDK Quickstart > Get started with Persona's server-side Relay SDK. The Server SDK handles the server-side steps of the Relay flow. It abstracts away Privacy Pass cryptography and handles retries automatically. > **Note** > > Looking for full API documentation? See the [Server SDK Reference](/relay-server-sdk-reference). #### See the Server SDK in action ## Installation #### Node.js **`npm`** ```bash title="npm" npm install @persona/sdk-node ``` **`yarn`** ```bash title="yarn" yarn add @persona/sdk-node ``` #### Go ```bash go get github.com/persona-id/relay-sdk-go ``` ## Setup #### Node.js ```typescript import Persona from '@persona/sdk-node'; const persona = new Persona({ apiKey: '' }); ``` Relay methods live on the `persona.relays` namespace. The environment is inferred from your API key: a sandbox key runs against [sandbox](/relay-sandbox), a production key against production. Requires `@persona/sdk-node` `0.4.0` or later. #### Go ```go import persona "github.com/persona-id/relay-sdk-go" client := persona.New(persona.Options{APIKey: ""}) ``` Relay methods live on the `client.Relays` namespace. The environment is inferred from your API key: a sandbox key runs against [sandbox](/relay-sandbox), a production key against production. Requires `relay-sdk-go` `0.1.1` or later. ## Creating a Relay session ![Relay Integration Overview](https://assets.withpersona.com/f_auto,q_auto/developer-docs/images/relay-integration-overview-phase1.png) Create the Relay session before starting your selected client-side integration. Store the Relay token and Relay secret on your server — never expose the Relay secret to the client. Return the Relay session access token to your 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. **`Node.js`** ```typescript title="Node.js" const { relayToken, relaySecret, relaySessionAccessToken } = await persona.relays.create({ claimType: "live_human_presence", encryptionKeyPem: "", // pass null to opt out of encryption }); // Return the Relay session access token to the client return { accessToken: relaySessionAccessToken }; ``` **`Go`** ```go title="Go" keyPEM := "" relay, err := client.Relays.Create(ctx, persona.CreateRelayParams{ ClaimType: "live_human_presence", EncryptionKeyPEM: &keyPEM, // pass nil to opt out of encryption }) if err != nil { return err } // Return the Relay session access token to the client accessToken := relay.RelaySessionAccessToken ``` Pass the Relay session access token to the [Embedded Widget](/relay-widget-usage), [Hosted Flow](/relay-hosted-flow), [iOS SDK](/relay-ios-sdk), or [Android SDK](/relay-android-sdk). ## Issuing a Privacy Pass A Privacy Pass is the billing unit for Relay. It is billed on creation and redeemed to fetch the claim result. Issuance uses your API key and identifies your platform to Persona for billing. Redemption uses the Privacy Pass token instead of your API key. Each Privacy Pass can only be redeemed once. Store `privacyPassToken` on your server and map it to the corresponding Relay session. Issuing a Privacy Pass depends only on the claim type — it is not tied to a specific relay, so it can be called at any time, even before the relay is created. 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](https://docs.withpersona.com/api-keys). > **Issue against the same claim type as your relay** > > The signing key the SDK fetches is determined by the claim type you pass, so each Privacy Pass is bound to that claim type. A pass can only redeem a relay created with the **same** claim type — if they don't match, the pass won't be able to redeem the claim. Always issue against the same claim type you used when creating the relay. **`Node.js`** ```typescript title="Node.js" const { privacyPassToken } = await persona.relays.issuePrivacyPass({ claimType: "live_human_presence", }); ``` **`Go`** ```go title="Go" issued, err := client.Relays.IssuePrivacyPass(ctx, persona.IssuePrivacyPassParams{ ClaimType: "live_human_presence", }) if err != nil { return err } privacyPassToken := issued.PrivacyPassToken ``` ## Redeeming the claim ![Relay Integration Overview](https://assets.withpersona.com/f_auto,q_auto/developer-docs/images/relay-integration-overview-phase3.png) Begin claim retrieval when your client-side integration indicates that the user-facing flow is complete: * **[Embedded Widget](/relay-widget-usage):** Call your server from `onComplete`. * **[Hosted Flow](/relay-hosted-flow):** Begin polling your backend when the user launches Hosted Flow. * **[iOS SDK](/relay-ios-sdk) and [Android SDK](/relay-android-sdk):** Call your server after the platform's normal Inquiry completion mechanism reports that the user-facing experience ended. 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. **`Node.js`** ```typescript title="Node.js" // Redeem the Privacy Pass and retrieve the claim result const { claimPayload, tokenConsumed } = await persona.relays.generateClaim({ relayToken, relaySecret, privacyPassToken, }); ``` **`Go`** ```go title="Go" // Redeem the Privacy Pass and retrieve the claim result claim, err := client.Relays.GenerateClaim(ctx, persona.GenerateClaimParams{ RelayToken: relay.RelayToken, RelaySecret: relay.RelaySecret, PrivacyPassToken: issued.PrivacyPassToken, }) if err != nil { return err } ``` ## Parsing the claim payload If you opted out of encryption, parse the claim payload directly: **`Node.js`** ```typescript title="Node.js" const claim = JSON.parse(claimPayload); console.log(claim.claim_type); console.log(claim.claim_result); // 'passed' or 'failed' console.log(claim.methodology); // array of MethodologyCalculation, if not hidden ``` **`Go`** ```go title="Go" var result struct { ClaimType string `json:"claim_type"` ClaimResult string `json:"claim_result"` } if err := json.Unmarshal([]byte(claim.ClaimPayload), &result); err != nil { return err } fmt.Println(result.ClaimType) fmt.Println(result.ClaimResult) // "passed" or "failed" ``` If you provided an encryption key, decrypt the payload with your private key first. **`Node.js`** ```typescript title="Node.js" import crypto from "crypto"; const decrypted = crypto.privateDecrypt( { key: "", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, }, Buffer.from(claimPayload, "base64"), ); const claim = JSON.parse(decrypted.toString("utf8")); console.log(claim.claim_type); console.log(claim.claim_result); // 'passed' or 'failed' ``` **`Go`** ```go title="Go" block, _ := pem.Decode([]byte("")) priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return err } ciphertext, err := base64.StdEncoding.DecodeString(claim.ClaimPayload) if err != nil { return err } plaintext, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, priv.(*rsa.PrivateKey), ciphertext, nil) if err != nil { return err } var result struct { ClaimType string `json:"claim_type"` ClaimResult string `json:"claim_result"` } if err := json.Unmarshal(plaintext, &result); err != nil { return err } fmt.Println(result.ClaimType) fmt.Println(result.ClaimResult) // "passed" or "failed" ``` See the [claim payload schema](/relay-schemas#claim-payload) for the full type definition. ## Full example **`Node.js`** ```typescript title="Node.js" import Persona from '@persona/sdk-node'; import crypto from 'crypto'; const persona = new Persona({ apiKey: '' }); // Step 1: Create a session and return the Relay session access token to the client const { relayToken, relaySecret, relaySessionAccessToken } = await persona.relays.create({ claimType: 'live_human_presence', encryptionKeyPem: '', }); return { accessToken: relaySessionAccessToken }; // Step 2 — Issue a Privacy Pass (any time before redemption; depends only on the claim type) const { privacyPassToken } = await persona.relays.issuePrivacyPass({ claimType: 'live_human_presence', }); // Steps 1–2 above are server-to-server. // After your client-side integration indicates verification is complete, redeem from your server. // Step 3 — Redeem the Privacy Pass and retrieve the claim result const { claimPayload, tokenConsumed } = await persona.relays.generateClaim({ relayToken, relaySecret, privacyPassToken, }); // Step 4 — Decrypt and parse the claim payload const decrypted = crypto.privateDecrypt( { key: '', padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, }, Buffer.from(claimPayload, 'base64') ); const claim = JSON.parse(decrypted.toString('utf8')); console.log(claim.claim_type); console.log(claim.claim_result); // 'passed' or 'failed' ``` **`Go`** ```go title="Go" package main import ( "context" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "log" persona "github.com/persona-id/relay-sdk-go" ) func main() { ctx := context.Background() client := persona.New(persona.Options{APIKey: ""}) // Step 1: Create a session and return the Relay session access token to the client keyPEM := "" relay, err := client.Relays.Create(ctx, persona.CreateRelayParams{ ClaimType: "live_human_presence", EncryptionKeyPEM: &keyPEM, }) if err != nil { log.Fatal(err) } // Return relay.RelaySessionAccessToken to the client // Step 2 — Issue a Privacy Pass (any time before redemption; depends only on the claim type) issued, err := client.Relays.IssuePrivacyPass(ctx, persona.IssuePrivacyPassParams{ ClaimType: "live_human_presence", }) if err != nil { log.Fatal(err) } // Steps 1–2 above are server-to-server. // After your client-side integration indicates verification is complete, redeem from your server. // Step 3 — Redeem the Privacy Pass and retrieve the claim result claim, err := client.Relays.GenerateClaim(ctx, persona.GenerateClaimParams{ RelayToken: relay.RelayToken, RelaySecret: relay.RelaySecret, PrivacyPassToken: issued.PrivacyPassToken, }) if err != nil { log.Fatal(err) } // Step 4 — Decrypt and parse the claim payload block, _ := pem.Decode([]byte("")) priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { log.Fatal(err) } ciphertext, err := base64.StdEncoding.DecodeString(claim.ClaimPayload) if err != nil { log.Fatal(err) } plaintext, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, priv.(*rsa.PrivateKey), ciphertext, nil) if err != nil { log.Fatal(err) } var result struct { ClaimType string `json:"claim_type"` ClaimResult string `json:"claim_result"` } if err := json.Unmarshal(plaintext, &result); err != nil { log.Fatal(err) } fmt.Println(result.ClaimType) fmt.Println(result.ClaimResult) // "passed" or "failed" } ``` > Get started with Persona's server-side Relay SDK.