# verification — the nurse trust pipeline > Client seam `client/src/services/verification/` · `USE_VERIFICATION_MOCK = true` (**mock is primary**) · 17 server ops > Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29). The largest domain, and the one the whole marketplace's trust claim rests on. A nurse is not bookable until this pipeline says so. ## Endpoints ### Nurse-facing | Method | Path | Verdict | | --- | --- | --- | | GET | `/api/v1/nurse_verification` | wired — **the one cached status query** | | POST | `/api/v1/nurse_verification/submit` | wired | | POST | `/api/v1/nurse_verification/steps/{stepId}/upload_url` | wired — presigned PUT | | POST | `/api/v1/nurse_verification/steps/{stepId}/documents` | wired — confirm the upload | | POST | `/api/v1/nurse_verification/steps/identity_kyc/run` | wired | | POST | `/api/v1/nurse_verification/steps/shahkar_match/run` | wired | | POST | `/api/v1/nurse_verification/steps/bank_account_verification/run` | wired | | POST | `/api/v1/nurse_verification/credential_details` | **unwired** — the write half of REQ-011; the client has no read-back (REQ-056) | ### Public | Method | Path | Auth | Verdict | | --- | --- | --- | --- | | GET | `/api/v1/nurses/{nurseId}/trust_badge` | **anonymous** | wired | ### Admin — all `DynamicPermission` + `sensitive` | Method | Path | Verdict | | --- | --- | --- | | GET | `/api/v1/admin_verifications` | wired · paginated | | GET | `/api/v1/admin_verifications/{nurseVerificationId}` | wired | | POST | `/api/v1/admin_verifications/steps/{stepId}/decide` | wired — **per-step** decision | | POST | `/api/v1/admin_verifications/{nurseVerificationId}/suspend` | **unwired** | | POST | `/api/v1/admin_verifications/scan_expiring` | **unwired** — an ops one-shot; the scheduler runs it | | GET | `/api/v1/admin_verification_step_types` | **unwired** — the step catalogue is data-driven; no editor | | POST | `/api/v1/admin_verification_step_types` | **unwired** | | DELETE | `/api/v1/admin_verification_step_types/{id}` | **unwired** | ### Phantom — 3 | Client call | REQ | Note | | --- | --- | --- | | `GET /api/v1/admin_verifications/documents/{id}/url` | REQ-034 | Deferred. No on-demand signed document URL, so the queue cannot open an uploaded document | | `POST /api/v1/admin_verifications/{id}/approve` | REQ-034 | Deferred. Today approval is **per-step** only — the aggregate flips when the last required step passes | | `POST /api/v1/admin_verifications/{id}/reject` | REQ-034 | Deferred | ## The two rules that must not be broken 1. **`status` is the source of truth; `nurse_profiles.is_verified` is derived.** The boolean is written **only** by the finalize transaction when the aggregate reaches `approved`, as one guarded cross-aggregate flip: load both tracked, mutate through one pure domain helper, commit once. Never set `is_verified` from a profile write, a controller, or out of band. See [profiles.md](profiles.md). 2. **The step catalogue is data, not code.** `verification_step_types` rows define which steps exist, which are required, and which are automated. Adding a step is a data change. The client must not hardcode the step list — it renders whatever `VerificationStatusDto.steps` contains. ## Shape rules the JSON does not express - **`VerificationStatusDto` is `{ status, isBookable, blockingSteps, steps }`** — the server computes `isBookable` and names the `blockingSteps`. The client does not derive bookability from the step array. This is why the client keeps **one cached status query** and every screen reads it. - **`expiresAt` on a step is real.** MoH competency licences and INO membership lapse; `scan_expiring` reverts a lapsed step to `expired`, which re-gates bookability. `expired` is therefore a normal state, not an error. - **Document upload is a two-step presign flow**: `upload_url` returns a presigned PUT, the client uploads **directly to storage**, then `documents` confirms. The document bytes never transit the API. `Seams:ObjectStorage:PresignExpirySeconds` (900) bounds the window. - **The three `run` steps are seams, each independently switchable** — `Seams:IdentityKyc:Provider`, `Seams:Shahkar:Provider`, `Seams:BankOwnership:Provider`, all `mock` by default, all `finnotech` for the real bridge (sharing `Seams:Finnotech` credentials). Designated test values make each failure path reachable: `SharedSimPhone` `09120000000`, `MismatchNationalId` `1111111111`, `FailNationalId` `0000000000`, `MismatchIban` `IR0000…0000`. - **`shahkar_match` failing as *shared SIM* is a distinct outcome** from a plain phone↔national-id mismatch, and the UI must say which — a shared family SIM is a common, innocent case. - **National id and licence numbers are encrypted at rest**; the admin queue sees them only where the decision requires it. - The nurse's INO membership field is **locked once submitted** (it feeds the public trust badge). ## Two vocabularies for one thing `GET /me` reports a *nurse verification summary* using `not_started` `in_progress` `pending_review` `verified` `rejected`. This domain's aggregate uses `not_started` `pending` `in_review` `approved` `rejected` `suspended`. **They are two read models over the same source of truth, not a drift** — but they are not interchangeable strings. Map deliberately. See [auth.md](auth.md). ## Enums | Vocabulary | Values | | --- | --- | | `VerificationStatus` (aggregate) | `not_started` `pending` `in_review` `approved` `rejected` `suspended` | | `VerificationStepStatus` | `not_started` `pending` `in_review` `passed` `failed` `expired` | | `StepTypeCode` | `identity_kyc` `shahkar_match` `moh_competency_license` `ino_membership` `criminal_record` `bank_account_verification` | | `CredentialType` | `moh_competency_license` `ino_membership` `criminal_record` | | `VerificationMethod` | `manual` `portal` `api` | | `BadgeState` *(client, from `TrustBadgeDto`)* | `verified` `unverified` `expired` | The first two are C# enums serialised as snake_case codes (`Entities/Verification/VerificationStatus.cs`, `VerificationStepStatus.cs`); the rest are string constants in `VerificationStepTypeCodes.cs`. All verified identical to the client's unions. `TrustBadgeDto` is `{ nurseId, isVerified, approvedAt, credentialTypes }` — a summary, with no per-step detail. That absence is REQ-043. ## Open REQs | REQ | Status | Effect | | --- | --- | --- | | REQ-034 | deferred | No nurse-grouped queue, no on-demand document URL, no whole-verification approve/reject → 3 phantom routes. This is the main reason the seam is mocked | | REQ-043 | open | `TrustBadgeDto` has no per-step detail (step codes + decision dates), so the public verification panel shows a summary only | | REQ-055 | open | No `submittedAt` on `VerificationStatusDto` — the real B6 screen omits the timestamp line | | REQ-056 | open | No nurse-facing read-back of submitted credential details. `credential_details` writes; nothing reads. The real form degrades to blank | | REQ-062 | open | No name/phone search and no per-status counts on the admin queue |