# Mobile guide: borrower selfie and identity check

**For:** the mobile app developer
**API base:** `/api/v1` (borrower Bearer token, same as every `/profile/*` call)

## Why

Some people opened several borrower profiles with different BVNs and got several loans. The
API now checks, for every borrower, whether another profile looks like the same person
(same phone, email, payout account, payroll ID, name + date of birth, device, or the same
face in the selfie). The app's part is:

1. take a **live selfie** of the borrower,
2. send a **device ID** header,
3. show the identity status and handle the new booking errors.

## 1. Send a device ID on every request

Add this header to **every** API request (at minimum `POST /profile/verify/stage-two`,
`POST /profile/identity-check/selfie` and the booking calls):

```
X-Device-Id: <stable id for this install>
```

- Android: a UUID generated on first launch and kept in secure storage (or `ANDROID_ID`).
- iOS: `identifierForVendor`, or a UUID kept in the Keychain so it survives reinstall.
- Do not send IMEI, phone number or anything personal. Max 200 characters.

## 2. New screen: selfie

**Where:** right after the BVN / bank step (stage two) succeeds, before employment. Also
show it whenever a booking call returns `SELFIE_REQUIRED` (see section 4).

**Camera rules (important):**

- Front camera only. **Do not allow picking from the gallery.**
- One face in the frame, face filling roughly the middle third, good light, no sunglasses or cap.
- JPEG (or PNG), at least 640 px on the short side, under 6 MB. Compress to ~80% JPEG.
- Recommended (free, on-device): use Google ML Kit Face Detection (Android and iOS) to
  - only enable the shutter when exactly one face is centred and large enough;
  - ask the borrower to **blink** and **turn their head slightly** before capturing (ML Kit
    gives eye-open probability and head angles). This stops most photo-of-a-photo attempts
    before upload. The server checks again anyway; the app check just saves retakes.

### Upload

```
POST /profile/identity-check/selfie
Content-Type: multipart/form-data
X-Device-Id: <id>

selfie=<file>            (required, jpg/jpeg/png, max 6 MB)
```

Rate limits: 5 per minute; 10 per borrower per day (then `429 SELFIE_LIMIT_REACHED`).

### Responses

The body always has the same `data` shape as `GET /profile/identity-check` (below), plus a
`message` you can show as is.

| HTTP | `data.selfie.status` | What to show |
|---|---|---|
| 200 | `passed` | Success, continue. |
| 200 | `review` | "Thanks. Your selfie is being reviewed by our team." Let them continue the journey; booking waits until an admin approves. |
| 422 | `rejected` | Show `message` (no face / more than one face / face unclear) and a **Retake** button. |
| 503 | `error` | "We could not check the selfie right now." Retake later. |
| 429 | — | `SELFIE_LIMIT_REACHED`: show `message`, no retake today. |
| 422 | — | Laravel validation error (`errors.selfie`): wrong file type or too big. |

## 3. Status endpoint

```
GET /profile/identity-check
```

```json
{
  "data": {
    "selfie_required": true,
    "selfie": {
      "status": "missing | passed | review | rejected | error",
      "reason": "no_face | multiple_faces | face_unclear | face_service_unavailable | null",
      "message": "Take a selfie to confirm your identity.",
      "submitted_at": "2026-11-01T10:00:00+00:00",
      "can_retake": true
    },
    "can_book": false,
    "block": { "error_code": "SELFIE_REQUIRED", "message": "Please take a selfie to confirm your identity before applying for a loan." }
  }
}
```

- Call it when the borrower opens the loan / booking screen. If `can_book` is false, show
  `block.message` instead of the Book button.
- `selfie_required: false` means the switch is still off (rollout). Still show the selfie
  screen after stage two: the selfie is used for duplicate checks either way.
- The response **never** says which other profile matched. Don't try to show that.

## 4. New booking errors

Every booking call (`/loans/offer/details`, `/loans/mandate/activate`, `/loans/drafts`,
`/loans/drafts/{ref}/collection`, `.../collection/retry`, `.../collection/recover-link`,
`.../processing-fee`, `.../apply`, `/loans/requests`, `/loans/retail-requests`,
`/loans/retail/analyze`, `/loans/retail/setup`, `/loans/{ref}/top-up/quote`) can now return
**HTTP 422** with one of these `error_code`s:

| `error_code` | Meaning | App action |
|---|---|---|
| `SELFIE_REQUIRED` | No passed selfie yet (or the last one was rejected). | Open the selfie screen. |
| `IDENTITY_REVIEW_REQUIRED` | Selfie in review, or this profile matches another profile and an admin must check. | Show `message`, link to support chat. No retry button. |
| `IDENTITY_DUPLICATE_CONFIRMED` | An admin confirmed this is a second profile of the same person. | Show `message`, link to support. |
| `IDENTITY_CHECK_UNAVAILABLE` | The check itself could not run. | "Try again shortly." |

Body shape (same as other booking errors):

```json
{ "message": "…", "error_code": "SELFIE_REQUIRED", "errors": { "identity": ["…"] } }
```

## 5. Checklist

- [ ] `X-Device-Id` header on all requests
- [ ] Selfie screen after stage two (front camera, no gallery, ML Kit one-face + blink/turn)
- [ ] Upload + the response states above, including Retake
- [ ] `GET /profile/identity-check` on the booking screen; hide Book when `can_book` is false
- [ ] Handle the four new booking `error_code`s
- [ ] Privacy text on the selfie screen (see below), with the borrower ticking consent

Suggested consent text (owner/legal to confirm): *"We take a photo of your face to confirm it
is really you and to stop people opening several accounts. It is stored securely, compared
only with other NextPayDay accounts and, where needed, with your BVN photo through our
verification partner."*
