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
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.
- 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.
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 |