Verify with MID
MID's QR + Biometric OAuth2 flow turns an unverified visitor into a government-verified customer in about 30 seconds — and the verified data stays with the user, not on your servers.
How it works
Request claims
Your backend signs a request for the verified fields you need. MID returns a QR.
User approves
The user scans the QR with the MID app and approves with face or fingerprint.
Receive claims
MID posts the verified claims to your signed webhook in seconds.
The user must be enrolled
404 User not found — prompt them to download MID and enrol.1. Create a verification request
/v1/oauth/requestsSigned request — returns a sessionId and QR payload
Requires HMAC request signing, and the application must have OAUTH enabled in its request types.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| recipient | string | Yes | The enrolled user's phone number |
| claims | string[] | Yes | Verified fields to request (see below) |
| scopes | string[] | No | OAuth scopes, e.g. openid, profile, identity |
| redirectUri | string | Yes | OAuth redirect URI registered on your app |
| codeChallenge | string | Yes | PKCE challenge (S256) |
| codeChallengeMethod | string | No | S256 (default) or plain |
| callbackUrl | string | No | Webhook URL (defaults to the app callback) |
const body = {
recipient: '+2348012345678',
scopes: ['openid', 'profile', 'identity'],
claims: ['name', 'dob', 'address', 'nin', 'liveSelfie'],
redirectUri: 'https://yourapp.com/mid/callback',
callbackUrl: 'https://yourapp.com/api/mid/webhook',
codeChallenge, // base64url(sha256(codeVerifier))
codeChallengeMethod: 'S256',
};
const res = await fetch('https://api.dev.mobid.io/v1/oauth/requests', {
method: 'POST',
headers: midHeaders(API_KEY, API_SECRET, body), // see Authentication
body: JSON.stringify(body),
});
const { data } = await res.json();
// Render data.qr for the user; keep codeVerifier for the optional token exchange{
"error": false,
"message": "OAuth request created",
"data": {
"sessionId": "b0f2…",
"qr": "<base64 png>",
"type": "OAUTH",
"redirectUri": "https://yourapp.com/mid/callback",
"status": "pending"
}
}Available claims
| Claim | Description |
|---|---|
| name / firstName / lastName | Full or split legal name |
| dob | Date of birth (derive age client-side) |
| address | Residential address |
| phone | Verified phone number |
| Email address | |
| nin | Nigeria NIN — tokenised, pre-verified via NINAuth |
| liveSelfie | Biometric face-match result (liveness) |
| photo | Verified portrait |
2. Poll for browser completion
Webhooks are the primary server notification. For browser flows that need to leave the QR screen as soon as approval completes, poll the signed status endpoint with the returned session ID.
/v1/oauth/requests/statusSigned request — returns PENDING or approved claims
const body = { sessionId };
const res = await fetch('https://api.dev.mobid.io/v1/oauth/requests/status', {
method: 'POST',
headers: midHeaders(API_KEY, API_SECRET, body),
body: JSON.stringify(body),
});
const { data } = await res.json();
if (data.status === 'APPROVED') {
// Continue to your dashboard and prefill from data.claims.
}{
"error": false,
"data": {
"sessionId": "b0f2…",
"status": "APPROVED",
"recipient": "+2348012345678",
"scopes": ["openid", "profile", "identity"],
"claims": {
"name": { "value": "John Doe", "verified": true },
"address": {
"value": {
"street": "12 Marina Road",
"city": "Lagos",
"country": "Nigeria"
},
"verified": true
}
},
"approvedAt": "2026-06-19T10:32:05.000Z"
}
}Pending responses
status: "PENDING" and no claims. Poll with a short delay and stop when the request expires or reaches a terminal status.3. Receive verified claims
When the user approves, MID posts a signed webhook to your callbackUrl. For OAUTH requests it includes the approved, verified claim values directly.
{
"sessionId": "b0f2…",
"status": "APPROVED",
"timestamp": 1717171717000,
"data": {
"userId": "+2348012345678",
"authorizationCode": "mac_…",
"scopes": ["openid", "profile", "identity"],
"claimsApproved": ["name", "dob", "address", "nin", "liveSelfie"],
"claims": {
"name": { "value": "John Doe", "verified": true, "verificationStatus": "verified" },
"dob": { "value": "1996-05-20", "verified": true, "verificationStatus": "verified" },
"address": { "value": "Lagos, Nigeria", "verified": true, "verificationStatus": "verified" },
"nin": { "value": "•••••••8901", "verified": true, "verificationSource": "ninauth" },
"liveSelfie": { "value": "Face Match", "verified": true, "verificationStatus": "verified" }
}
}
}4. (Optional) Exchange the code for a token
For a standards-based OAuth flow, exchange the authorizationCode for an access token and claims using your PKCE codeVerifier.
/v1/oauth/tokenSigned request — exchange code + verifier for claims
const body = { code: authorizationCode, codeVerifier };
const res = await fetch('https://api.dev.mobid.io/v1/oauth/token', {
method: 'POST',
headers: midHeaders(API_KEY, API_SECRET, body),
body: JSON.stringify(body),
});
// { access_token, token_type: 'Bearer', expires_in: 3600, subject, scopes, claims }That's it