Friend requests
Friend requests start in clients: A sends B a request, and B accepts or declines. Sending, accepting and declining can only be done by the users themselves in clients; your server can only query friend requests.
| You want to | Do this |
|---|---|
| See requests a user received or sent, in your admin tools | Query friend requests |
| Make two users friends from the server | Add a friend, without a request |
| Let your server decide whether users can add friends | The before-add-friend callback, see below |
| Change whether approval is needed | Friend add mode |
How requests are handled
After A sends a request, it's handled according to B's friend add mode:
| B's friend add mode | Result |
|---|---|
need_confirm (approval required, the default) | A pending request is created and B's online devices are notified. If B accepts, they become friends and A is told about the new friend; if B declines, A is told it was declined |
allow_any (anyone can add) | They become friends without B's approval. The request is still recorded with status "became friends", so B can see who added them |
deny_any (nobody can add) | The request can't be sent; A gets 403 friend_add_denied |
A few rules:
| Rule | Details |
|---|---|
| Mutual requests | If B has a pending, unexpired request to A and A then requests B, they become friends right away, even if B is deny_any: B's request already means yes |
| One request per pair | Requesting again while A's request to B is unhandled updates the message, preset remark, source and send time; B is notified again and the validity restarts. Declined or expired requests are updated the same way |
| Validity | Expires (expired) if unhandled 30 days after the latest send, and can no longer be accepted or declined. Records are deleted 60 days after sending |
| No withdrawal | Requesters can't withdraw requests. When A adds B to their block list, A's pending request to B is deleted |
| Server additions | After Add a friend, pending requests between the two become accepted, and the remark preset in a request is written to the requester's side |
| Request content | A message of up to 256 characters; an optional preset remark for the other user (up to 64 characters), written once they're friends; an optional add source (same rules as in the friend object). The other user can see the message and source |
Content checks: the message and add source go through moderation when sending; the remark isn't checked.
- If they fail, the request isn't sent and A gets
403 content_rejectedwithdetails.fieldmessageoradd_source, see Errors callers see. - A message matching a replacement rule is saved with the replacement, so queried messages may contain
*; an add source is an identifier and is rejected outright if it matches.
Before-add-friend callback: with the before-add-friend callback on, your server is asked before A sends a request (including ones that would be accepted automatically), and optionally also when B accepts. Server Add a friend doesn't call it.
- Order: after content checks, before the "already friends, block list, friend add mode, friend limit" checks.
- On rejection A gets
403 permission_denied, the request isn't written and B never sees it; if it's rejected when B accepts, B gets the same error and the request stays pending. details.reasonisapp_rejected(your server rejected it) orcallback_unavailable(the callback failed and the "reject" policy applied);details.app_reasonis your reason code andmessageyour text ("Operation rejected" if you gave none).
Limits on sending requests
To prevent harassment, sending requests is limited as follows. Over a limit A gets 429 rate_limited, with the seconds to wait in the Retry-After header and the limit in details.reason:
| Limit | details.reason |
|---|---|
| Each user can send at most 20 requests per minute and 500 per day | request_rate |
| One user can request the same person at most 5 times a day: after 5 in a row, about one more becomes available every 4.8 hours | pair_daily_limit |
| After being declined, no new request to that person for 24 hours | declined_cooldown |
Sending also checks the relationship:
| Case | A gets |
|---|---|
| Already friends | 409 already_exists |
A's friend list is full, or B is allow_any and B's is full | 409 limit_exceeded |
| A blocked B, or B blocked A (not checked when the app turns off block list enforcement, see Block list) | 403 user_blocked |
Request status
status | Description |
|---|---|
pending | Pending and not expired |
accepted | Became friends: the other user accepted, their allow_any mode accepted it automatically, they requested each other, or the server added them as friends |
declined | Declined |
expired | Expired: a pending request not handled within 30 days of the latest send |
Query friend requests
Query the requests a user received or sent, newest first, optionally filtered by status.
/{org_name}/{app_name}/users/{username}/friend-requestsPath parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
username | String | The username |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
direction | String | Yes | received for requests the user received, sent for requests they sent |
status | String | No | Filter by pending, accepted, declined or expired, see Request status. All when omitted |
limit | Number | No | Page size, default 20, 1 to 100 |
cursor | String | No | The cursor for the next page, see Paging. Keep direction and status the same while paging |
Request example
curl "$IM_API/$ORG/$APP/users/alice/friend-requests?direction=received" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK. items holds this page's requests, each a friend request object, and next_cursor is the next page's cursor (null when there's none). Requests involving deleted users are left out, so a page may have fewer than limit items; rely only on next_cursor.
Received requests:
{
"items": [
{
"username": "dave",
"nickname": "戴夫",
"avatar_url": "",
"message": "你好,我是戴夫",
"add_source": "search",
"status": "pending",
"unread": true,
"requested_at": "2026-10-02T19:12:59.406Z",
"expires_at": "2026-11-01T19:12:59.406Z",
"handled_at": null
},
{
"username": "erin",
"nickname": "艾琳",
"avatar_url": "",
"message": "你好,我是艾琳",
"add_source": "qrcode",
"status": "accepted",
"unread": false,
"requested_at": "2026-10-02T19:08:09.923Z",
"expires_at": null,
"handled_at": "2026-10-02T19:09:09.491Z"
}
],
"next_cursor": null
}Sent requests (direction=sent) have no unread, and include the requester's preset remark:
{
"items": [
{
"username": "alice",
"nickname": "爱丽丝",
"avatar_url": "",
"message": "你好,我是戴夫",
"remark": "小爱",
"add_source": "search",
"status": "pending",
"requested_at": "2026-10-02T19:12:59.406Z",
"expires_at": "2026-11-01T19:12:59.406Z",
"handled_at": null
}
],
"next_cursor": null
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | direction is missing or not received or sent; status isn't an allowed value; limit isn't between 1 and 100; cursor is invalid |
| 404 | not_found | The user doesn't exist or was deleted |
Data structures
Friend request object
A request from {username}'s point of view: in received requests the "other user" is the requester, in sent requests it's the person requested.
| Field | Type | Description |
|---|---|---|
username | String | The other user |
nickname | String | The other user's nickname; empty string if not set |
avatar_url | String | The other user's avatar address; empty string if not set |
message | String | The requester's message; empty string if none |
remark | String | Sent requests only: the remark the requester preset for the other user; empty string if none |
add_source | String | How they met; empty string if none |
status | String | pending, accepted, declined or expired, see Request status |
unread | Boolean | Received requests only: whether the user hasn't viewed it in a client yet. Only pending requests can be true |
requested_at | String | Time of the latest send |
expires_at | String | When it expires; only set for pending, null otherwise |
handled_at | String | When it was accepted, declined or became friends; null for pending and expired |
