Skip to content

Age Verification ​

E-dates are for verified adults only. "Verified" means two things, both checked on the server:

  1. The self-declared birthdate on the account (User.birthdate, locked once set) says 18 or older.
  2. An admin has checked a photo of the user's driver's license against that birthdate, once.

PlayPals hosting e-dates are verified by their creator approval (an admin reviewed their government ID, and approval requires 18+); their birthdate is re-checked too. 1-on-1 sessions don't need any of this.

User endpoints ​

All require authentication. Mounted at /api/v1/age-verification.

MethodPathPurpose
GET/age-verificationStatus, and whether the user can book e-dates now
POST/age-verification/documentSend the license photo (multipart/form-data, field document, JPEG or PNG, ≤ 10 MB)
json
{
  "status": "NONE" | "PENDING" | "APPROVED" | "REJECTED",
  "submittedAt": "…" | null,
  "reviewedAt": "…" | null,
  "rejectionReason": "…" | null,
  "edates": { "eligible": false, "blockers": ["AGE_VERIFICATION_PENDING"] }
}

Sending while PENDING replaces the photo. Errors: 403 BIRTHDATE_REQUIRED, 403 UNDERAGE (no photo is collected from someone under 18 by their own birthdate), 409 AGE_ALREADY_VERIFIED, 503 AGE_VERIFICATION_UNAVAILABLE (KYC storage or encryption not configured).

The photo is handled like a creator's ID: memory-only upload, re-encoded (drops EXIF/GPS), AES-GCM encrypted, stored in the private KYC bucket as a VerificationAsset of type age_document. It is deleted as soon as an admin approves or rejects; AgeVerification keeps the decision, reviewer and time.

Enforcement ​

assertEdateEligible({ bookerId, playpalId }) (apps/api/src/services/ageVerification.ts) throws 403 with the first blocker as the code:

CodeWhoMeaning
BIRTHDATE_REQUIREDbookerNo birthdate on the account
UNDERAGEbookerUnder 18 today (re-checked every time, even with an approved check on file)
AGE_VERIFICATION_REQUIREDbookerNever sent a license, or it was rejected
AGE_VERIFICATION_PENDINGbookerSent, not reviewed yet
PLAYPAL_NOT_VERIFIEDhostNot an approved PlayPal, or not an adult (one code, so nothing about the host's age leaks)

It is called when an e-date is held, when it's paid for, and when either person asks to join its room.

Admin review ​

Admin-only, mounted at /api/v1/admin/age-verifications; the admin panel's Age verifications page uses it.

MethodPathPurpose
GET/Queue (status = PENDING default, APPROVED, REJECTED; cursor paging, oldest first)
GET/:userIdOne verification, with accountBirthdate and accountAge to compare against
GET/:userId/documents/:assetIdThe decrypted photo (no-store); every view is logged
POST/:userId/approve{ "birthdateMatches": true } required; the account's birthdate must be 18+
POST/:userId/reject{ "reason": "…" }, shown to the user

Every view and decision writes a VerificationAccessLog row; decisions also write AdminAuditLog (age.approved, age.rejected) and notify the user. Purpose limitation: this module only touches age_document assets, never creator IDs or selfies.

Deleting an account removes its age verification (and any pending photo).

Internal documentation — PlayPalz platform