API: Users, wallet, support
Controller: admin-users.controller.ts. Mount admin/users.
GET /v1/admin/users
Section titled “GET /v1/admin/users”Permission: users:read.
Query:
| Param | Type | Notes |
|---|---|---|
q | string | Email / phone / userId / device:… / name |
status | enum | active | suspended | banned | deleted |
guest | enum | guest | registered |
country | ISO-2 | |
createdFrom | ISO date | |
createdTo | ISO date | |
sort | created_at / last_seen_at / balance / spend | |
order | asc / desc | |
cursor | opaque | |
limit | int, ≤200 | Default 50 |
Response:
{ "items": [ /* UserListItem[] */ ], "nextCursor": "opaque or null", "total": 1234}UserListItem shape: id, email, phoneE164, displayName, countryCode, language, isGuest, status, balanceTotal, totalSpendCents, lastSeenAt, createdAt.
Detail (User 360)
Section titled “Detail (User 360)”GET /v1/admin/users/:id
Section titled “GET /v1/admin/users/:id”Permission: users:read.
Returns rich UserDetail:
{ "id": "...", "email": "alice@twistcue.dev", "phoneE164": null, "displayName": "Alice", "avatarUrl": "https://...", "authProvider": "email", "countryCode": "PH", "language": "en", "isGuest": false, "status": "active", "suspendedUntil": null, "banReason": null, "tokenVersion": 3, "createdAt": "...", "lastSeenAt": "...", "balanceTotal": 480, "balancePaid": 320, "balanceBonus": 160, "totalSpendCents": 1497, "unlocksCount": 12, "streak": { "current": 7, "longest": 14, "lastClaimDate": "..." }, "pushPrefs": { "drops": true, "marketing": false }, "devices": [ /* UserDevice[] */ ], "subscriptions": [ /* UserSubscription[] */ ], "strikes": [ /* UserStrike[] */ ]}Ledger
Section titled “Ledger”GET /v1/admin/users/:id/ledger
Section titled “GET /v1/admin/users/:id/ledger”Permission: wallet:read.
Query: limit, cursor.
Response:
{ "items": [ { "id": "...", "eventId": "...", "amount": -30, "reason": "unlock", "refType": "episode", "refId": "0194...", "createdAt": "..." } ], "nextCursor": "opaque or null"}Purchases
Section titled “Purchases”GET /v1/admin/users/:id/purchases
Section titled “GET /v1/admin/users/:id/purchases”Permission: payments:read. Query: limit.
Returns UserPurchase[].
Watch history
Section titled “Watch history”GET /v1/admin/users/:id/watch-history
Section titled “GET /v1/admin/users/:id/watch-history”Permission: users:read. Query: limit.
Returns UserWatchHistoryEntry[].
Wallet adjust
Section titled “Wallet adjust”POST /v1/admin/users/:id/wallet/adjust
Section titled “POST /v1/admin/users/:id/wallet/adjust”Permission: wallet:adjust.
Request:
{ "amount": 300, "direction": "grant", "reason": "apology_bad_video", "note": "Ticket #4432"}amountint, 1–1,000,000directiongrant|deductreason3–200 charsnote≤500 chars, optional
Response 200 (applied):
{ "kind": "applied", "eventId": "...", "balanceTotal": 480}Response 200 (approval_pending):
{ "kind": "approval_pending", "approvalId": "...", "reason": "amount_exceeds_threshold"}Response 400 (support cap):
{ "statusCode": 400, "error": "support_daily_cap_exceeded", "capRemaining": 200}Suspend / ban / reactivate
Section titled “Suspend / ban / reactivate”POST /v1/admin/users/:id/suspend
Section titled “POST /v1/admin/users/:id/suspend”Permission: users:ban.
{ "reason": "Suspicious activity: investigating", "until": "2026-07-14T00:00:00Z"}until optional (null = indefinite).
POST /v1/admin/users/:id/ban
Section titled “POST /v1/admin/users/:id/ban”Permission: users:ban.
{ "reason": "Confirmed fraud: chargeback ring device_id 9d3b..." }POST /v1/admin/users/:id/reactivate
Section titled “POST /v1/admin/users/:id/reactivate”Permission: users:ban.
{ "note": "Cleared after appeal: ticket #7002" }POST /v1/admin/users/:id/devices/logout-all
Section titled “POST /v1/admin/users/:id/devices/logout-all”Permission: users:ban.
No body. Bumps token_version, invalidates all JWTs for the user.
Anonymize
Section titled “Anonymize”POST /v1/admin/users/:id/anonymize
Section titled “POST /v1/admin/users/:id/anonymize”Permission: users:anonymize.
{ "reason": "GDPR request: ticket #7841" }Approval-gated: creates an admin_approval row; a second admin must approve.
| Method | Path | Perm |
|---|---|---|
| GET | /admin/users/:id/notes | users:read |
| POST | /admin/users/:id/notes | users:read |
| DELETE | /admin/users/:id/notes/:noteId | users:read |
Body for POST:
{ "body": "Customer confirmed identity via email." }Comp subscription
Section titled “Comp subscription”POST /v1/admin/users/:id/comp-subscription
Section titled “POST /v1/admin/users/:id/comp-subscription”Permission: users:ban (shared for P3; may be relaxed in a later phase).
{ "days": 30, "productId": "twistcue_plus_web_monthly", "reason": "Retention (ticket #9021)"}