---
name: setup-x401-verifier
description: Protect an HTTP endpoint with x401 so callers must present a Verifiable Identity issued by Proof. Generate an ES256 key, publish a Client ID Metadata Document, challenge with PROOF-REQUEST and verify PROOF-RESPONSE.
representative_queries:
  - require a verified identity before serving my API
  - set up x401 on my server
  - verify a Proof credential in Node
  - check that the agent calling my endpoint is a verified person
---

# x401 Verifiable Identity

Use x401 to require a verified identity before serving a request.

[x401](https://x401.proof.com/spec/latest/) is a protocol for Verifiable Identity. When a client requests a protected resource, your server returns HTTP `401` with a `PROOF-REQUEST` header containing a signed [OID4VP](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) request. The client presents a Verifiable Credential issued by [Proof](https://www.proof.com/) and retries with a `PROOF-RESPONSE` header. Your server verifies the presentation and returns the resource. x401 works for both web and agentic traffic.

Examples use Node.js with Express. Adapt them to your stack.

## Identity lifecycle

```text
[Proof] -- Fetch Client ID Metadata Document (CIMD) --> [Server]        (one-time enrollment as a Relying Party)
[Client] -- Request protected resource without PROOF-RESPONSE --> [Server]
[Server] -- HTTP 401 with PROOF-REQUEST (signed OID4VP request) --> [Client]
[Client] -- Retry request with PROOF-RESPONSE (VP token from a Proof credential) --> [Server]
[Server] -- Verify VP token (signature, aud = clientId) --> [Server]
[Server] -- Return requested resource --> [Client]
```

## Install dependencies

```bash
npm install @proof.com/x401-node @proof.com/proof-vc-server
```

- [`@proof.com/x401-node`](https://github.com/proof/x401-node): build, encode, and decode x401 `PROOF-REQUEST` / `PROOF-RESPONSE` headers.
- [`@proof.com/proof-vc-server`](https://github.com/proof/proof-vc-common/tree/main/packages/server): create the CIMD, sign OID4VP requests, and verify Verifiable Credentials issued by Proof.

## Generate a key pair

The key pair signs your OID4VP requests. It must be `ES256` (P-256). Keep the private key secret; its public half is published in the CIMD.

```bash
openssl ecparam -name prime256v1 -genkey -noout -out x401-key.pem
```

## Expose a Client ID Metadata Document (CIMD)

Serve a [CIMD](https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-02.html) at a public URL. Proof fetches it and enrolls your business as a *Relying Party* on the [Proof network](https://www.proof.com/digital-id) through domain name verification. The business can also set up a Proof account manually.

The `clientId` must be the exact URI at which the CIMD is hosted.

```javascript
import express from "express";
import { readFileSync } from "node:fs";
import { createPrivateKey, createPublicKey } from "node:crypto";
import { createClientIdMetadataDocument } from "@proof.com/proof-vc-server";

const CLIENT_ID = "https://example.com/x401-client"; // must match the URL of the route below
const privateKey = createPrivateKey(readFileSync("x401-key.pem"));
const publicJwk = createPublicKey(privateKey).export({ format: "jwk" });

const app = express();

const document = await createClientIdMetadataDocument({
  environment: "sandbox",
  clientId: CLIENT_ID,
  clientName: "Example",
  redirectUris: ["https://api.proof.com/.well-known/agents-trust-list"], // Proof maintains a list of trusted agents allowed to present Verifiable Identities for x401
  jwks: [publicJwk],
});

app.get("/x401-client", (req, res) => {
  res.json(document);
});
```

## Protect your endpoint

Add middleware to each route that requires a verified identity:

1. No `PROOF-RESPONSE` header: respond `401` with a `PROOF-REQUEST` header carrying a signed request and a fresh nonce.
2. `PROOF-RESPONSE` present: decode it and verify the VP token with `aud` set to your `clientId`. Respond `401` if decoding or verification throws.

```javascript
import { randomUUID } from "node:crypto";
import { verifier as x401, HEADER, DC_API_PROTOCOL } from "@proof.com/x401-node";
import { createClient, createVerifier } from "@proof.com/proof-vc-server";

const proofClient = createClient({
  environment: "sandbox",
  clientId: CLIENT_ID,
  useSecuredAuthorizationRequest: true,
  privateKeyFactory: () => privateKey,
});
const proofVerifier = createVerifier({ environment: "sandbox" });

async function requireVerifiedIdentity(req, res, next) {
  const response = req.get(HEADER.PROOF_RESPONSE);

  if (response === undefined) {
    const request = await proofClient.signedDcApiRequest({
      scope: "urn:proof:params:scope:verifiable-credentials:basic",
      nonce: randomUUID(),
      expectedOrigins: ["https://api.proof.com/.well-known/agents-trust-list"],
    });
    const payload = x401.buildPayload({
      credentialRequirements: {
        digital: {
          requests: [{ protocol: DC_API_PROTOCOL.SIGNED, data: { request } }],
        },
      },
    });
    return res
      .status(401)
      .set(HEADER.PROOF_REQUEST, x401.encodePayload(payload))
      .send("x401 PROOF-RESPONSE required");
  }

  try {
    const artifact = x401.decodeResultArtifact(response);
    const presentation = await proofVerifier.verifyVPToken({
      encodedVPToken: artifact.credential_result.data.vp_token,
      aud: CLIENT_ID,
    });
    req.credential = presentation.proof_id_default[0];
  } catch {
    return res.status(401).send("x401 PROOF-RESPONSE invalid");
  }
  next();
}

app.get("/x401-protected", requireVerifiedIdentity, (req, res) => {
  res.json({ message: "Access granted", claims: req.credential.toJSON() });
});
```

`verifyVPToken` does not validate the nonce. Checking that `req.credential.getNonce()` matches the nonce you issued is a recommended security practice to prevent replay.

## Test your endpoint

```bash
curl -i https://example.com/x401-protected
```

The response is `401` with a `PROOF-REQUEST` header and the body `x401 PROOF-RESPONSE required`. With a valid `PROOF-RESPONSE`, the endpoint returns `200` and the verified claims.

## Reference

| Item | Value |
| --- | --- |
| `environment` | `"sandbox"` or `"production"`. Use the same value everywhere. |
| `clientId` | Exact URL of the CIMD. Also the `aud` checked on the VP token. |
| Agents trust list | `https://api.proof.com/.well-known/agents-trust-list`, used as `redirectUris` and `expectedOrigins`. |
| `scope` | `urn:proof:params:scope:verifiable-credentials:basic` discloses `given_name`, `family_name`, `age_equal_or_over.18`. |
| Headers | `HEADER.PROOF_REQUEST` = `PROOF-REQUEST`, `HEADER.PROOF_RESPONSE` = `PROOF-RESPONSE`. |
| `req.credential` | `ProofCredentialV1`. Properties: `givenName`, `familyName`, `birthDate`, `isOver18`, `isOver21`, `isOver65`, `isNationalUS`. Methods: `toJSON()`, `getClaims()`, `getNonce()`. |

## Related resources

- [x401 specification](https://x401.proof.com/spec/latest/)
- [Proof Digital Credentials overview](https://dev.proof.com/docs/digital-credentials-overview)
- [Proof integration guide](https://dev.proof.com/docs/integration)
- [@proof.com/x401-node](https://github.com/proof/x401-node)
- [@proof.com/proof-vc-server](https://github.com/proof/proof-vc-common/tree/main/packages/server)
- [OAuth Client ID Metadata Document (draft 02)](https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-02.html)
