OAuth2 · User Info
User Info
Understand the UserInfo endpoint response structure and scope-based data filtering.
Response shape
UserInfo returns the signed-in user’s profile as JSON. Optional fields appear only when the matching scope was granted — check for a key before reading it.
Endpoint: GET /api/v2/oauth/userinfo with Authorization: Bearer <access_token>.
Base response (always)
These fields return for any valid access token (even with only openid). They are not gated on the profile scope.
{
"id": "1234567890123456789",
"user": {
"displayName": "Duru",
"username": "duru",
"avatar": {
"baseUrl": "//media.cengel.net",
"path": "/avatars/1234567890123456789/latest",
"url": "//media.cengel.net/avatars/1234567890123456789/latest",
"color": "#000"
},
"about": "User bio or description"
},
"language": "en",
"createdAt": "2024-01-01T00:00:00Z"
}
user.avatar.url when displaying an image. baseUrl + path are for composing your own CDN URLs. With profile scope
Adds theme preference at the top level:
{
"theme": "system"
}
With email scope
{
"user": {
"email": {
"username": "john",
"domain": "example.com",
"isVerified": true
}
}
}
With details scope
{
"user": {
"website": "https://example.com",
"location": "Istanbul, Turkey",
"birthDate": "1990-01-01"
}
}
birthDate is null when the user keeps birth privacy non-public.
With phone / social
{
"user": {
"phone": "+905551234567",
"socialLinks": [
{ "platform": "x", "url": "https://x.com/cengelstudio", "label": "X" },
{ "platform": "github", "url": "https://github.com/cengelstudio", "label": "GitHub" }
]
}
}
With space.read scope
Returned only for managed (Space) accounts. Otherwise the space key is omitted. If the user has no membership, value is null.
{
"user": {
"space": {
"id": "9876543210987654321",
"displayName": "Acme Corp",
"isOwner": true,
"domain": "acme.com"
}
}
}
Field reference
Quick lookup for types and when each field appears.
Always Base fields
| Field | Type | Notes |
|---|---|---|
| id | string | Snowflake user id |
| user.displayName | string | Display name |
| user.username | string | Unique username |
| user.avatar.url | string | Ready-to-use CDN URL |
| user.avatar.baseUrl | string | e.g. //media.cengel.net |
| user.avatar.path | string | Path under the media host |
| user.avatar.color | string | UI theme preference when set |
| user.about | string | May be empty string |
| language | string | May be null |
| createdAt | string | ISO 8601, or empty string |
profile Theme
| Field | Type | Notes |
|---|---|---|
| theme | string | light, dark, system, or null |
email Email
| Field | Type | Notes |
|---|---|---|
| user.email.username | string | Local part before @ |
| user.email.domain | string | Domain after @ |
| user.email.isVerified | boolean | Verification flag |
details / phone / social
| Field | Type | Notes |
|---|---|---|
| user.website | string | Nullable |
| user.location | string | Nullable |
| user.birthDate | string | YYYY-MM-DD or null (privacy) |
| user.phone | string | E.164 or null |
| user.socialLinks | array | Objects with platform, url, label; may be [] |
space.read Space
| Field | Type | Notes |
|---|---|---|
| user.space | object | null if no membership; omitted if not a managed account |
| user.space.id | string | Space snowflake id |
| user.space.displayName | string | Space display name |
| user.space.isOwner | boolean | true when this user owns the space |
| user.space.domain | string | Primary domain (e.g. acme.com) |
Scope-based filtering
UserInfo returns only data for scopes the user granted. Missing scope → missing fields (not empty placeholders), except base fields which always appear.
email or space exist. Authentication
Send a valid access token on every UserInfo call.
GET /api/v2/oauth/userinfo
Authorization: Bearer YOUR_ACCESS_TOKEN
- Missing/invalid token →
401 Unauthorized - User revoked access →
403(“Access revoked by user”) - Banned/suspended account →
403 Forbidden - Responses are always JSON