Quick Start
The fastest way to add ZKPassport to your app is the Verify with ZKPassport button from @zkpassport/ui. Clicking it opens ZKPassport's hosted verification page in a popup, which guides the user through the verification with the ZKPassport app and hands the proofs back to your page. You then verify the proofs on your server with @zkpassport/sdk. It works in React and in any vanilla JS / framework setup.
Installation
Install the UI package alongside the SDK using npm (or any JavaScript package manager such as yarn, pnpm or bun):
npm install @zkpassport/sdk @zkpassport/ui
No API key and no account are required to get started — just install the packages and you're good to go. Your app's name and logo come from your dashboard project; until you register your domain, the ZKPassport app shows the domain itself.
Add the verify button
The button takes a purpose to show the user and a query describing what to verify. Below we verify the user is 18 or older. When the user is done, onSuccess receives the proofs and the result, which you send to your server to be verified.
- React
- Vanilla JS
- Script tag
import { VerifyWithZKPassport } from "@zkpassport/ui/react-button";
export default function VerifyPage() {
return (
<VerifyWithZKPassport
purpose="Prove you are 18+ years old"
service={{ scope: "adult" }}
query={{ age: { min: 18 } }}
onSuccess={async ({ proofs, result }) => {
const response = await fetch("/api/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proofs, result }),
});
const { verified } = await response.json();
// Returning false shows the button's error state instead of success
return verified;
}}
/>
);
}
In the Next.js App Router, the React entry is marked "use client", so import it from a client component.
import { VerifyWithZKPassport } from "@zkpassport/ui/button";
// Renders into an existing element, e.g. <div id="zkpassport"></div>
const handle = VerifyWithZKPassport.render("#zkpassport", {
purpose: "Prove you are 18+ years old",
service: { scope: "adult" },
query: { age: { min: 18 } },
onSuccess: async ({ proofs, result }) => {
const response = await fetch("/api/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proofs, result }),
});
const { verified } = await response.json();
// Returning false shows the button's error state instead of success
return verified;
},
});
// handle.update(nextOptions) — swap options
// handle.unmount() — remove the button
VerifyWithZKPassport.render() takes an element or a selector and works the same in plain JS, Vue, Svelte, Solid, Astro, or any bundler-based stack.
Without a bundler, load the button from ZKPassport's CDN. Put both scripts at the end of <body>, loader first, so the element exists and the global is defined when render runs:
<div id="zkpassport"></div>
<script src="https://cdn.zkpassport.id/ui/v1.js"></script>
<script>
VerifyWithZKPassport.render("#zkpassport", {
purpose: "Prove you are 18+ years old",
service: { scope: "adult" },
query: { age: { min: 18 } },
onSuccess: async ({ proofs, result }) => {
const response = await fetch("/api/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proofs, result }),
});
const { verified } = await response.json();
// Returning false shows the button's error state instead of success
return verified;
},
});
</script>
v1 follows the latest release of the current API, 0.x included; a breaking change moves to v2. To pin a release instead, use https://cdn.zkpassport.id/ui/<version>.js. You still install @zkpassport/sdk on your server to verify the proofs.
The query is a plain object: one entry per attribute, with min/max for bounds, included/excluded for country sets, and disclose: true to reveal a value. See Basic Usage for the full shape.
Verify the proofs on your server
On your server, recreate the same query with createQuery() and pass it to verify() along with the proofs, the result, and the scope you used. Create the ZKPassport instance with the domain of the page the button runs on.
import { ZKPassport } from "@zkpassport/sdk";
const zkPassport = new ZKPassport("your-domain.com");
// Call this from the route behind POST /api/verify with the request body
export async function verifyProofs({ proofs, result }) {
// Recreate the query rather than accepting it from the browser
const { query } = zkPassport.createQuery().gte("age", 18).done();
const { verified, uniqueIdentifier } = await zkPassport.verify({
proofs,
originalQuery: query,
queryResult: result,
scope: "adult",
});
if (verified) {
console.log("User is 18+", result.age.gte.result);
// The same ID always gets the same identifier for your domain and scope
console.log("Unique identifier", uniqueIdentifier);
}
return { verified };
}
That's a complete, working verification flow. See the Client-Server example for a full server.
Where to go next
- Basic Usage — build richer queries, understand the full lifecycle callbacks, and drop down to the SDK directly when you need custom UI.
- Dashboard & Policies — manage branding and the exact request from the ZKPassport Dashboard and reference it by its policy id instead of building the query in code.
- Examples — age, nationality, residency, personhood, KYC, FaceMatch, and client-server verification.
Prefer a full project to start from? Check out our Next.js boilerplate here.