Join requests and invitations
When a user applies to join a public group from a client, or a member invites someone, and they can't join directly, a pending join request is created for the owner, admins or invitee to handle.
Applying, inviting, approving, accepting and declining can only be done by users in clients; your server can only query:
| You want to | Do this |
|---|---|
| See a group's applications and invitations in your admin tools | Query join requests and invitations |
| Put someone in the group directly | Add group members, without their consent; their pending requests for the group become invalid |
| Let your server decide whether someone may join | The before-join-group callback, see Content checks and the before-join-group callback |
| Change whether approval is needed | The member_invite and join_mode group settings, see Group settings; for whether invitees confirm, see the user's group settings |
The three ways to join at a glance:
| Way | Started by | Joins directly when | Otherwise creates | Handled by |
|---|---|---|---|---|
| Applying to a public group | The user | The group's join_mode is free | application | Owner or admins |
| Member invitations | A group member | The inviter may invite directly and the invitee's group setting is allow_any | invite_review (a regular member's invitation that needs approval), invitation (waiting for the invitee) | Owner or admins; the invitee |
| Invite links | Created by the owner or an admin, opened by the user | Always | Nothing | — |
How joining works
Applying to a public group
- The user finds a public group in a client and applies, optionally with a reason of up to 256 characters. Private groups can't be applied to.
- If the group's
join_modeisfree, the user joins directly (joined_viaapply) with no request record; if it'sapproval, anapplicationrequest is created and the online devices of the owner and all admins are notified. - Once the owner or any admin accepts, the applicant joins and the request becomes
accepted; declining can include a reason, the request becomesdeclined, the applicant is notified, and they can't apply to that group again for 24 hours. - Applicants can withdraw their own pending applications, which become
canceled(cancel_causewithdrawn).
Applications from one user to one group are limited to 5 per day.
Member invitations
Members can invite up to 100 people at a time from a client, optionally with a message. Who may invite depends on the member_invite group setting: owners and admins can always invite; regular members can invite directly with free, need owner or admin approval with approval, and can't invite with disabled.
- When a regular member invites in a group that needs approval, an
invite_reviewrequest waits for the owner or an admin; once approved (accepted), the invitee is handled as in the next step; if declined, it becomesdeclined. - If the invitee's group setting is
allow_any, they join directly (joined_viainvite) with no request record; if it'sneed_confirm, aninvitationrequest is created and the invitee's online devices are notified. - Once the invitee accepts, they join and the request becomes
accepted; if they decline, it becomesdeclined. - Inviters can withdraw their own pending invitations, and owners and admins can withdraw anyone's; they become
canceled(cancel_causewithdrawn).
Invitation cooldown: the app policy group_invite_cooldown_enabled is off by default, so someone who left a group or declined an invitation can be invited again right away. Turn it on under "App policy → Groups" in the console to block re-inviting that user to the same group for 24 hours.
- With the cooldown off, confirmation and approval rules still apply.
- The 24-hour cooldown after a declined application isn't affected by this switch.
- Adding members from the server isn't subject to the invitation cooldown.
Invitations also check that:
- The invitee isn't on the group block list.
- The group's size and the invitee's group count are within their limits.
- With block list enforcement on (
user_blacklist_enabledin the runtime policy), the invitee hasn't put the inviter on their block list. - With "invite friends only" on (
group_invite_friends_only), members can only invite their own friends.
Invite links
Owners and admins can create invite links in clients, valid for 1 to 7 days, which clients can show as QR codes. A user who opens the link and confirms joins directly (joined_via link), even for private groups, with no approval, confirmation or request record. Each group has only one valid link at a time; creating a new one or revoking it invalidates the old one immediately, and a link also stops working when its creator is no longer the owner or an admin.
Checks when handling requests
Accepting an application, approving a member's invitation and accepting an invitation re-check the conditions for joining: the group is normal (not banned or dissolved), the user isn't on the group block list, and the group's size and the user's group count are within their limits. If not, the operation fails and the request stays pending; it can be handled again once the conditions are met (for example after raising the group's size limit).
If two admins handle the same request at the same time, the later one is told it was already handled, and nobody joins twice.
Content checks and the before-join-group callback
Moderation: application reasons, invitation messages (including messages to initial members when a user creates a group in a client) and reasons for declining go through moderation when submitted.
- If they fail, the operation doesn't take effect and the client gets
403 content_rejectedwithdetails.fieldmessageorreason, see Errors callers see. - Text matching a replacement rule is saved with the replacement, so queried
messageandreasonmay contain*.
Before-join-group callback: with the before-join-group callback on, your server is asked before the operations below (after the moderation check). Your server adding group members doesn't call it.
| Client operation | When rejected |
|---|---|
| Creating a group with initial members, inviting others | Each rejected person gets permission_denied in the results; everyone else is handled as usual and the group is still created |
| Applying to a public group, joining through an invite link | The whole request returns 403 permission_denied |
| Accepting an application, approving a member's invitation, accepting an invitation | Not called |
details.reason is app_rejected (your server rejected it) or callback_unavailable (the callback failed and the "reject" policy applied), plus details.app_reason.
Request status and validity
There are three kinds of request, kind:
kind | Description | Handled by |
|---|---|---|
application | A user applying to join a public group | Owner or admins |
invite_review | A regular member's invitation in a group that needs approval | Owner or admins |
invitation | An invitation waiting for the invitee to confirm | The invitee |
A group has only one record per user per kind. Starting it again (applying again, inviting again) updates that record: it's pending again, requested_at becomes the latest time, and the previous result is cleared.
Pending requests expire 7 days after the latest start if unhandled. Records are deleted 30 days after being started, and together with the members when the group is dissolved.
Request status:
status | Description |
|---|---|
pending | Pending |
accepted | Accepted: an application approved, an invitation accepted, or a member's invitation approved |
declined | Declined; reason holds the reason |
canceled | No longer valid; cancel_cause says why |
expired | Not handled within 7 days |
Why a request became invalid, cancel_cause:
cancel_cause | Description |
|---|---|
withdrawn | Withdrawn: the applicant withdrew the application, or the inviter, owner or an admin withdrew the invitation |
joined | The user joined the group some other way (added by the server, invited by someone else, and so on) |
blacklisted | The user was added to the group block list |
group_private | The public group became private, invalidating all earlier applications, which don't come back if it turns public again (application only) |
not_eligible | When approving a member's invitation, the invitee could no longer receive invitations to this group (invite_review only) |
dismissed | The group was dissolved |
While a group is banned, its pending requests are paused: queries show them as canceled with cancel_cause null and they can't be handled; after unbanning, unexpired ones go back to pending.
Query join requests and invitations
Get a group's join requests of all three kinds, newest first, page by page, optionally filtered by kind and status.
Statuses are computed at query time (for example, expired or the group is banned), so a page may have fewer than limit items; rely on next_cursor to know whether there's another page.
/{org_name}/{app_name}/groups/{group_id}/requestsPath parameters
For org_name and app_name, see the server overview.
| Parameter | Type | Description |
|---|---|---|
group_id | String | The group ID |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | String | No | Only this kind: application, invite_review or invitation |
status | String | No | Only this status: pending, accepted, declined, canceled or expired |
limit | Number | No | Page size, default 20, 1 to 100 |
cursor | String | No | The cursor for the next page, see Paging |
Request example
curl -X GET "$IM_API/$ORG/$APP/groups/99582688326844416/requests?limit=3" \
-H "Authorization: Bearer $APP_TOKEN"Response
On success you get 200 OK, with each item in items a join request object and the next page's cursor in next_cursor (null when there's no next page).
{
"items": [
{
"group_id": "99582688326844416",
"kind": "invitation",
"username": "chener",
"nickname": "陈二",
"avatar_url": "https://cdn.example.com/avatar/chener.png",
"inviter": {
"username": "lisi",
"nickname": "李四",
"avatar_url": "https://cdn.example.com/avatar/lisi.png"
},
"message": "周末一起去拍日出",
"reason": "",
"status": "pending",
"cancel_cause": null,
"requested_at": "2026-10-02T19:10:58.660Z",
"expires_at": "2026-10-09T19:10:58.660Z",
"handled_at": null,
"handled_by": null
},
{
"group_id": "99582688326844416",
"kind": "application",
"username": "qianyi",
"nickname": "钱一",
"avatar_url": "https://cdn.example.com/avatar/qianyi.png",
"inviter": null,
"message": "喜欢拍风光",
"reason": "",
"status": "pending",
"cancel_cause": null,
"requested_at": "2026-10-02T19:10:58.496Z",
"expires_at": "2026-10-09T19:10:58.496Z",
"handled_at": null,
"handled_by": null
},
{
"group_id": "99582688326844416",
"kind": "invitation",
"username": "sunqi",
"nickname": "孙七",
"avatar_url": "https://cdn.example.com/avatar/sunqi.png",
"inviter": {
"username": "lisi",
"nickname": "李四",
"avatar_url": "https://cdn.example.com/avatar/lisi.png"
},
"message": "欢迎加入",
"reason": "",
"status": "accepted",
"cancel_cause": null,
"requested_at": "2026-10-02T19:10:46.831Z",
"expires_at": null,
"handled_at": "2026-10-02T19:10:46.913Z",
"handled_by": "sunqi"
}
],
"next_cursor": "U7pn0kDw8E-YL7NKq_kENEd90r-JeBiz-xsQjenN4Dv_-H8IWD2XbO4kuGJmYuLlerJn-EImW29ht1_vqRfziguogEpszO1CEQrNOjcFsnHultBR7C4nPUPR2XYdu9DaZqeckq6rFTZESsRe2wv5koZrktk6m0rXVuMbRQ"
}Errors
| HTTP status | code | Description |
|---|---|---|
| 400 | invalid_argument | kind or status has an invalid value, or limit is out of range |
| 404 | not_found | The group doesn't exist |
Data structures
Join request object
| Field | Type | Description |
|---|---|---|
group_id | String | The group ID |
kind | String | application, invite_review or invitation, see Request status and validity |
username | String | Who the request is about: the applicant or invitee |
nickname | String | Their nickname; empty string if none |
avatar_url | String | Their avatar address; empty string if none |
inviter | Object | The inviter, with username, nickname and avatar_url; null for application or when the inviter was deleted |
message | String | The application reason or invitation message; empty string if none. Shown with replacements if it matched moderation replacement rules |
reason | String | The reason for declining; empty string if none. Shown with replacements if it matched moderation replacement rules |
status | String | pending, accepted, declined, canceled or expired |
cancel_cause | String | Why it became invalid; only possibly set when status is canceled, otherwise null |
requested_at | String | When it was last started |
expires_at | String | When it expires; only set when status is pending, otherwise null |
handled_at | String | When it was handled; null if not yet |
handled_by | String | Who handled it: the owner or admin who accepted or declined, the invitee who accepted or declined, or whoever withdrew it; null when it lapsed automatically or that person was deleted |
