> ## Documentation Index
> Fetch the complete documentation index at: https://pulkit-fix-docs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Verified Human Verification

> Add server-side Verified Human verification to your application, confirming a user has completed document-based identity verification before granting access.

## Overview

[Verified Human](/billions-wallet/verified-human) is the credential the Billions Wallet issues after a user completes document-based verification, an NFC passport, Aadhaar, driving license, or national ID. Any backend can request proof of this credential from a user who holds it, then confirm the proof on its own servers before granting access, distributing a reward, or unlocking a feature.

This guide covers the full verification flow: requesting the credential, the user completing it in the Billions Wallet, and confirming the resulting proof on your backend.

<Info>
  This is a reference example to get you running locally and to show the shape of the integration. For a production rollout, reach out to the Billions integration team — they'll provide the issuer and schema values your verifier needs, and can walk you through rate limits and the persistent storage your deployment requires.
</Info>

## Verification Flow

A verification request is a standard DID-authentication request with one addition: a `scope` naming the exact credential and issuer you'll accept as proof.

<Steps>
  <Step title="User requests verification">
    Once a user is signed into your app, your frontend asks the backend for a verification request scoped to that user's session.
  </Step>

  <Step title="Backend builds a scoped authorization request">
    The backend generates an authorization request and attaches a proof request to it, naming the Verified Human credential it's asking the user to present.
  </Step>

  <Step title="User opens the Billions Wallet">
    The request is encoded into a Universal Link. Opening it in the Billions Wallet shows the user exactly what's being asked: proof that they hold a genuine Verified Human credential.
  </Step>

  <Step title="Wallet generates the proof">
    If the user holds the credential, the wallet builds a zero-knowledge proof against it and signs a token, without exposing the underlying document data.
  </Step>

  <Step title="Wallet posts to your callback">
    The wallet sends the signed token to your backend's callback endpoint, tagged with the session ID from Step 1.
  </Step>

  <Step title="Backend verifies and checks for replay">
    Your backend verifies the token against the request it stored, then checks the proof's nullifier, a privacy-preserving identifier unique to that person, to confirm the same human hasn't already verified through a different session.
  </Step>

  <Step title="Session marked verified">
    Once the nullifier check passes, mark the session verified in your store. From here you can gate access, unlock a reward, or anything else that depends on the user being a document-verified human.
  </Step>
</Steps>

## Verification Query Structure

This is the structure of the verification query used to make a verification request, naming the credential you're asking the user to present:

```javascript theme={null}
POVH: {
  name: "Verified Human Credential",
  verification_description: "Verify you are a verified human",
  circuitId: CircuitId.AtomicQueryV3Stable,
  query: {
    allowedIssuers: [
      "did:iden3:billions:test:2VxnoiNqdMPxzqp7X6MV7GfoPkDZ7ij499mDZAo72y",
      "did:iden3:billions:test:2VxnoiNqdMPyMXmEKpP8wGqrY6Vb7mgeQQUywyVeWe"
    ],
    context: "ipfs://QmZbsTnRwtCmbdg3r9o7Txid37LmvPcvmzVi1Abvqu1WKL",
    type: "BasicPerson"
  }
}
```

* **`circuitId`**: the circuit used to generate and verify the proof.
* **`allowedIssuers`**: the DIDs allowed to have issued the credential. Verified Human accepts proofs from either of Billions Network's issuers listed above.
* **`context`**: the schema context for the `BasicPerson` type, hosted on IPFS.
* **`type`**: the credential type being requested, always `BasicPerson` for Verified Human.

<Note>
  Need a custom query for your specific requirements, whether that's a variation on Verified Human, Proof of Uniqueness, or a combination of the two? Reach out to the Billions integration team to discuss it.
</Note>

## Setup Instructions

### Prerequisites

