Skip to main content

Salted Unique Identifiers (OPRF)

Every verification result comes with a unique identifier tied to the user's ID. By default, it is derived purely from the ID data, your domain, and the request scope — which means anyone with complete knowledge of the ID chip data (for example the government that issued the ID) could recompute it and recognize the user.

Salted unique identifiers (NullifierType.SALTED) close this gap. The user's device derives the identifier with the help of an extra secret (the "salt") held by an independent network of servers, using a technique called an OPRF (Oblivious Pseudo-Random Function):

  • The servers never see the user's data — they only receive a blinded value.
  • No single server holds the whole secret, so nobody can recompute the identifier on their own — not even with full knowledge of the ID data.
  • The result is still deterministic: the same ID yields the same identifier for your app, so you can still recognize returning users.

Use this when the identifier itself is sensitive — for example anonymous voting, whistleblowing platforms, or any personhood check where users must not be linkable to their real-world identity by anyone.

To request one:

  • Ask for a salted identifier: uniqueIdentifierType: "salted" under service on the @zkpassport/ui verify button, or uniqueIdentifierType: NullifierType.SALTED on the SDK's request() method.
  • Add strict FaceMatch to your query. Salted identifiers require it, as it confirms the ID is being used by its actual holder; the request throws an error without it.
import { VerifyWithZKPassport } from "@zkpassport/ui/react-button";

<VerifyWithZKPassport
purpose="Prove your personhood"
service={{ scope: "personhood", uniqueIdentifierType: "salted" }}
query={{ facematch: true }}
onSuccess={async ({ proofs, result }) => {
// Send the proofs to your server to verify them
const response = await fetch("/api/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proofs, result }),
});
return (await response.json()).verified;
}}
/>;

On your server, recreate the query and verify the proofs (see Quick Start). Pass the same uniqueIdentifierType to verify() so proofs with a different type of identifier fail verification:

import { NullifierType } from "@zkpassport/sdk";

const { query } = zkPassport.createQuery().facematch("strict").done();
const { verified, uniqueIdentifier } = await zkPassport.verify({
proofs,
originalQuery: query,
queryResult: result,
scope: "personhood",
uniqueIdentifierType: NullifierType.SALTED,
});
if (verified) console.log("Unique identifier", uniqueIdentifier);
info

verify() also returns the identifier's type as uniqueIdentifierType. In dev mode, mock proofs return NullifierType.SALTED_MOCK instead of NullifierType.SALTED, so you can tell real salted identifiers apart from test ones.