Skip to main content

Identity Verification Review

Admins review the age/identity (KYC-style) verification queue — government ID + selfie submissions required before a creator can request a cashout. This is a separate system from the notability/badge verification queue at /verification (see User Verification): different GraphQL operations, different permission (MODERATE_CONTENT, not VERIFY_USERS), and this page has a document/selfie review modal the badge queue doesn't.

This is the admin-side counterpart to Age & Identity Verification's "Admin review queue" checkbox (already marked done on that page) — it was previously undocumented on the admin side specifically. New doc.

Implementation checklist

  • List pending identity-verification submissions (adminPendingIdentityVerifications)
  • View submitted document + selfie photos in a review modal
  • Approve a submission (adminApproveIdentityVerification)
  • Reject a submission with a reason (adminRejectIdentityVerification)

Where this lives

Backend

  • apps/backend/graphql/types/identity-verification.type.js — schema (its header comment frames this as the "identity (KYC-style) half" of Age & Identity Verification, distinct from the badge system; approval gates real-money cashouts)
  • apps/backend/graphql/resolvers/identity-verification.resolver.jsadminPendingIdentityVerificationsidentityVerificationManager.getPendingRequests; adminApproveIdentityVerification/adminRejectIdentityVerificationidentityVerificationManager.approve/.reject. Note: these were renamed this cycle to be admin-prefixed, so the schema split routes them onto the admin schema (/admin/graphql) — even though the resolver file itself still lives outside the admin/ folder. Each also enforces context.admin + the MODERATE_CONTENT permission inside the resolver.
  • apps/backend/managers/user-managers/identity-verification.manager.js — approve/reject business logic (a separate file from verification-badges.manager.js)

Frontendapps/frontend-admin/src/app/moderation/identity-verifications/page.tsx. The list table shows username, email and identityVerificationRequestedAt; a review modal opened per-row renders identityDocumentUrl/identitySelfieUrl as clickable images plus a reject-reason textarea. Gated on MODERATE_CONTENT, matching the server-side check.

GraphQL reference

query PendingIdentityVerifications($limit: Int, $offset: Int) {
adminPendingIdentityVerifications(limit: $limit, offset: $offset) {
id username email firstName lastName profilePicture
identityVerificationStatus identityDocumentUrl
identitySelfieUrl identityVerificationRequestedAt
}
}

mutation ApproveIdentityVerification($userId: ID!) {
adminApproveIdentityVerification(userId: $userId) { identityVerificationStatus }
}

mutation RejectIdentityVerification($userId: ID!, $reason: String!) {
adminRejectIdentityVerification(userId: $userId, reason: $reason) { identityVerificationStatus }
}

Note: adminPendingIdentityVerifications returns a plain list ([IdentityVerificationRequest!]!), not a paginated { total, requests } wrapper — the frontend pages via limit/offset and disables "next" once a page comes back shorter than the page size. id on each item is the user's id (used directly as userId in the mutations); there is no separate user { id username } sub-object. Both mutations return an IdentityVerificationStatusResult (identityVerificationStatus, identityVerificationRequestedAt, identityVerifiedAt, identityRejectionReason), not a { success, message } payload, and reason on adminRejectIdentityVerification is required.