Guide

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

Step 1

Request claims

Your backend signs a request for the verified fields you need. MID returns a QR.

Step 2

User approves

The user scans the QR with the MID app and approves with face or fingerprint.

Step 3

Receive claims

MID posts the verified claims to your signed webhook in seconds.

The user must be enrolled

Verification targets a user who has completed MID enrolment in the mobile app. If the phone number is not enrolled, the request returns 404 User not found — prompt them to download MID and enrol.

1. Create a verification request

POST
/v1/oauth/requests

Signed request — returns a sessionId and QR payload

Requires HMAC request signing, and the application must have OAUTH enabled in its request types.

Request Body

ParameterTypeRequiredDescription
recipientstringYesThe enrolled user's phone number
claimsstring[]YesVerified fields to request (see below)
scopesstring[]NoOAuth scopes, e.g. openid, profile, identity
redirectUristringYesOAuth redirect URI registered on your app
codeChallengestringYesPKCE challenge (S256)
codeChallengeMethodstringNoS256 (default) or plain
callbackUrlstringNoWebhook URL (defaults to the app callback)
Create request (Node.js)
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
Response
{
  "error": false,
  "message": "OAuth request created",
  "data": {
    "sessionId": "b0f2…",
    "qr": "<base64 png>",
    "type": "OAUTH",
    "redirectUri": "https://yourapp.com/mid/callback",
    "status": "pending"
  }
}

Available claims

ClaimDescription
name / firstName / lastNameFull or split legal name
dobDate of birth (derive age client-side)
addressResidential address
phoneVerified phone number
emailEmail address
ninNigeria NIN — tokenised, pre-verified via NINAuth
liveSelfieBiometric face-match result (liveness)
photoVerified 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.

POST
/v1/oauth/requests/status

Signed request — returns PENDING or approved claims

Status request
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.
}
Approved response
{
  "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

Until the mobile approval completes, the endpoint returns the same session metadata with 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.

Webhook payload (data)
{
  "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.

POST
/v1/oauth/token

Signed request — exchange code + verifier for claims

Token exchange
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

You now hold government-verified claims for a live, present user — store only what you need. MID keeps the rest.