* Public URL for callbacks (use [ngrok](https://ngrok.com/) for local development)
* A Verifier DID

### Example Repository

The steps below follow the same verifier scaffold used for [Proof of Uniqueness verification](https://github.com/PrivadoID/Integration-examples/tree/main/POU-integration-for-verifiers%20\(For%20Privado%20Wallet\)) — clone it to follow along. Swapping in the query above is what adapts it to Verified Human.

### 1. Server and Request Handler

Sets up the server and the endpoint that builds a scoped verification request for each new session. `HOST_URL` and `AUDIENCE_DID` are read from configuration; the Verified Human issuer DIDs and schema context are the same values shown in [Verification Query Structure](#verification-query-structure) above.

```javascript theme={null}
require('dotenv').config();
const express = require("express");
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const { auth, resolver } = require("@iden3/js-iden3-auth");
const { CircuitId } = require('@0xpolygonid/js-sdk');
const { randomInt } = require('crypto');
const { v4: uuidv4 } = require('uuid');

const app = express();
const port = process.env.PORT || 8080;

app.use(helmet());
app.use(cors({ origin: '*' })); // restrict to your frontend's origin in production
app.use(rateLimit({ windowMs: 60 * 1000, max: 100 }));
app.use(express.json());

const requestMap = new Map();   // sessionId -> authRequest
const statusMap = new Map();    // requestId -> status

app.get("/api/verification-request", async (req, res) => {
  const sessionId = randomInt(10000);
  const uri = `${process.env.HOST_URL}/api/callback?sessionId=${sessionId}`;
  const nullifier = BigInt('0x' + uuidv4().replace(/-/g, '').slice(0, 16));

  const request = auth.createAuthorizationRequest(
    "Verify you are a verified human",
    process.env.AUDIENCE_DID,
    uri,
  );

  const proofRequest = {
    circuitId: CircuitId.AtomicQueryV3Stable,
    id: sessionId,
    params: { nullifierSessionId: nullifier.toString() },
    query: {
      allowedIssuers: [
        "did:iden3:billions:test:2VxnoiNqdMPxzqp7X6MV7GfoPkDZ7ij499mDZAo72y",
        "did:iden3:billions:test:2VxnoiNqdMPyMXmEKpP8wGqrY6Vb7mgeQQUywyVeWe",
      ],
      context: "ipfs://QmZbsTnRwtCmbdg3r9o7Txid37LmvPcvmzVi1Abvqu1WKL",
      type: "BasicPerson",
    },
  };

  request.body.scope = [proofRequest];
  requestMap.set(sessionId, request);
  statusMap.set(sessionId, "pending");

  res.status(200).json(request);
});

app.get("/api/status/:id", (req, res) => {
  const status = statusMap.get(parseInt(req.params.id)) || "not_found";
  res.status(200).json({ requestId: req.params.id, status });
});

app.listen(port, () => console.log(`Verifier backend running on port ${port}`));
```

<Info>
  **Get Your Verifier DID:** Sign in to your [Billions Wallet](https://wallet.billions.network/) and copy your profile DID to use during setup. This is your `AUDIENCE_DID` — it represents the entity doing the verification, which in this case is your application.
</Info>

<Note>
  Generate `nullifierSessionId` fresh for every request, as shown here. Computing it once at server startup and reusing it across sessions breaks the anti-replay check every subsequent verification depends on.
</Note>

### 2. Verification Callback

Verifies the proof the wallet sends back, and enforces that a given nullifier, a given real person, can only complete this verification once.

```javascript theme={null}
const getRawBody = require("raw-body");
const { AtomicQueryV3PubSignals } = require('@0xpolygonid/js-sdk');
const byteEncoder = new TextEncoder();

const userVerificationMap = new Map(); // nullifier -> { sessionId, verified }

app.post("/api/callback", async (req, res) => {
  const sessionId = parseInt(req.query.sessionId);
  const authRequest = requestMap.get(sessionId);
  if (!authRequest) {
    return res.status(400).json({ error: "Invalid or expired sessionId" });
  }

  const tokenStr = (await getRawBody(req)).toString().trim();

  const resolvers = {
    ["billions:main"]: new resolver.EthStateResolver(
      process.env.BILLIONS_RPC_URL,
      process.env.BILLIONS_CONTRACT,
    ),
  };

  const verifier = await auth.Verifier.newVerifier({
    stateResolver: resolvers,
    ipfsGatewayURL: process.env.IPFS_GATEWAY || "https://ipfs.io",
  });

  try {
    const authResponse = await verifier.fullVerify(tokenStr, authRequest, {
      AcceptedStateTransitionDelay: 5 * 60 * 1000,
    });

    const proof = authResponse.body.scope.find(
      (s) => s.circuitId === CircuitId.AtomicQueryV3Stable && s.id === sessionId,
    );
    if (!proof) {
      return res.status(400).json({ error: "No valid proof found in response." });
    }

    const pubSignals = new AtomicQueryV3PubSignals().pubSignalsUnmarshal(
      byteEncoder.encode(JSON.stringify(proof.pub_signals)),
    );
    const { nullifier } = pubSignals;

    if (userVerificationMap.get(nullifier)?.verified) {
      return res.status(400).json({ message: "This person has already verified." });
    }

    userVerificationMap.set(nullifier, { sessionId, verified: true });
    statusMap.set(sessionId, "success");

    res.status(200).json(authResponse);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
```

The nullifier is what makes this Sybil-resistant at the application layer: a stable, privacy-preserving identifier for the person behind the credential, not their DID or wallet address. Checking it against `userVerificationMap` before marking a session verified is what stops the same human from claiming a reward twice under two different accounts.

<Warning>
  `requestMap`, `statusMap`, and `userVerificationMap` are all in-memory here and reset on every restart. Replace them with a persistent store (Postgres, Redis) before going to production, or every in-flight session and past verification is lost the moment the process restarts.
</Warning>

### Testing Steps

1. Start the server: `node index.js`
2. Visit `http://localhost:8080`, the example ships a static demo page with a "Verify with Billions" button wired to the flow above.
3. Open the generated link in the Billions Wallet and complete verification with a Verified Human credential.
4. Poll `GET /api/status/<sessionId>` (or watch the demo page) until the status flips to `success`.

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="The callback endpoint never fires">
    `HOST_URL` must be a publicly reachable URL, not `localhost`. Use [ngrok](https://ngrok.com/) for local development, and confirm the callback URI includes the `sessionId` query parameter.
  </Accordion>

  <Accordion title="'No valid proof found in response'">
    The wallet returned a proof, but not one matching the `circuitId` and `id` your backend is looking for. This usually means the `id` on the `proofRequest` doesn't match the `sessionId` used to build the callback URI. They must be the same value.
  </Accordion>

  <Accordion title="'This person has already verified'">
    The nullifier from this proof already has a `verified: true` record, meaning this same human has completed verification through a different session already. This is the anti-Sybil check working as intended. If you see this unexpectedly during testing, it likely means you're reusing the same Verified Human credential across test sessions, which is expected behavior, not a bug.
  </Accordion>

  <Accordion title="Verification fails with a state resolution error">
    The `resolvers` map only knows about the networks it's configured for (`billions:main` , `privado:main`). Confirm your RPC and contract configuration is correct, and that the RPC endpoint is reachable from your server.
  </Accordion>
</AccordionGroup>

***

## Learn More

<Card title="Verified Human" icon="id-card" href="/billions-wallet/verified-human">
  What the credential is, and how users claim it.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.