Sign-in and devices
Users must sign in to IM before sending and receiving messages. The recommended way is a sign-in ticket: after a user signs in to your system, your server issues them a ticket, and the app uses it to sign in to IM. Users don't need a second password, and your server doesn't store IM passwords.
- The user signs in to your app and your system.
- Your server calls Issue a sign-in ticket and returns the
ticketto the app. Withauto_create, a missing user is created automatically, so you can skip creating accounts separately. - Within 5 minutes, the app calls Client sign-in with
grant_type: "ticket"(the client SDKs wrap this) and gets a User Token and refresh token.
Your server only needs this one call:
curl -X POST "$IM_API/$ORG/$APP/users/zhangsan/login-tickets" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "auto_create": true }'
# {"ticket":"ult_...","expires_in":300,"user_created":true}Password sign-in also works: the app submits the username and password directly, and the user needs a password first (set when creating the user or with Set a password).
Each sign-in creates a session for the current device (one "signed-in device"). The User Token is used for client APIs and the long-lived connection, and is refreshed with the refresh token before it expires. This page also covers:
| Topic | See |
|---|---|
| How many devices a user can be signed in on, and what happens beyond that | Multiple devices |
| Seeing a user's devices and signing devices out | List devices, Sign out all devices, Sign out one device |
Why a client received session_revoked | Sign-out reasons |
Issue a sign-in ticket
Issue a one-time sign-in ticket for a user; the app uses it to sign in to IM.
- One-time: valid for 5 minutes and usable once. It's spent as soon as sign-in is submitted, even if sign-in fails (for example because the device limit is reached), so issue a new one then.
- Safe to retry: each call issues a new ticket and earlier ones stay valid for their 5 minutes. When retrying after a timeout, give the app only the last one.
- When tickets are invalidated: if, after issuing, the user has their password set, is banned, has all devices signed out or is deleted, or the console requires all users to sign in again.
- Automatic creation: with
auto_create, a missing user is created without a password (created_viaisopenapi).
A ticket is effectively a temporary password: return it only to that user's app over HTTPS and never log it.
/{org_name}/{app_name}/users/{username}/login-ticketsPath parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username |
Request body
The body is optional.
| Field | Type | Required | Description |
|---|---|---|---|
auto_create | Boolean | No | Create the user if they don't exist; default false |
Request example
curl -X POST "$IM_API/$ORG/$APP/users/zhangsan/login-tickets" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"auto_create": true
}'Response
On success you get 201 Created:
| Field | Type | Description |
|---|---|---|
ticket | String | The sign-in ticket, starting with ult_ |
expires_in | Number | Validity in seconds, always 300 |
user_created | Boolean | Whether this call created the user |
{
"ticket": "ult_RjcQrK8ff-Y0lfQ_7DqodLbw4WJZQNgdwIgOfsaW3gI",
"expires_in": 300,
"user_created": true
}Errors
| HTTP status | code | Description |
|---|---|---|
| 403 | user_disabled | The user is banned |
| 403 | app_unavailable | The user needs to be created but the app is read-only. Issuing for existing users isn't affected |
| 404 | not_found | The user doesn't exist or was deleted, and auto_create wasn't given |
| 409 | limit_exceeded | The user needs to be created but the app has reached its registered-user limit |
Client sign-in
The app signs in to IM with a ticket or password. This is a client API the app calls directly:
- The path starts with
/client/v1with noorg_nameorapp_name;app_keyin the body identifies the app. - No
Authorizationheader is needed. Never use the App Token in an app. - Cross-origin browser calls are allowed, so web pages can call it directly.
The AppKey is the app's public identifier in the form {org_name}#{app_name}, such as 1575529652#demo, shown in the app details in the console. It isn't a secret and can be built into apps; it only allows signing in and registering, with no admin rights.
/client/v1/sessionsRequest body
| Field | Type | Required | Description |
|---|---|---|---|
app_key | String | Yes | The app's AppKey |
grant_type | String | Yes | ticket for ticket sign-in, password for password sign-in |
ticket | String | For ticket sign-in | The ticket issued by your server |
username | String | For password sign-in | Username, case-insensitive. Optional for ticket sign-in; if given it must be the ticket's user, otherwise the ticket counts as invalid |
password | String | For password sign-in | The password |
device_id | String | Yes | Device ID, 1 to 64 visible ASCII characters. Generated by the app and stored on the device, so the same device always uses the same value |
device_name | String | No | Device name such as iPhone 15, up to 64 characters (longer is truncated). Other devices signed out by this one see this name |
platform | String | No | ios, android, harmony, windows, macos, linux, web or mini_program; anything else or omitted is recorded as other. Used for per-platform device limits, see Multiple devices |
sdk_version | String | No | Client version, up to 32 characters (longer is truncated) |
Signing in again with the same device_id replaces that device's previous session. On the web, have all tabs in a browser share one device_id and one sign-in; signing in separately in each tab makes them replace each other.
Request example
Ticket sign-in:
curl -X POST "$IM_API/client/v1/sessions" \
-H "Content-Type: application/json" \
-d '{
"app_key": "1575529652#demo",
"grant_type": "ticket",
"ticket": "ult_RjcQrK8ff-Y0lfQ_7DqodLbw4WJZQNgdwIgOfsaW3gI",
"device_id": "5f0c2a9e-7d1b-4c7e-9a51-2f6d0f3b8c11",
"device_name": "iPhone 15",
"platform": "ios",
"sdk_version": "1.0.0"
}'Password sign-in:
curl -X POST "$IM_API/client/v1/sessions" \
-H "Content-Type: application/json" \
-d '{
"app_key": "1575529652#demo",
"grant_type": "password",
"username": "zhangsan",
"password": "Passw0rd!",
"device_id": "5f0c2a9e-7d1b-4c7e-9a51-2f6d0f3b8c11",
"device_name": "iPhone 15",
"platform": "ios",
"sdk_version": "1.0.0"
}'Response
On success you get 201 Created:
| Field | Type | Description |
|---|---|---|
access_token | String | The User Token. Clients send it as Authorization: Bearer <User Token> when calling client APIs and opening the long-lived connection |
access_token_expires_in | Number | User Token validity in seconds, 86400 (1 day) by default. May be null for no expiry |
refresh_token | String | The refresh token, starting with urt_ |
refresh_token_expires_in | Number | Session validity in seconds, 604800 (7 days) by default; refreshing before then extends it. May be null for no expiry |
session_id | String | This sign-in's session ID, the same as session_id in the device object |
user | Object | The user's own profile: username, nickname, avatar_url, attributes, has_password, version, as in the user object |
config | Object | The app's runtime config, such as device limits, message size limits, the recall window and whether read receipts are on; clients use it to decide what to show. Values come from the runtime policy in the console |
Both validity periods come from the app's runtime policy. Tokens are shortened in this example and config only shows some fields:
{
"access_token": "eyJhbGciOiJFZERTQSIsImtpZCI6ImsxIiwidHlwIjoiSldUIn0.eyJpc3Mi...",
"access_token_expires_in": 86400,
"refresh_token": "urt_99582942128373760_2AhAQZTPK86ahHMlW1waLQP2vOw0wJcGlFTuZPqGSEs",
"refresh_token_expires_in": 604800,
"session_id": "99582942128373760",
"user": {
"username": "zhangsan",
"nickname": "张三丰",
"avatar_url": "",
"attributes": { "gender": "1", "level": "vip" },
"has_password": true,
"version": 3
},
"config": {
"version": "1.1.0",
"max_online_devices_per_user": 4,
"device_limits_by_platform": null,
"single_read_ack_enabled": true,
"max_message_body_bytes": 5120,
"recall_window_seconds": 120,
"message_retention_days": 90
}
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | grant_type isn't password or ticket; device_id is missing or malformed; for password sign-in, the username is malformed or the password empty |
| 401 | invalid_credentials | Wrong username or password, the user doesn't exist or has no password; or the ticket is invalid, used, expired or revoked. These aren't distinguished, to prevent probing usernames. If ticket sign-in fails, the app can get a new ticket from your system once and retry |
| 403 | user_disabled | The user is banned. Only returned when the password is correct or the ticket valid, so you can show "Account banned" |
| 403 | tenant_unavailable | The tenant's service is suspended |
| 403 | app_unavailable | The app is disabled or suspended |
| 404 | not_found | No app matches app_key |
| 409 | device_limit_exceeded | The device limit is reached and the app is set to reject new devices, see Multiple devices |
| 429 | too_many_attempts | Too many wrong passwords, so password sign-in is temporarily blocked; wait for the Retry-After header and don't retry automatically. Ticket sign-in isn't affected |
| 429 | rate_limited | Too many sign-in requests; retry after Retry-After. At most 120 per client IP per minute, also counted toward the app's client request quota |
To prevent password guessing, failed password sign-ins are limited as follows. Devices already signed in and ticket sign-in aren't affected:
| Failures | Result |
|---|---|
| 10 failures for one user from one IP | That IP can't use password sign-in for that user for 15 minutes |
| 100 failures from one IP on one app within an hour | All password sign-in from that IP to that app is suspended |
| 100 failures for one user within an hour (any IP) | Password sign-in for that user is suspended |
Refresh and sign-out
- Refresh: before the User Token expires, the app calls
POST /client/v1/sessions/refreshwith{"refresh_token": "..."}and gets a newaccess_token,refresh_tokenand their validity. Each refresh issues a new refresh token, so save the new value; the session's validity is extended, so as long as the user uses the app within it, they stay signed in. - Sign-out: when the user signs out, the app calls
POST /client/v1/sessions/logoutwith{"refresh_token": "..."}, which returns204. Only clearing tokens locally makes the server think the device is still signed in, so it keeps using a device slot and keeps receiving this user's offline pushes. - Signed out remotely: after a device is signed out, replaced or the user is banned, client APIs return
401 session_revokedwith the reason indetails.reason, see Sign-out reasons. Don't refresh then; clear local tokens and go back to the sign-in screen. - Push is tied to the sign-in: the push token an app registers belongs to that sign-in. After signing out or being signed out, the device's push registration is removed automatically; the app doesn't need to unregister and registers again after the next sign-in.
Client self-registration
Users register themselves in the app without going through your server. Only allowed when the app's registration mode (registration_mode) is open, which you can change in App management in the console. New apps default to authorized, where only the server can create users and this call returns 403 permission_denied.
Only enable this for test apps
Anyone can call self-registration. Production apps should create users from your server, or create them automatically when issuing tickets.
- A password is required and
created_viaisclient; call Client sign-in after registering. - At most 10 registrations per client IP per minute.
- Nicknames go through moderation; a nickname matching a replacement rule is saved with the replacement.
/client/v1/usersRequest body
| Field | Type | Required | Description |
|---|---|---|---|
app_key | String | Yes | The app's AppKey |
username | String | Yes | Username, see Usernames and profiles |
password | String | Yes | Password, 1 to 64 characters |
nickname | String | No | Nickname, up to 64 characters |
Request example
curl -X POST "$IM_API/client/v1/users" \
-H "Content-Type: application/json" \
-d '{
"app_key": "1575529652#demo",
"username": "xiaoming",
"password": "secret123",
"nickname": "小明"
}'Response
On success you get 201 Created with the user's own profile:
{
"username": "xiaoming",
"nickname": "小明",
"avatar_url": "",
"attributes": {},
"has_password": true,
"version": 1
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | No password, or a parameter breaks the rules in Usernames and profiles |
| 403 | permission_denied | The app's registration mode isn't open, so clients can't register |
| 403 | tenant_unavailable / app_unavailable | The tenant or app is suspended, or the app is read-only |
| 403 | content_rejected | The nickname failed moderation; details.field is nickname |
| 404 | not_found | No app matches app_key |
| 409 | already_exists | The username is taken |
| 409 | limit_exceeded | The app has reached its registered-user limit |
| 429 | rate_limited | Too many registrations; retry after Retry-After |
Multiple devices
A user can be signed in on several devices at once, with one session per device. There are two levels of device limits, set in the app's runtime policy. device_limits_by_platform can't be changed in the console yet; contact us if you need per-platform limits:
| Policy | Description | Default |
|---|---|---|
max_online_devices_per_user | Total devices a user can be signed in on, 1 to 20 | 4 |
device_limits_by_platform | Devices within one platform group, 1 to 20 per group, for rules like "one phone and one computer". Groups are mobile (ios, android, harmony), desktop (windows, macos, linux), web, mini_program and other | No per-platform limit |
device_overflow_policy | What happens over the limit: kick_oldest signs out the earliest-signed-in device, reject_new rejects the new device | kick_oldest |
When a new device signs in, its platform group's limit is checked first, then the total:
kick_oldest: over the group limit, the group's earliest-signed-in device is signed out; over the total, the earliest of all devices is. The signed-out device gets reasondevice_limitplus the new device's name and platform, so it can show "Your account signed in on iPhone 15". For example, with{"mobile": 1, "desktop": 1}, a new phone signs out the old phone and computers aren't affected.reject_new: this sign-in returns409 device_limit_exceeded.Counted by session, not by being online: a device that's offline still uses a slot until it signs out, is signed out or its session expires. With
reject_new, if a user loses an old device, sign out that device to free the slot.Lowering the limit doesn't sign out devices already signed in; the new limit applies the next time a device signs in.
List devices
List the user's currently signed-in devices: only valid sessions (not signed out, logged out or expired), most recently active first. The number is bounded by the device limit, so there's no paging.
/{org_name}/{app_name}/users/{username}/sessionsPath parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username |
Request example
curl "$IM_API/$ORG/$APP/users/zhangsan/sessions" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK. items holds the devices, each a device object; empty when none are signed in.
{
"items": [
{
"session_id": "99583185116987392",
"device_id": "mac-1a2b3c",
"device_name": "MacBook Pro",
"platform": "macos",
"sdk_version": "1.0.0",
"login_method": "password",
"ip": "203.0.113.8",
"created_at": "2026-10-02T19:08:01.501Z",
"last_active_at": "2026-10-02T19:08:01.501Z",
"expires_at": "2026-10-09T19:08:01.501Z"
},
{
"session_id": "99582942128373760",
"device_id": "5f0c2a9e-7d1b-4c7e-9a51-2f6d0f3b8c11",
"device_name": "iPhone 15",
"platform": "ios",
"sdk_version": "1.0.0",
"login_method": "ticket",
"ip": "198.51.100.23",
"created_at": "2026-10-02T19:07:03.568Z",
"last_active_at": "2026-10-02T19:07:53.750Z",
"expires_at": "2026-10-09T19:07:53.750Z"
}
]
}Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | The user doesn't exist or was deleted |
Sign out all devices
Sign out all of a user's devices: online devices are disconnected with reason kicked, and issued but unused tickets become invalid. Each device's push token is unregistered, so they stop getting this user's pushes. Succeeds even if no devices are signed in.
Signing out doesn't stop the user from signing in again; to prevent sign-in, ban the user. Each call signs out every device signed in at that moment, so a retry after a timeout also signs out devices that signed in between the two calls.
/{org_name}/{app_name}/users/{username}/sessionsPath 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/zhangsan/sessions" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 204 No Content with no body.
Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | The user doesn't exist or was deleted |
Sign out one device
Sign out one of a user's devices; if online, it's disconnected with reason kicked. Its push token is unregistered and it stops receiving pushes, see Push devices and settings. The device can sign in again later.
Succeeds even if the device is already signed out (kicked, logged out or replaced), so retries are safe.
/{org_name}/{app_name}/users/{username}/sessions/{session_id}Path parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username |
session_id | String | The session ID, from session_id in List devices |
Request example
curl -X DELETE "$IM_API/$ORG/$APP/users/zhangsan/sessions/99583185116987392" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 204 No Content with no body.
Errors
| HTTP status | code | Description |
|---|---|---|
| 404 | not_found | The user doesn't exist or was deleted; the session doesn't exist or belongs to another user |
Sign-out reasons
When a device is signed out, online devices are disconnected and offline devices get 401 session_revoked on their next request, with the reason in details.reason:
reason | Cause |
|---|---|
replaced | The same device (same device_id) signed in again |
device_limit | The device limit was exceeded and a new device took the slot |
kicked | Signed out by your server, the console, or the user from another device |
password_changed | The password was changed or reset |
user_disabled | The user was banned |
user_deleted | The user was deleted |
relogin_required | The console required all users of the app to sign in again |
token_reuse | A refresh token that was already exchanged was used again and may have leaked |
expired | The session expired (not refreshed within its validity) |
For replaced and device_limit, details also has device_name and platform for the newly signed-in device:
{
"error": {
"code": "session_revoked",
"message": "登录设备数超过上限,你的账号已在其他设备登录",
"details": { "reason": "device_limit", "device_name": "Chrome", "platform": "web" },
"request_id": "LE3YXRXUWZWAUNQY7YODQNV45U"
}
}Data structures
Device object
| Field | Type | Description |
|---|---|---|
session_id | String | The session ID |
device_id | String | Device ID, submitted by the client at sign-in |
device_name | String | Device name; empty string if none |
platform | String | ios, android, harmony, windows, macos, linux, web, mini_program or other |
sdk_version | String | Client version; empty string if none |
login_method | String | ticket or password |
ip | String | The client IP at sign-in |
created_at | String | Sign-in time |
last_active_at | String | Time of the latest sign-in or refresh |
expires_at | String | When the session expires under the app's current runtime policy; refreshing before then extends it. May be null for no expiry |
{
"session_id": "99582942128373760",
"device_id": "5f0c2a9e-7d1b-4c7e-9a51-2f6d0f3b8c11",
"device_name": "iPhone 15",
"platform": "ios",
"sdk_version": "1.0.0",
"login_method": "ticket",
"ip": "198.51.100.23",
"created_at": "2026-10-02T19:07:03.568Z",
"last_active_at": "2026-10-02T19:07:53.750Z",
"expires_at": "2026-10-09T19:07:53.750Z"
}