API Documentation

Everything you need to integrate with the India Identity Exchange using standard OpenID Connect and FAPI 2.0.

Quick start

The Identity Exchange implements OpenID Connect Core 1.0 on top of a FAPI 2.0 security profile. You can integrate with any standards-compliant OIDC library.

Discovery endpoint

GET https://exchange.identityexchange.in/.well-known/openid-configuration

Step-by-step

  1. Register your application and receive a client_id.
  2. Generate a PKCE pair — a random code_verifier and its S256 challenge.
  3. Push the authorization request (PAR) to obtain a request_uri.
  4. Redirect the user to the authorization endpoint with the request_uri.
  5. Exchange the code for tokens using DPoP-bound requests.
  6. Verify the ID token and extract verified claims.

Authentication flow

The exchange acts as an OpenID Provider (OP). Your application is the Relying Party (RP). Users authenticate via an upstream Identity Provider (IdP) — currently DigiLocker and Aadhaar.

Claims are verified by the IdP, minimised by our privacy engine, and forwarded only as boolean assertions (e.g. age_over_18: true) — your app never receives raw dates of birth or Aadhaar numbers.

http
POST /oauth2/par HTTP/1.1
Host: exchange.identityexchange.in
Content-Type: application/x-www-form-urlencoded
DPoP: <dpop-proof>

response_type=code
&client_id=your-client-id
&redirect_uri=https://app.example.com/callback
&scope=openid+age:over18
&code_challenge=<s256-challenge>
&code_challenge_method=S256
&state=<random-state>

PKCE + PAR

All authorization requests must use Pushed Authorization Requests (PAR, RFC 9126) with PKCE (RFC 7636, S256). Plain code_challenge_method is rejected.

Generate PKCE

typescript
import { createHash, randomBytes } from "node:crypto";

const codeVerifier = randomBytes(48).toString("base64url");
const codeChallenge = createHash("sha256")
  .update(codeVerifier)
  .digest("base64url");

PAR endpoint

POST /oauth2/par — returns a request_uri valid for 60 seconds.

DPoP setup

DPoP (RFC 9449) is required for all token requests. DPoP binds access tokens to your client's key pair, preventing token replay.

Generate a DPoP key pair

typescript
import { generateKeyPair, exportJWK } from "jose";

const { publicKey, privateKey } = await generateKeyPair("ES256", {
  extractable: true,
});
const publicJwk = await exportJWK(publicKey);

Create a DPoP proof

typescript
import { SignJWT } from "jose";

const dpopProof = await new SignJWT({
  htu: "https://exchange.identityexchange.in/oauth2/token",
  htm: "POST",
  iat: Math.floor(Date.now() / 1000),
  jti: crypto.randomUUID(),
})
  .setProtectedHeader({ alg: "ES256", typ: "dpop+jwt", jwk: publicJwk })
  .sign(privateKey);

Include the proof in every token request as the DPoP header. A new proof (fresh jti and iat) must be created for each request.

Supported claims

All claims are delivered as boolean assertions inside the ID token. Raw PII (dates of birth, Aadhaar numbers, mobile numbers) is never forwarded to your application.

ClaimTypeDescriptionSource
name_verifiedbooleanGovernment-issued name has been verifiedDigiLocker / Aadhaar
age_over_18booleanUser is 18 years of age or olderDigiLocker / Aadhaar
age_over_21booleanUser is 21 years of age or olderDigiLocker / Aadhaar
genderM | F | T | UVerified gender from identity documentDigiLocker / Aadhaar
aadhaar_linkedbooleanUser's account is linked to AadhaarDigiLocker
phone_verifiedbooleanMobile number has been verifiedAadhaar

Error codes

All errors follow RFC 6749 / RFC 9700 format. Check the error_description field for human-readable details.

json
{
  "error": "invalid_dpop_proof",
  "error_description": "DPoP proof has expired or iat is in the future"
}