Users Service
Manage users within a company, including their experience points, assets, notes, and quickrolls. This service provides comprehensive user management for World of Darkness campaigns.
Onboarding from a provider login
To onboard a user from a provider login (Apple, Google, Discord, GitHub), use IdentityService.identify(). For users without a supported provider, create the account directly with create() below.
Usage
from vclient import users_service
users = users_service(on_behalf_of="USER_ID", company_id="COMPANY_ID")
Methods
CRUD Operations
| Method | Returns | Description |
|---|---|---|
get(user_id, *, include=None) |
UserDetail |
Retrieve a user by ID, optionally embedding quickrolls, notes, assets, characters |
create(request=None, **kwargs) |
User |
Create a new user |
update(user_id, request=None, **kwargs) |
User |
Update user properties |
delete(user_id) |
None |
Delete a user |
Embedding Child Resources
The get() method accepts an optional include parameter that embeds child resources directly in the response, eliminating the need for follow-up requests.
Valid values (UserInclude type alias): "quickrolls", "notes", "assets", "characters"
Semantic nuances
"assets"returns assets attached to the user, not assets the user uploaded."characters"returns only the characters the user plays (not characters they created for others).
When a value is not included in the request, the corresponding field on UserDetail is None. When requested, the field contains a list of fully populated objects — the same DTOs returned by the dedicated child endpoints.
from vclient import users_service
users = users_service(on_behalf_of="...", company_id="...")
# Embed quickrolls and characters in a single request
user = await users.get("user_id", include=["quickrolls", "characters"])
assert user.quickrolls is not None # list[Quickroll]
assert user.characters is not None # list[Character] — only characters the user plays
assert user.notes is None # not requested
assert user.assets is None # not requested
Pagination Methods
| Method | Returns | Description |
|---|---|---|
get_page(user_role=None, limit=10, offset=0) |
PaginatedResponse[User] |
Retrieve a paginated page |
list_all(user_role=None) |
list[User] |
Retrieve all users |
iter_all(user_role=None, limit=100) |
AsyncIterator[User] |
Iterate through all users |
User Approval
| Method | Returns | Description |
|---|---|---|
get_unapproved_page(limit=10, offset=0) |
PaginatedResponse[User] |
List unapproved users (paginated) |
list_all_unapproved() |
list[User] |
List all unapproved users |
iter_all_unapproved(limit=100) |
AsyncIterator[User] |
Iterate through unapproved users |
approve_user(user_id, role) |
User |
Approve a user and assign a role |
deny_user(user_id) |
None |
Deny an unapproved user |
merge(primary_user_id, secondary_user_id) |
User |
Merge unapproved user into primary |
link_identity(user_id, provider=..., token=...) |
User |
Attach a verified provider identity |
unlink_identity(user_id, provider=...) |
User |
Disconnect a provider identity |
Statistics
| Method | Returns | Description |
|---|---|---|
get_statistics(user_id, num_top_traits=5) |
RollStatistics |
Retrieve aggregated roll stats |
Experience Management
| Method | Returns | Description |
|---|---|---|
get_experience(user_id, campaign_id) |
CampaignExperience |
Retrieve XP and cool points |
add_xp(user_id, campaign_id, amount) |
CampaignExperience |
Award experience points |
remove_xp(user_id, campaign_id, amount) |
CampaignExperience |
Deduct experience points |
add_cool_points(user_id, campaign_id, amount) |
CampaignExperience |
Award cool points |
Asset Management
| Method | Returns | Description |
|---|---|---|
get_assets_page(user_id, limit=10, offset=0) |
PaginatedResponse[Asset] |
Get a page of assets |
list_all_assets(user_id) |
list[Asset] |
Get all assets |
iter_all_assets(user_id, limit=100) |
AsyncIterator[Asset] |
Iterate through assets |
get_asset(user_id, asset_id) |
Asset |
Retrieve an asset by ID |
upload_asset(user_id, filename, content) |
Asset |
Upload a new image |
delete_asset(user_id, asset_id) |
None |
Delete an asset |
Image uploads only
upload_asset accepts only image files: PNG, JPEG, GIF, and WEBP. Any other upload (documents, audio, video, archives, SVG, or a non-image payload mislabeled as an image) is rejected with 400 Bad Request. The stored mime_type is detected from the file's bytes; the declared content type is ignored. Newly uploaded assets always have asset_type == "image".
Avatar Management
| Method | Returns | Description |
|---|---|---|
upload_avatar(user_id, filename, content, content_type=None) |
User |
Upload a custom avatar, replacing any existing one |
delete_avatar(user_id) |
User |
Remove the custom avatar |
Both methods require the On-Behalf-Of header (permitted for the user themselves or an admin) and return the updated User (including the resolved avatar_url). delete_avatar responds 200 OK with the updated user body, not 204.
upload_avatar accepts PNG, JPEG, WEBP, or GIF (first frame) up to 5 MB; the server normalizes the image to a 512×512 WebP and it overrides any identity-provider-derived avatar. delete_avatar falls the avatar back to the identity-provider avatar, or null if none.
user = await client.users("user-123", company_id="company-456").upload_avatar(
"user-123", "pic.png", image_bytes,
)
print(user.avatar_url)
user = await client.users("user-123", company_id="company-456").delete_avatar("user-123")
Notes Management
| Method | Returns | Description |
|---|---|---|
get_notes_page(user_id, limit=10, offset=0) |
PaginatedResponse[Note] |
Retrieve a paginated page |
list_all_notes(user_id) |
list[Note] |
Retrieve all notes |
iter_all_notes(user_id, limit=100) |
AsyncIterator[Note] |
Iterate through all notes |
get_note(user_id, note_id) |
Note |
Retrieve a note by ID |
create_note(user_id, request=None, **kwargs) |
Note |
Create a new note |
update_note(user_id, note_id, request=None, **kwargs) |
Note |
Update note content |
delete_note(user_id, note_id) |
None |
Delete a note |
Quickrolls Management
| Method | Returns | Description |
|---|---|---|
get_quickrolls_page(user_id, limit=10, offset=0) |
PaginatedResponse[Quickroll] |
Retrieve a paginated page |
list_all_quickrolls(user_id) |
list[Quickroll] |
Retrieve all quickrolls |
iter_all_quickrolls(user_id, limit=100) |
AsyncIterator[Quickroll] |
Iterate through all quickrolls |
get_quickroll(user_id, quickroll_id) |
Quickroll |
Retrieve a quickroll by ID |
create_quickroll(user_id, request=None, **kwargs) |
Quickroll |
Create a new quickroll |
update_quickroll(user_id, quickroll_id, request=None, **kwargs) |
Quickroll |
Update quickroll configuration |
delete_quickroll(user_id, quickroll_id) |
None |
Delete a quickroll |
User Roles
| Role | Description |
|---|---|
ADMIN |
Full administrative access to company |
STORYTELLER |
Game master role with elevated access |
PLAYER |
Standard player with limited access |
UNAPPROVED |
Registered but not yet approved |
Examples
Create a User
Add a new user to the company.
from vclient.models import UserCreate
# Option 1: Use a model object (preferred)
request = UserCreate(
name_first="John",
name_last="Doe",
username="john_doe",
email="john@example.com",
role="PLAYER",
)
user = await users.create(request=request)
# Option 2: Pass fields as keyword arguments
user = await users.create(
name_first="John",
name_last="Doe",
username="john_doe",
email="john@example.com",
role="PLAYER",
)
Manage Experience Points
Award or deduct experience points for character progression.
# Get current experience
experience = await users.get_experience(user.id, campaign_id)
print(f"Current XP: {experience.xp_current}")
print(f"Total earned: {experience.xp_total}")
# Award XP
updated = await users.add_xp(user.id, campaign_id, amount=50)
print(f"New XP: {updated.xp_current}")
# Award cool points (converted to XP automatically)
updated = await users.add_cool_points(user.id, campaign_id, amount=3)
# Deduct XP for character upgrades
updated = await users.remove_xp(user.id, campaign_id, amount=20)
Create and Manage Notes
Store session notes and character information.
from vclient.models import NoteCreate, NoteUpdate
# Create a note with markdown content
note_request = NoteCreate(
title="Session 12 Notes",
content="## Key Events\n\n- Met the Prince\n- Discovered betrayal"
)
note = await users.create_note(user.id, note_request)
# Update note content
update = NoteUpdate(content="Updated content here...")
updated = await users.update_note(user.id, note.id, update)
# List all notes
notes = await users.list_all_notes(user.id)
for note in notes:
print(f"{note.title}: {len(note.content)} characters")
Create Quickrolls
Set up pre-configured dice pools for common actions.
from vclient.models import QuickrollCreate
# Create a quickroll for a common dice pool
quickroll_request = QuickrollCreate(
name="Strength + Brawl",
description="Melee combat attack",
trait_ids=["trait_strength_id", "trait_brawl_id"]
)
quickroll = await users.create_quickroll(user.id, quickroll_request)
# List all quickrolls
quickrolls = await users.list_all_quickrolls(user.id)
for qr in quickrolls:
print(f"{qr.name}: {len(qr.trait_ids)} traits")
Upload and Manage Assets
Store character portraits, handouts, and other files.
# Upload a character portrait
with open("portrait.jpg", "rb") as f:
content = f.read()
asset = await users.upload_asset(
user.id,
filename="john_doe_portrait.jpg",
content=content,
)
print(f"Asset URL: {asset.public_url}")
# List all assets
all_assets = await users.list_all_assets(user.id)
for asset in all_assets:
print(f"{asset.original_filename}: {asset.asset_type}")
# Delete an asset
await users.delete_asset(user.id, asset.id)
Get Roll Statistics
View aggregated dice roll statistics for a user.
stats = await users.get_statistics(user.id, num_top_traits=10)
print(f"Total rolls: {stats.total_rolls}")
print(f"Success rate: {stats.success_percentage:.1f}%")
print(f"Critical rate: {stats.criticals_percentage:.1f}%")
print(f"Botch rate: {stats.botch_percentage:.1f}%")
Filter Users by Role
Retrieve users filtered by their role in the company.
from vclient.constants import UserRole
# Get all storytellers
storytellers = await users.list_all(user_role=UserRole.STORYTELLER)
# Get paginated list of players
players_page = await users.get_page(user_role=UserRole.PLAYER, limit=25)
print(f"Total players: {players_page.total}")
Approve or Deny Users
Manage the user approval workflow.
from vclient.models import UserApproveDTO
# List all pending users
unapproved = await users.list_all_unapproved()
for user in unapproved:
print(f"Pending: {user.name_first} {user.name_last} ({user.email})")
# Approve a user as a player
approved = await users.approve_user(user_id=user.id, role="PLAYER")
print(f"Approved {approved.name_first} as {approved.role}")
# Deny a user
await users.deny_user(user_id=user.id)
Merge Users
Merge an unapproved user's data into an existing primary user.
# Merge secondary user into primary user
merged_user = await users.merge(
primary_user_id="primary_user_id",
secondary_user_id="unapproved_user_id",
)
print(f"Merged into: {merged_user.username}")
Link a Provider Identity
Connect an existing user account to a verified provider credential. This is the connect-your-account flow: the user is already logged in and wants to link their Discord, Google, GitHub, or Apple account so future logins can use that provider.
The API verifies the credential with the provider before linking. Only the account owner, or a company admin acting on another user's behalf, may link an identity.
updated = await users.link_identity(
"USER_ID",
provider="discord",
token=discord_access_token,
)
If the provider identity already belongs to a different user, or the target user already has a different identity from the same provider, a ConflictError is raised with code == "IDENTITY_ALREADY_LINKED".
Linking prevents duplicate accounts before they exist. Use merge() to clean up duplicates that already exist, including accounts with Apple private-relay emails that can never be auto-linked by IdentityService.identify().
Unlink a Provider Identity
Disconnect a previously linked provider from a user account. This is the inverse of link_identity(): the user (or a company admin acting on their behalf) removes a Discord, Google, GitHub, or Apple identity so it can no longer be used to log in. The returned User has the matching profile field (for example discord_profile) cleared to None.
updated = await users.unlink_identity(
"USER_ID",
provider="discord",
)
The API refuses to remove a user's only remaining identity, since that would leave the account unable to authenticate. In that case a ConflictError is raised with code == "LAST_IDENTITY", so disable or block the unlink control for the last connected provider. If the user has no identity from the requested provider, a NotFoundError is raised with code == "IDENTITY_NOT_LINKED".
Related Documentation
- Response Models - View
User,UserMergeDTO,UserApproveDTO,UserIdentityLinkDTO,CampaignExperience,Asset,Note, andQuickrollmodel schemas - IdentityService - Verified provider login resolution (API-key-only, no
on_behalf_ofrequired)