Skip to content

Add createUserModeration (instance kick) - #617

Open
elsy0111 wants to merge 3 commits into
vrchatapi:mainfrom
elsy0111:feat/user-moderations
Open

elsy0111 wants to merge 3 commits into
vrchatapi:mainfrom
elsy0111:feat/user-moderations

Conversation

@elsy0111

@elsy0111 elsy0111 commented Nov 4, 2026 •

Copy link
Copy Markdown

Closes #424

This adds POST /user/{userId}/moderations as createUserModeration.

kick-flow

A kick doesn't remove the user from the instance. Once they leave, they can't join that instance again, even from an invite. I tested this with two of my accounts, once in a Friends+ instance and once in an Invite instance, and got the same result both times.

The kick only goes through when worldId and instanceId point at the instance the user is in. If either one is missing, or the user isn't there, the API returns 403. The validator itself only requires type and reason, and the type enum comes from its error message. The payload in the issue also has targetUserId and created, but the API doesn't validate either one, so I left them out. Sending another moderation for the same user within about a minute (63 seconds in my tests) returns 429.

Request Response
Both fields, target in that instance 200
Both fields, target in a different instance 403
Both fields, target left the instance 403
Only one of the fields, or neither 403
Caller as the target 403
Same target again right away 429
Requests and responses
Kick, target in a Friends+ instance
POST /api/1/user/usr_<target>/moderations
{"type":"kick","targetUserId":"usr_<target>","reason":"test","created":"0001-01-01T00:00:00.0000000","expires":"0001-01-01T00:00:00.0000000","isPermanent":false,"worldId":"wrld_<world>","instanceId":"07444~hidden(usr_<owner>)~region(jp)"}
200 {"id":"mod_f862716d-23bf-49e0-a931-a08759450834"}

Kick, target in an Invite instance
POST /api/1/user/usr_<target>/moderations
{"type":"kick","reason":"test","worldId":"wrld_<world>","instanceId":"42690~private(usr_<owner>)~region(jp)"}
200 {"id":"mod_f51bf03d-e899-481e-af3d-b1012d9dc3a8"}

Another instance, target is in a different one
POST /api/1/user/usr_<target>/moderations
{"type":"kick","reason":"test","worldId":"wrld_<world>","instanceId":"67928~hidden(usr_<owner>)~region(jp)"}
403 {"error":{"message":"You cannot moderate this user․","status_code":403}}

Target left the instance (never kicked from it)
POST /api/1/user/usr_<target>/moderations
{"type":"kick","reason":"test","worldId":"wrld_<world>","instanceId":"42690~private(usr_<owner>)~region(jp)"}
403 {"error":{"message":"You cannot moderate this user․","status_code":403}}

Only instanceId, target in the instance
POST /api/1/user/usr_<target>/moderations
{"type":"kick","reason":"test","instanceId":"42690~private(usr_<owner>)~region(jp)"}
403 {"error":{"message":"You cannot moderate this user․","status_code":403}}

Only worldId, target in the instance
POST /api/1/user/usr_<target>/moderations
{"type":"kick","reason":"test","worldId":"wrld_<world>"}
403 {"error":{"message":"You cannot moderate this user․","status_code":403}}

Neither, target in the instance
POST /api/1/user/usr_<target>/moderations
{"type":"kick","reason":"test"}
403 {"error":{"message":"You cannot moderate this user․","status_code":403}}

Caller as the target
POST /api/1/user/usr_<owner>/moderations
{"type":"kick","reason":"x"}
403 {"error":{"message":"You cannot moderate this user․","status_code":403}}

Same target again right away
POST /api/1/user/usr_<owner>/moderations
{"type":"kick","reason":"x"}
429 {"error":{"message":"Too many moderations being created at one player․","status_code":429}}

Unknown type
POST /api/1/user/usr_00000000-0000-0000-0000-000000000000/moderations
{"type":"x","reason":"x"}
400 {"error":{"message":"type must be one of ["ban"‚"note"‚"warning"‚"nuisance"‚"hardban"‚"kick"‚"warn"‚"banpubliconly"‚"publicinstance"‚"avatar"‚"world"‚"event"‚"group"‚"voice"‚"chat"‚"profile"‚"sticker"‚"emoji"‚"gallery"‚"print"‚"icon"]‚ not "x"","status_code":400}}

Missing fields
POST /api/1/user/usr_00000000-0000-0000-0000-000000000000/moderations
{}
400 {"error":{"message":"type is required․‚ reason is required․","status_code":400}}

Bad values
POST /api/1/user/usr_00000000-0000-0000-0000-000000000000/moderations
{"type":"kick","reason":"test","worldId":"x","instanceId":"x","targetUserId":"x","created":"x","expires":"x","isPermanent":"x"}
400 {"error":{"message":"expires˸ cannot parse date string˸ 'x'‚ isPermanent must be a boolean˸ 'x'","status_code":400}}

Unknown user
POST /api/1/user/usr_00000000-0000-0000-0000-000000000000/moderations
{"type":"kick","reason":"test"}
404 {"error":{"message":"User usr_00000000-0000-0000-0000-000000000000 not Found","status_code":404}}

No auth cookie
POST /api/1/user/usr_00000000-0000-0000-0000-000000000000/moderations
{"type":"kick","reason":"test"}
401 {"error":{"message":"\"Missing Credentials\"","status_code":401}}

User and world IDs are replaced with placeholders.

The new workflows cover 400, 401, 403, 404 and 429, using the caller's own account and a user that doesn't exist, and they all pass. Because of the 429, create-user-moderation-self fails if it runs twice within a minute. The 200 needs a second account inside the instance, so the suite can't cover it.

Copilot AI balanced review requested due to automatic review settings November 4, 2026 15:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Add kick endpoint

2 participants