How it works

The 401 handshake.

An agent hits a protected URL. The verifier responds with a set of identity requirements to proceed. The wallet signs a presentation. The agent retries. The whole exchange runs on headers an agent already knows how to read — no SDK, no out-of-band channels, no new transport.

01At the verifier

Declare the proof requirement

401 Unauthorized with Proof-Required carrying a base64url-encoded payload. Credentials, predicates, challenge, trust list — all in one header.

02At the agent

Build an OpenID4VP request

Copy dcql_query verbatim, use challenge.value as the OpenID4VP nonce, announce client_id, send to the wallet.

03Back at the verifier

Retry with a VP artifact

Wallet signs a presentation; the agent base64url-encodes the artifact and replays with Proof-Presentation. Verifier checks four things at once.

04At the token endpoint

Or exchange for a bearer

For multi-request flows, exchange the VC presentation for an OAuth token which can be used for the duration of the session.

01At the verifier

Declare the proof requirement.

When an agent hits a protected route without an Authorization header, the verifier responds 401 Unauthorized. The full proof requirement rides in a single response header, so no body parsing is required. The payload names the credential type, the predicates the wallet must satisfy, the challenge to bind the presentation to, and the trust list that fixes which issuers count.
HTTP responseHTTP
HTTP/1.1 401 Unauthorized
Proof-Required: <base64url-x401-payload>
Cache-Control: no-store
decoded payloadJSON
{
  "scheme": "x401",
  "version": "0.1.0",
  "proof": {
    "presentation_protocol": "openid4vp",
    "dcql_query": {
      "credentials": [
        {
          "id": "board_certification",
          "format": "jwt_vc_json",
          "meta": { "type_values": ["BoardCertificationCredential"] },
          "claims": [
            { "path": ["credentialSubject", "boardCertification", "status"], "values": ["active"] }
          ]
        }
      ]
    },
    "challenge": {
      "value": "x401:<base64url-utf8-verifier-id>:<nonce>",
      "expires_at": "2026-05-06T18:45:00Z"
    },
    "oauth": { "token_endpoint": "https://research.example.com/oauth/token" },
    "issuers": {
      "trust_establishment_url": "https://research.example.com/.well-known/x401/trust/board-certified-doctor-v1"
    },
    "request_id": "proof-template-board-certified-doctor-v1",
    "satisfied_requirements": ["urn:example:x401:satisfaction:board-certified-doctor:v1"]
  }
}
02At the agent

Build an OpenID4VP request.

The x401 payload is not a finished OpenID4VP request — the agent constructs one. Copy dcql_query verbatim. Drop the verifier's challenge.value in as the OpenID4VP nonce. Announce client_id — this is the agent identity the verifier back-references on the retry.
authorization requestJSON
{
  "response_type": "vp_token",
  "client_id": "did:web:agent.example",
  "nonce": "x401:aHR0cHM6Ly9yZXNlYXJjaC5leGFtcGxlLmNvbQ:uX7Vq3mZJH6MeN0qz2L7SQ",
  "dcql_query": { "credentials": [...] },
  "response_uri": "https://agent.example/wallet/callback/7c9e"
}
03Back at the verifier

Retry with a VP artifact.

The wallet returns a signed verifiable presentation. The agent packages it together with the original challenge and its own agent_id, base64url-encodes the JSON, and replays the request with Proof-Presentation set.

Verifier checks four things at once

  • Challenge match
  • Agent match
  • Credential satisfaction
  • Issuer trust

If they all pass, the resource is released on this very request.

VP artifactJSON
{
  "agent_id": "did:web:agent.example",
  "challenge": "x401:aHR0cHM6Ly9yZXNlYXJjaC5leGFtcGxlLmNvbQ:uX7Vq3mZJH6MeN0qz2L7SQ",
  "request_id": "proof-template-board-certified-doctor-v1",
  "vp_token": "<wallet-returned-vp-token>"
}
retryHTTP
GET /papers/medical-study-123 HTTP/1.1
Host: research.example.com
Proof-Presentation: eyJhZ2VudF9pZCI6ImRpZDp3...
04At the token endpoint

Or exchange for a bearer.

One-shot proofs are great for single requests; longer flows need amortization. The agent submits the same VP artifact to proof.oauth.token_endpoint using OAuth 2.0 Token Exchange. The verifier returns a short-lived Verification Token. From here the agent can use Authorization: Bearer on subsequent requests until the token expires.
token exchange requestHTTP
POST /oauth/token HTTP/1.1
Host: research.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange&
subject_token_type=urn:x401:params:oauth:token-type:vp_artifact&
subject_token=<base64url-vp-artifact-json>&
resource=https%3A%2F%2Fresearch.example.com%2Fpapers%2Fmedical-study-123
token responseJSON
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "papers:read",
  "x401": {
    "agent_id": "did:web:agent.example",
    "verifier_id": "https://research.example.com",
    "request_id": "proof-template-board-certified-doctor-v1",
    "satisfied_requirements": ["urn:example:x401:satisfaction:board-certified-doctor:v1"],
    "resource": "https://research.example.com/papers/medical-study-123",
    "method": "GET"
  }
}
Try it yourself

Walk this exact flow against six live demos.

Each demo returns a real HTTP 401 with a real Proof-Required challenge. Pop the diagnostic in the corner to see the headers move, then walk through the full four-exchange flow inline.