User management
Create, query, update and delete IM users from your server. Users belong to an app, and users of different apps are completely separate.
| You want to | API |
|---|---|
| Create the IM user when someone registers in your system | Create a user; or skip this and create them automatically when issuing a sign-in ticket |
| Import users from another system | Create users in batches, up to 100 at a time |
| List or search users in your admin tools | List users (prefix search supported) |
| Show nicknames and avatars for a set of users | Look up users by username |
| Sync nicknames, avatars and custom profile data | Update a user's profile; for avatars see Avatars |
| A user closes their account | Delete a user |
For example, create the user zhangsan:
curl -X POST "$IM_API/$ORG/$APP/users" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "username": "zhangsan", "nickname": "张三" }'For passwords, bans and unbans see Passwords and bans; for sign-in and devices see Sign-in and devices.
Usernames and profiles
| Item | Rules |
|---|---|
Username username | 1 to 64 characters of letters, digits, _, - and ., and not . or ... Case-insensitive: uppercase letters become lowercase, so Tom and tom are the same user, and responses always use lowercase. Can't be changed after creation |
Password password | Optional, 1 to 64 characters, no complexity rules, stored as is (leading and trailing spaces kept). Users without a password can only sign in with tickets |
Nickname nickname | Up to 64 characters, no control characters, leading and trailing whitespace trimmed; may be empty |
Avatar avatar_url | An http or https address, up to 512 characters, no whitespace; may be empty. Can be an avatar address from this service or your own, see Avatars. The server never fetches it |
Custom attributes attributes | String key-value pairs for your own profile data such as gender or a bio. Keys are 1 to 32 characters of letters, digits, _, - and ., case-sensitive; values are any text; up to 32 entries and 4 KB (4096 bytes) of JSON in total |
Every API identifies users by username, and usernames in paths are case-insensitive too.
Choosing usernames
Use the user ID from your own system as the username. Don't use phone numbers, email addresses or other personal data: usernames are shown to other users of the app, and clients can use them to tell whether a username is registered.
Profiles are visible to everyone
Nicknames, avatars and custom attributes are visible to every user of the app (clients can look up any user's profile for display), so don't store phone numbers, ID numbers or other sensitive data in them.
Moderation: nicknames and avatars go through moderation when creating users and updating profiles (content from your server is checked against platform rules only by default, see Server-submitted content). Updates only check fields that changed; clearing a field and custom attributes aren't checked.
| Case | Result |
|---|---|
| Fails | Nothing is written; you get 403 content_rejected with details.field nickname or avatar_url, see Errors callers see |
| Nickname matches a replacement rule | The replaced text is saved (matches become *); rely on the response |
| The app enables avatar review | The avatar must be a publicly reachable http or https address; internal addresses and unresolvable domains are rejected too |
| Later found to violate rules | The nickname or avatar may be cleared, with clients notified as for a profile update, see Actions after delivery |
The app's non-deleted users (banned users included) can't exceed the registered-user quota of its plan, see Plans and billing.
Avatars
Avatars can be stored by this service or use your own image addresses.
Using this service's avatars (also the order to follow when importing users with avatars):
- Create the user.
- Upload the image with purpose
user_avatarand the user asowner(they must already exist). The image is scaled to at most 640 pixels on its long edge, and you get a public address anyone can open directly (theurlof the file object), such ashttps://cdn.example.com/t99604914749046784/a99604957400924160/u/99606512787259392_i13hkkg3.jpg. - Update the user's profile with that address in
avatar_url.
This service keeps avatar files based on whether they're currently the owner's avatar:
| Case | File |
|---|---|
| Not set as the avatar within 24 hours of upload | Deleted; the address stops working |
| Replaced (changed to another address or cleared) | Deleted after 7 days; the address stops working |
Put in another user's profile (not the owner's) | Doesn't count as in use; it also stops working 7 days after the owner changes avatars |
Using your own addresses: avatar_url written through server APIs only has its format checked and can point to any host; such avatars don't go through this service. But when users change their own avatar from a client and the app enables media_url_only in its runtime policy, only this app's avatar addresses or https addresses on hosts in media_allowed_hosts are allowed; otherwise you get 400 invalid_argument (url_not_allowed).
Create a user
Create a user. If the username is taken you get 409 already_exists; when retrying after a timeout, already_exists can be treated as the earlier attempt having succeeded.
/{org_name}/{app_name}/usersPath parameters
For org_name and app_name, see the server overview.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
username | String | Yes | Username, see Usernames and profiles |
password | String | No | Password, 1 to 64 characters. Omitted or empty means no password |
nickname | String | No | Nickname, up to 64 characters |
avatar_url | String | No | Avatar address, http or https, up to 512 characters |
attributes | Object | No | Custom attributes; keys and values are strings, up to 32 entries |
Request example
curl -X POST "$IM_API/$ORG/$APP/users" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "zhangsan",
"password": "Passw0rd!",
"nickname": "张三",
"avatar_url": "https://cdn.example.com/avatar/zhangsan.png",
"attributes": { "gender": "1", "sign": "你好" }
}'Response
On success you get 201 Created with a user object.
{
"username": "zhangsan",
"nickname": "张三",
"avatar_url": "https://cdn.example.com/avatar/zhangsan.png",
"attributes": { "gender": "1", "sign": "你好" },
"status": "active",
"status_reason": null,
"disabled_by": null,
"disabled_until": null,
"has_password": true,
"created_via": "openapi",
"created_at": "2026-10-02T19:05:46.167Z",
"last_login_at": null,
"version": 1
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | A parameter breaks the rules in Usernames and profiles; message says which |
| 403 | app_unavailable | The app is read-only and can't create users, see Rate limits and app status |
| 403 | content_rejected | The nickname or avatar failed moderation; details.field is nickname or avatar_url and details.reason gives the reason |
| 409 | already_exists | The username is taken |
| 409 | limit_exceeded | The app has reached its registered-user limit |
| 429 | rate_limited | Creating with a password exceeded the app's password-hashing quota (100 per second per app, shared with setting passwords); retry after Retry-After |
Create users in batches
Create 1 to 100 users, one by one in order; one failure doesn't affect the others. Good for importing users from another system; the response format is described in Batch APIs.
- Rate limits: counts toward the app's OpenAPI calls per second once per user, so 100 users count as 100 calls; if the quota isn't enough, the whole request returns
429 rate_limited. Users with passwords also count toward the password-hashing quota (100 per second); users over it appear infailed(rate_limited) and can be retried individually later. - Moderation: nicknames are only checked against word lists; nicknames and avatars that need third-party review are reviewed after creation, and violations are cleared from the profile. Rejected users appear in
failed(content_rejected).
/{org_name}/{app_name}/users/batchRequest body
| Field | Type | Required | Description |
|---|---|---|---|
users | Array<Object> | Yes | Users to create, 1 to 100, each with the same fields as Create a user |
Request example
curl -X POST "$IM_API/$ORG/$APP/users/batch" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"users": [
{ "username": "wangwu", "nickname": "王五" },
{ "username": "zhaoliu", "password": "123456" },
{ "username": "ZhangSan" },
{ "username": "bad name" }
]
}'Response
On success you get 200 OK. Even if every user failed it's still 200, so check failed.
| Field | Type | Description |
|---|---|---|
created | Array<Object> | Users created, each a user object, in request order |
failed | Array<Object> | Users that failed, in request order |
failed[].username | String | The username from the request (lowercased) |
failed[].code | String | The error code, as for Create a user, such as already_exists, invalid_argument, content_rejected, limit_exceeded or rate_limited |
failed[].message | String | The error text |
{
"created": [
{
"username": "wangwu",
"nickname": "王五",
"avatar_url": "",
"attributes": {},
"status": "active",
"status_reason": null,
"disabled_by": null,
"disabled_until": null,
"has_password": false,
"created_via": "openapi",
"created_at": "2026-10-02T19:06:04.505Z",
"last_login_at": null,
"version": 1
},
{
"username": "zhaoliu",
"nickname": "",
"avatar_url": "",
"attributes": {},
"status": "active",
"status_reason": null,
"disabled_by": null,
"disabled_until": null,
"has_password": true,
"created_via": "openapi",
"created_at": "2026-10-02T19:06:04.563Z",
"last_login_at": null,
"version": 1
}
],
"failed": [
{ "username": "zhangsan", "code": "already_exists", "message": "用户名已被占用" },
{ "username": "bad name", "code": "invalid_argument", "message": "用户名应为 1 到 64 位,由字母、数字、_、-、. 组成,且不能是 . 或 .." }
]
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | users is empty or has more than 100 entries |
| 403 | app_unavailable | The app is read-only and can't create users |
| 429 | rate_limited | The app's OpenAPI quota can't cover this many users; nothing was done |
List users
List the app's users page by page, excluding deleted users. Ordered by creation by default, or by username with username_prefix. For paging see Paging; start from the first page again after changing filters.
/{org_name}/{app_name}/usersQuery parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
status | String | No | Filter by status: active or disabled (banned). Both when omitted |
username_prefix | String | No | Search by username prefix: 1 to 64 username characters, case-insensitive, matched literally (_ isn't a wildcard) |
limit | Number | No | Page size, 1 to 100, default 20 |
cursor | String | No | Paging cursor, the previous page's next_cursor; omit for the first page |
Request example
curl "$IM_API/$ORG/$APP/users?status=active&limit=2" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK:
| Field | Type | Description |
|---|---|---|
items | Array<Object> | Users on this page, each a user object |
next_cursor | String | The next page's cursor; null when there's no next page |
{
"items": [
{
"username": "zhangsan",
"nickname": "张三",
"avatar_url": "https://cdn.example.com/avatar/zhangsan.png",
"attributes": { "gender": "1", "sign": "你好" },
"status": "active",
"status_reason": null,
"disabled_by": null,
"disabled_until": null,
"has_password": true,
"created_via": "openapi",
"created_at": "2026-10-02T19:05:46.167Z",
"last_login_at": null,
"version": 1
},
{
"username": "lisi",
"nickname": "",
"avatar_url": "",
"attributes": {},
"status": "active",
"status_reason": null,
"disabled_by": null,
"disabled_until": null,
"has_password": false,
"created_via": "openapi",
"created_at": "2026-10-02T19:05:46.203Z",
"last_login_at": null,
"version": 1
}
],
"next_cursor": "Rq65HvagQGJhPXM4qOLjuRBLiQjV-J2USEzW9r0fve4kz850Z89l..."
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | status isn't active or disabled; username_prefix has non-username characters or more than 64 characters; limit is out of range; the cursor is invalid or from a different query |
Look up users by username
Look up 1 to 100 users. Results follow the request order; nonexistent or deleted users are left out, and duplicate or malformed usernames are ignored.
/{org_name}/{app_name}/users/lookupRequest body
| Field | Type | Required | Description |
|---|---|---|---|
usernames | Array<String> | Yes | Usernames to look up, 1 to 100, case-insensitive |
Request example
curl -X POST "$IM_API/$ORG/$APP/users/lookup" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"usernames": ["LISI", "nobody", "zhangsan", "lisi"]
}'Response
On success you get 200 OK. items holds the users found, each a user object. In the example, nobody doesn't exist and the duplicate lisi is returned once.
{
"items": [
{
"username": "lisi",
"nickname": "",
"avatar_url": "",
"attributes": {},
"status": "active",
"status_reason": null,
"disabled_by": null,
"disabled_until": null,
"has_password": false,
"created_via": "openapi",
"created_at": "2026-10-02T19:05:46.203Z",
"last_login_at": null,
"version": 1
},
{
"username": "zhangsan",
"nickname": "张三",
"avatar_url": "https://cdn.example.com/avatar/zhangsan.png",
"attributes": { "gender": "1", "sign": "你好" },
"status": "active",
"status_reason": null,
"disabled_by": null,
"disabled_until": null,
"has_password": true,
"created_via": "openapi",
"created_at": "2026-10-02T19:05:46.167Z",
"last_login_at": null,
"version": 1
}
]
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | usernames is empty or has more than 100 entries |
Get a user
Get one user's details, including ban status.
/{org_name}/{app_name}/users/{username}Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username, case-insensitive |
Request example
curl "$IM_API/$ORG/$APP/users/zhangsan" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK with a user object.
{
"username": "zhangsan",
"nickname": "张三",
"avatar_url": "https://cdn.example.com/avatar/zhangsan.png",
"attributes": { "gender": "1", "sign": "你好" },
"status": "active",
"status_reason": null,
"disabled_by": null,
"disabled_until": null,
"has_password": true,
"created_via": "openapi",
"created_at": "2026-10-02T19:05:46.167Z",
"last_login_at": null,
"version": 1
}Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | The user doesn't exist or was deleted. Malformed usernames also return not_found |
Update a user's profile
Update a user's nickname, avatar and custom attributes. Send only the fields to change, at least one. The user's online devices are notified of the change.
An empty string for nickname or avatar clears it. Custom attributes are merged entry by entry, not replaced as a whole:
In attributes | Result |
|---|---|
"key": "value" | Adds or overwrites that entry |
"key": null | Deletes that entry |
| Keys not included | Unchanged |
attributes itself null | Clears everything |
The merged result still has to fit in 32 entries and 4 KB. Two requests changing different entries at the same time don't overwrite each other.
- Concurrent changes:
versionis optional. If you send it, it must match the user's currentversion, otherwise you get409 version_conflict, see Concurrent changes. - No change: when the submitted content equals the current profile,
versionisn't incremented and no notification is sent.
/{org_name}/{app_name}/users/{username}Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
nickname | String | No | New nickname; an empty string clears it |
avatar_url | String | No | New avatar address; an empty string clears it, see Avatars. After replacing or clearing an avatar from this service, the old file is deleted after 7 days |
attributes | Object | No | Custom attributes to change, merged as described above. null for a value deletes that entry; null for the whole field clears everything |
version | Number | No | The user version you read, used to detect concurrent changes when given |
Request example
Change the nickname to "张三丰", delete the sign attribute, add a level attribute and keep the rest:
curl -X PATCH "$IM_API/$ORG/$APP/users/zhangsan" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"nickname": "张三丰",
"attributes": { "sign": null, "level": "vip" }
}'Set the avatar to the public address from a user_avatar upload:
curl -X PATCH "$IM_API/$ORG/$APP/users/zhangsan" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"avatar_url": "https://cdn.example.com/t99604914749046784/a99604957400924160/u/99606512787259392_i13hkkg3.jpg"
}'Response
On success you get 200 OK with the updated user object.
{
"username": "zhangsan",
"nickname": "张三丰",
"avatar_url": "https://cdn.example.com/avatar/zhangsan.png",
"attributes": { "gender": "1", "level": "vip" },
"status": "active",
"status_reason": null,
"disabled_by": null,
"disabled_until": null,
"has_password": true,
"created_via": "openapi",
"created_at": "2026-10-02T19:05:46.167Z",
"last_login_at": null,
"version": 2
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | Nothing to change was sent; a field breaks the rules in Usernames and profiles; merged attributes exceed 32 entries or 4 KB |
| 403 | app_unavailable | The app is read-only and profiles can't be changed |
| 403 | content_rejected | The new nickname or avatar failed moderation; details.field is nickname or avatar_url and details.reason gives the reason |
| 404 | not_found | The user doesn't exist or was deleted |
| 409 | version_conflict | version doesn't match the user's current one; query again and retry |
Delete a user
Delete a user. Deletion can't be undone, and it has these effects:
| Area | After deletion |
|---|---|
| Sign-in | Every device is signed out immediately and online devices are told the account was deleted; issued but unused sign-in tickets become invalid |
| Profile | Password, nickname, avatar, custom attributes and global mutes are cleared; queries return 404 not_found, and the user no longer counts toward the app's users |
| Username | Released, so a user with the same name can be created again. The new user is a different person and inherits none of the old user's friends, groups or conversations |
| Friends | Friendships, friend requests and the block list are cleared; their friends are told the friend was deleted |
| Groups | Removed from every group; join requests, invitations and group block list entries about them are deleted. If they owned a group, ownership goes to the earliest-joined admin, or the earliest-joined member if there are no admins; a group with nobody else in it is dissolved |
| Messages and conversations | Messages they sent are kept by default with the sender shown as a deleted user; erased if the app enables erase_messages_on_user_delete (runtime policy). One-to-one conversations stay on the other side, who can read history but can no longer message them |
| Push | Push settings, conversation Do Not Disturb and push devices are deleted, see Push devices and settings |
| Moderation | Violation records are deleted |
Files (where they're the owner) | User avatars are deleted and their public addresses stop working within about 10 minutes (cached copies up to 1 day). With erase_messages_on_user_delete, attachments and group files are deleted too; otherwise attachments follow their retention and group files stay in the group. Group avatars aren't deleted. Files a user uploads from a client are owned by that user |
- Cleanup timing: friends, groups and files are cleaned up asynchronously, usually within about a minute; messages and conversations about 10 minutes later.
- Users banned by the platform can't be deleted, see Passwords and bans.
- Retries:
404 not_foundcan be treated as the earlier deletion having succeeded.
When users close their account
Clients have no API for deleting their own account. If your app offers account closure, have the app call your system, verify the user, and then call this API to delete the IM user.
/{org_name}/{app_name}/users/{username}Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username |
Request example
curl -X DELETE "$IM_API/$ORG/$APP/users/wangwu" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 204 No Content with no body.
Errors
| HTTP status | code | Description |
|---|---|---|
| 403 | permission_denied | The user is banned by the platform and can't be deleted |
| 404 | not_found | The user doesn't exist or was deleted |
Data structures
User object
| Field | Type | Description |
|---|---|---|
username | String | Username, lowercase |
nickname | String | Nickname; empty string if none |
avatar_url | String | Avatar address; empty string if none |
attributes | Object | Custom attributes, string keys and values; {} if none |
status | String | active or disabled (banned) |
status_reason | String | Ban reason; may be null (not banned, or no reason given) |
disabled_by | String | Who banned them: tenant your server or console, platform the platform. null when not banned |
disabled_until | String | When the ban ends and the user is unbanned automatically. null when not banned or banned permanently |
has_password | Boolean | Whether a password is set |
created_via | String | How they were created: openapi via OpenAPI (including automatic creation when issuing tickets), console, or client self-registration |
created_at | String | Creation time |
last_login_at | String | Last sign-in time; may be null (never signed in) |
version | Number | Version, incremented when the profile, password or ban status changes; usable to detect concurrent changes when updating the profile |
For time formats see Data formats.
{
"username": "lisi",
"nickname": "李四",
"avatar_url": "",
"attributes": { "gender": "2" },
"status": "disabled",
"status_reason": "发布违规内容",
"disabled_by": "tenant",
"disabled_until": "2026-10-03T19:08:44.477Z",
"has_password": true,
"created_via": "openapi",
"created_at": "2026-10-02T19:05:46.203Z",
"last_login_at": "2026-10-02T19:08:44.402Z",
"version": 5
}