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-configurationStep-by-step
- Register your application and receive a
client_id. - Generate a PKCE pair — a random
code_verifierand its S256 challenge. - Push the authorization request (PAR) to obtain a
request_uri. - Redirect the user to the authorization endpoint with the
request_uri. - Exchange the code for tokens using DPoP-bound requests.
- 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.
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
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
import { generateKeyPair, exportJWK } from "jose";
const { publicKey, privateKey } = await generateKeyPair("ES256", {
extractable: true,
});
const publicJwk = await exportJWK(publicKey);Create a DPoP proof
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.
| Claim | Type | Description | Source |
|---|---|---|---|
name_verified | boolean | Government-issued name has been verified | DigiLocker / Aadhaar |
age_over_18 | boolean | User is 18 years of age or older | DigiLocker / Aadhaar |
age_over_21 | boolean | User is 21 years of age or older | DigiLocker / Aadhaar |
gender | M | F | T | U | Verified gender from identity document | DigiLocker / Aadhaar |
aadhaar_linked | boolean | User's account is linked to Aadhaar | DigiLocker |
phone_verified | boolean | Mobile number has been verified | Aadhaar |
Error codes
All errors follow RFC 6749 / RFC 9700 format. Check the error_description field for human-readable details.
{
"error": "invalid_dpop_proof",
"error_description": "DPoP proof has expired or iat is in the future"
}