Sixteen agents, one controller each, over every route the coverage test reported as missing or wrong-verb. Route+verb coverage is now 516/522 (98.9%), up from 90.8%. Each handler was written against binary evidence rather than the audit's prose: verb from the BestHTTP.HTTPMethods ordinal at the call site, response shape from the issuing method's return type, request shape from whether the client uses form fields, a raw JsonUtility body, or query params. Agents were told to skip rather than stub, and several did. New surface worth noting: - Link host: actionlink create/get/consume and datalink get/put, previously unrouted entirely. - Club roster reads: members, banned, requests, requests/search, members/search, each with the client's real sort/paging params and the wire row shape its reader expects (the banned row has no MembershipType field, so it isn't sent). - Home clubhouse set/clear, club disband (soft, via the existing State column), and DELETE mainimage as its own action rather than a verb alias — the client parses the details envelope from it, so it is not status-only. - Club chat threads, comment CRUD, keepsake collect/delete, progression-event xp boosts, image report and cheered/bulk, iOS StoreKit product reporting. The six routes still missing all need storage that does not exist yet (per-role club permissions, club additional-image slots, MinLevel, ClubChatEnabled). Those are left for a single schema change rather than faked. Also excludes DELETE /account/me from the endpoint contract probe. That route is now a real, correctly-guarded account purge, and the probe walks every route with one test player — so calling it destroyed the probe's own session and turned one real event into 49 cascading foreign-key and 401 failures.
39 KiB
URL coverage — Rec Room 2020 client vs DorkNet server
Generated: 2026-05-09. Source: client URLs from Cpp2IL_ISIL ISIL dump (curated list provided to this report); server routes harvested via grep of [HttpGet]/[HttpPost]/[HttpPut]/[HttpDelete]/[Route] attributes across Controllers/**/*.cs plus the namespace-wildcard fall-throughs in ApiNamespaceStubsController and the global ApiCatchAllController / GlobalCatchAllController.
Snapshot scope: this file is a historical controller-coverage matrix, not the live gateway route table. The dedicated microservice hosts still reuse the shared controller stack behind route guards, so the endpoint shape notes remain useful. For current service ownership, use
DorkNet.Contracts/DorkNetRouteOwnership.csor the gateway/internal/routesendpoint.
How this matrix is built
Each client URL is matched against the server's full route table. ASP.NET Core routes resolve "specific wins over wildcard", so a route literally registered as api/foo/v1/bar always wins over api/foo/{*path}. The Status column reflects the route that actually wins:
- REAL — a specific route exists and the body does real work: hits
DorkNetDbContext, mutates entities, looks up presence, computes results, etc. Round-trips across sessions. - STUB — a specific route exists but the body just returns
Ok(new { }),Ok(Array.Empty<object>()), anAck()({success:true,error:""}), or a hand-rolled empty-but-shape-correct DTO so the strict 2020 deserialiser doesn't throw. No persistence; the client thinks it succeeded but nothing is actually stored. - WILDCARD — no specific route exists. The request is absorbed by
ApiNamespaceStubsController's namespace catch-all (api/<ns>/{*path}), which returns either[](list-typed namespaces) or{}(object-typed namespaces) or{success:true}(bug/report/sanitize namespaces). - MISSING — neither a specific route nor a namespace wildcard handles it. Falls through to the very last-resort
GlobalCatchAllController(/{*path}), which logs the URL and returns200 {}. From the client's point of view this looks like an empty object response — usually triggers a deserialiser crash on the watch unless the call site happened to be the rare one that tolerates{}.
The Handler column points at the controller method (or wildcard route attribute) that wins. File paths can be reconstructed from the namespace prefix: Controllers/API/<NamespacePart>/....
Summary
- Total client URLs (api/*): 156
- Total client URLs (non-api goto/heartbeat): 7
- REAL: 88
- STUB: 67
- WILDCARD: 6
- MISSING: 2 (both covered by the global
{*path}returning200 {}— neither has a specific or namespace handler)
Note: counts are approximate because several client URLs (api/avatar/v3/saved, the groups/v1/{groupId} GET vs POST pair, playerevents/v2) collapse to the same handler under different verbs. They're listed separately below.
Coverage by API prefix
api/announcement/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/announcement/v1/get |
STUB | AllEndpointsController.MiscLists → [] |
api/avatar/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/avatar/v2 |
REAL | AvatarV2Controller.GetAvatar (reads AvatarEntity for current player) |
| GET | api/avatar/v2/gifts |
REAL | AvatarGiftsController.GetGifts |
| POST | api/avatar/v2/gifts |
REAL | AvatarGiftsController.GenerateGifts (api/avatar/v2/gifts/generate + the bare gifts POST verb-overlap) — actually the bare-path POST falls through to ApiNamespaceStubsController's api/avatar/{*path} is not registered (api/avatar/ is NOT in the namespace wildcard list), so this lands on the global {*path} returning {}. Treat as STUB-via-global. |
| POST | api/avatar/v2/gifts/consume/{giftId} |
REAL | AvatarGiftsController.ConsumeGift (id) — Controllers/API/Avatar/V2/AvatarGiftsController.cs line 99 |
| POST | api/avatar/v2/set |
REAL | AvatarV2Controller.SetAvatar (writes AvatarEntity) |
| GET | api/avatar/v3/saved |
REAL | AvatarSavedOutfitsController.GetSaved (V3) — pulls saved outfit slots from DB |
| POST | api/avatar/v3/saved/set |
REAL | AvatarSavedOutfitsController.SetSaved (writes the slot) |
| GET | api/avatar/v4/items |
REAL | AvatarItemsController.GetItems — returns owned-item ids from inventory tables |
api/bugreporting/
| Verb | URL | Status | Handler |
|---|---|---|---|
| POST | api/bugreporting/v2/reportbug |
REAL | BugReportsController.ReportBugV2 (persists BugReportEntity) |
api/catalog/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/catalog/v1/all |
STUB | ApiNamespaceStubsController.CatalogAll → [] (explicit override of the namespace wildcard so the SKU shape matches) |
api/challenge/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/challenge/v2/getCurrent |
REAL | ProgressionController.GetChallengeV2 — pulls active challenge for the player from the progression tables |
| POST | api/challenge/v2/updateProgress |
REAL | ProgressionController.UpdateChallengeProgress — writes back delta into progression rows |
api/checklist/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/checklist/v1/current |
REAL | ProgressionController.GetChecklistV1 |
| POST | api/checklist/v1/complete |
REAL | ProgressionController.CompleteChecklistV1 (marks checklist item done in progression) |
api/communityboard/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/communityboard/v1/current |
REAL | CommunityBoardController.Current — reads data/community_board.json via CommunityBoardService, returns FeaturedPlayer / CurrentAnnouncement shape |
api/config/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/config/v2 |
REAL | Config.V2.ConfigController.GetConfig — returns the full RecRoomConfig populated by ConfigService.GetConfig(baseUrl) |
| GET | api/config/v1/amplitude |
STUB | Config.V1.ConfigController.Amplitude (returns { AmplitudeKey = "" }) — also has a duplicate stub at AllEndpointsController.Amplitude; the V1 ConfigController route wins via more-specific match |
| GET | api/config/v1/cohortnux/{cohortId} |
STUB | Config.V1.ConfigController.CohortNux — returns the empty cohort NUX shape |
api/consumables/
| Verb | URL | Status | Handler |
|---|---|---|---|
| POST | api/consumables/v1/updateActive |
REAL | InventoryController.UpdateActiveConsumable (writes the active-consumable row) |
| POST | api/consumables/v1/consume |
REAL | InventoryController.ConsumeConsumable (decrements inventory count) |
| GET | api/consumables/v1/getUnlocked |
REAL | InventoryController.GetUnlockedConsumables — reads the player's consumable inventory |
api/equipment/
| Verb | URL | Status | Handler |
|---|---|---|---|
| POST | api/equipment/v1/update |
REAL | InventoryController.UpdateEquipment — persists the equip-loadout selections |
| GET | api/equipment/v2/getUnlocked |
REAL | InventoryController.GetUnlockedEquipment (V2) — reads the player's owned equipment list |
api/gameconfigs/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/gameconfigs/v1/all |
STUB | GameConfigurationsController.GameConfigsAll (also shadowed by AllEndpointsController.GameConfigs — the explicit gameconfigs/v1/all route wins). Returns []. |
api/groups/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/groups/v1/{groupId} |
REAL | GroupsController.GetGroup — reads GroupEntity |
| GET | api/groups/v1/memberships/{groupId} |
REAL | GroupsController.GetMemberships (pulls members from GroupMembershipEntity) |
| POST | api/groups/v1 |
REAL | GroupsController.CreateGroup — inserts GroupEntity + creator membership |
| POST | api/groups/v1/delete/{groupId} |
REAL | GroupsController.DeleteGroup |
| POST | api/groups/v1/{groupId} |
WILDCARD | No specific POST api/groups/v1/{id} route exists — only the GET. Falls through to ApiNamespaceStubsController api/groups/{*path} → []. The 2020 client likely uses this verb to update group metadata; nothing is persisted. |
| POST | api/groups/v1/name/{groupId} |
WILDCARD | Same as above — only the GET-by-name (api/groups/v1/name/{name}) is implemented. The name/{groupId} POST hits the namespace wildcard. |
api/images/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/images/v1/listsaved |
STUB | MissingEndpointsController.ImagesListSaved → [] (the real saved-image list is served at api/images/v2/saved+ via ImagesController; the v1 surface is empty for the 2020 client) |
| POST | api/images/v1/modifyaccessibility |
STUB | MissingEndpointsController.ImagesAck → Ack() |
| POST | api/images/v1/deletesaved |
STUB | MissingEndpointsController.ImagesDeleteSaved → Ack() |
| POST | api/images/v1/sendlink |
STUB | MissingEndpointsController.ImagesAck → Ack() |
| POST | api/images/v1/cheer |
STUB | MissingEndpointsController.ImagesAck → Ack() |
| GET | api/images/v2/named |
STUB | MiscStubsController.NamedImages → [] (overrides the wildcard's {} to satisfy the strict ExpectListResponse<NamedImageDTO> deserialiser) |
| GET | api/images/v1/slideshow |
STUB | MiscStubsController.SlideshowInfo — returns { ValidTill = 9999-12-31, Images = [] }; required keys present so SlideshowInfoDTO.Deserialize succeeds, but no actual slideshow images |
api/inventions/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/inventions/v1/personaldetails/{playerId} |
WILDCARD | InventionsController only registers api/inventions/v1/personaldetails/{id:long} for the SAVED count by player — wait — re-reading: route IS registered (line 173 in InventionsController.cs). REAL. → InventionsController.PersonalDetails reads invention saved-count for the target player |
| GET | api/inventions/v1?inventionId={id} |
REAL | InventionsController.GetInvention (api/inventions/v1) reads InventionEntity by query param |
| GET | api/inventions/v1/update |
REAL | InventionsController.Update — yes, the client uses GET for "update invention metadata" (the ISIL routing tag uses Core.Get); writes back the invention row |
| POST | api/inventions/v1/settags |
REAL | InventionsController.SetTags — replaces the Tags column on the invention |
| POST | api/inventions/v1/batch |
REAL | InventionsController.Batch — bulk-fetch inventions by id list, returns multi-row payload |
| GET | api/inventions/v1/details |
REAL | InventionsController.Details — extended invention payload with version/creator metadata |
| GET | api/inventions/v1/versions |
REAL | InventionsController.Versions — list of InventionVersionEntity rows for an invention |
| GET | api/inventions/v1/creatorIds |
REAL | InventionsController.CreatorIdsInRoom — distinct creator account ids of the inventions placed in the room, resolved via InventionEntity.CreationRoomId (direct + imported-cluster, shared with /room). List<int> |
| GET | api/inventions/v1/tagfilters |
REAL | InventionsController.TagFilters — returns the curated tag-filter set (currently empty list, but registered as a real handler) |
| GET | api/inventions/v1/delete |
REAL | InventionsController.Delete — soft-deletes an invention the caller owns |
| GET | api/inventions/v2/publish |
REAL | InventionsController.PublishV2 — flips IsPublished=true, sets FirstPublishedAt |
| GET | api/inventions/v1/unpublish |
REAL | InventionsController.Unpublish — flips IsPublished=false |
| GET | api/inventions/v1/download |
REAL | InventionsController.Download — streams the invention payload + bumps NumDownloads |
| GET | api/inventions/v1/search |
REAL | InventionsController.Search — text/@username search over the inventions table |
| POST | api/inventions/v1/report |
REAL | InventionsController.Report — persists InventionReportEntity |
| POST | api/inventions/v1/cheer |
REAL | InventionsController.Cheer — increments CheerCount and inserts cheer row, fires notification |
| GET | api/inventions/v1/mine |
REAL | InventionsController.Mine (delegates to the SAVED-by-creator query, returns the caller's own creations) |
| GET | api/inventions/v3/popular |
REAL | InventionsController.Popular — most-cheered published inventions |
| GET | api/inventions/v3/saved |
REAL | InventionsController.Saved — caller's own creations (SAVED list) |
| POST | api/inventions/v3/save |
REAL | InventionsController.Save — creates a new InventionEntity (initial save) |
| POST | api/inventions/v3/addversion |
REAL | InventionsController.AddVersion — appends InventionVersionEntity, bumps CurrentVersionNumber |
| POST | remote-run/push-to-studio |
REAL | InventionsController.PushToStudio (also roomserver/ alias) — Rec Room Studio remote-run handoff. Raw-JSON body {SessionId, RoomId, SubRoomId, UnityAssetId, RoomData:{Filename,Hash,OwnershipProof}, SubRoomData:{…}, SavedByAccountId}; stamps the pushed RoomDataBlobEntity with its room/sub-room, records UnityAssetId on the RoomScene, sets Rooms.StudioSessionId, keeps the push under remoterun:{SessionId} in PlayerSettings. Does NOT commit the live save (that stays rooms/{id}/subrooms/{id}/data). Replies with the flattened RemoteRunDetails |
api/Leaderboard/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/Leaderboard/v2/getPlayerRank |
STUB | AllEndpointsController.MiscLists (matched via the api/Leaderboard/v1/{*path} GET stub for v1; v2/getPlayerRank specifically falls into the ApiNamespaceStubsController api/Leaderboard/{*path} → []). Functionally STUB-via-WILDCARD. |
| POST | api/Leaderboard/v1/SetStats |
WILDCARD | No specific route. ApiNamespaceStubsController api/Leaderboard/{*path} → [] — the stat row isn't persisted. |
api/messages/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/messages/v1/favoriteFriendOnlineStatus |
REAL | Messages.V2.MessagesController.FavoriteFriendOnlineStatus — joins relationships+presence to return a per-friend online/offline list |
| POST | api/messages/v1/sendMultiple |
REAL | MissingEndpointsController.MessagesSendMultiple — fan-out: persists one MessageEntity per recipient |
| POST | api/messages/v2/send |
REAL | Messages.V2.MessagesController.Send — inserts a MessageEntity and fires the SignalR notify |
| GET | api/messages/v2/get |
REAL | Messages.V2.MessagesController.Get — returns the recipient inbox from MessageEntity rows |
| GET | api/messages/v1/IOSGetNotificationPreferences |
REAL | IOSNotificationPrefsController.Get — reads per-player iOS pref settings (table-backed) |
| POST | api/messages/v1/IOSModifyNotificationPreferences |
REAL | IOSNotificationPrefsController.Modify — upserts the prefs row |
| POST | api/messages/v1/send |
STUB | No api/messages/v1/send POST route. Falls through to global {*path} → 200 {}. The v1 send was superseded by v2; the 2020 client almost always hits v2 instead. MISSING-via-global. |
api/notification/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/notification/hub/v1 |
STUB | Notification.V2.NotificationController.Connect returns 410 Gone with a hint to connect to notify.rec.net/hub/v1 instead — not the same path, so the bare /hub/v1 GET is intercepted by the V2 controller's [Route("api/[controller]/v2")] only when the URL path is api/notification/v2. The literal client URL api/notification/hub/v1 has no specific handler; it lands on the global {*path} → 200 {}. The actual notification stream the client wants is the SignalR hub at notify.rec.net/hub/v1. |
api/objectives/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/objectives/v1/myprogress |
REAL | MiscStubsController.MyObjectiveProgress — reads ObjectiveProgressEntity rows for the player and emits the strict Objectives + ObjectiveGroups shape the client's MyProgress.Deserialize requires |
| POST | api/objectives/v1/updateobjective |
WILDCARD | No specific route. ApiNamespaceStubsController api/objectives/{*path} → []. Per-objective progress increments aren't actually persisted — only the cleargroup route below writes anything. |
| POST | api/objectives/v1/cleargroup |
REAL | MiscStubsController.ClearGroup — upserts an ObjectiveProgressEntity row keyed group:{n}, sets IsCompleted=true, returns the wire-shape the client's ObjectiveGroupProgress.Deserialize requires |
| POST | api/objectives/v1/completegroup |
REAL | ProgressionController.CompleteObjectiveGroup — same as ClearGroup but also grants the group's reward |
api/PlayerCheer/
| Verb | URL | Status | Handler |
|---|---|---|---|
| POST | api/PlayerCheer/v1/create |
REAL | MissingEndpointsController.PlayerCheerCreate — inserts CheerEntity for the target player |
| POST | api/PlayerCheer/v1/SetSelectedCheer |
REAL | MissingEndpointsController.SetSelectedCheer — upserts PlayerSettings[SelectedCheer] for the caller |
api/PlayerElo/
| Verb | URL | Status | Handler |
|---|---|---|---|
| POST | api/PlayerElo/v1/reportPlayerElo |
WILDCARD | ApiNamespaceStubsController api/PlayerElo/{*path} → Object() {}. Match-result Elo deltas aren't recorded. |
api/playerevents/
| Verb | URL | Status | Handler |
|---|---|---|---|
| POST | api/playerevents/v1/{eventId}/responses |
WILDCARD | No specific responses POST route — only {eventId}/rsvp (POST) and respond (POST). Falls into the global {*path} since api/playerevents/ is NOT in the namespace wildcard list. Effectively MISSING-via-global. |
| GET | api/playerevents/v2 |
STUB | No specific route at the bare api/playerevents/v2 path; only api/playerevents/v2/delete/{id} (DELETE) exists. Falls into the global {*path} → {}. The 2020 watch usually reads the per-player feed from api/playerevents/v1/all (REAL) instead. |
| GET | api/playerevents/v2/{eventId} |
STUB | Same — no specific GET route for a single v2 event. Global {*path} → {}. |
| POST | api/playerevents/v1/respond |
REAL | MissingEndpointsController.PlayerEventRespond — upserts PlayerEventResponseEntity (RSVP yes/no/maybe) |
| POST | api/playerevents/v1/deleteResponse |
REAL | MissingEndpointsController.PlayerEventDeleteResponse — deletes the RSVP row |
| POST | api/playerevents/v1/bulkInvite |
REAL | PlayerEventsController.BulkInvite — fires per-recipient invite notifications via the notification service |
| GET | api/playerevents/v1/all |
REAL | MiscStubsController.AllPlayerEvents — joins PlayerEvents (caller-created) + PlayerEventResponses (caller-RSVPed) for the player's local "my events" panel |
api/PlayerReporting/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/PlayerReporting/v2/detail |
WILDCARD | ApiNamespaceStubsController api/PlayerReporting/{*path} → Ack() {success:true}. The 2020 watch reads moderation detail; we ack so the UI doesn't crash, but no real moderation history is returned. |
| POST | api/PlayerReporting/v2/ban |
WILDCARD | Same wildcard Ack(). Bans posted via this v2 path aren't persisted (the canonical persistence path is api/PlayersBanned/v2/ban REAL below). |
| POST | api/PlayerReporting/v1/deviceId |
REAL | InGameModerationController.ReportDeviceId — persists the device ID -> player association for moderation lookups |
| POST | api/PlayerReporting/v1/hile |
WILDCARD | ApiNamespaceStubsController api/PlayerReporting/{*path} → Ack(). Steam signature/anti-cheat ping; intentionally not stored. |
| POST | api/PlayerReporting/v3/create |
REAL | InGameModerationController.CreateReport — inserts a PlayerReportEntity and notifies moderators |
| POST | api/PlayerReporting/v3/voteToKick |
REAL | InGameModerationController.VoteToKick — records the kick vote and triggers room-instance kick if threshold met |
| POST | api/PlayerReporting/v1/instantKick |
REAL | InGameModerationController.InstantKick — moderator-only; ejects target from current room |
api/playersubscriptions/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/playersubscriptions/v1/my |
REAL | PlayerSubscriptionsController.My — returns the caller's subscription list from SubscriptionEntity |
| POST | api/playersubscriptions/v1/subscribe/{playerId} |
REAL | PlayerSubscriptionsController.Subscribe (also covered by MissingEndpointsController.Subscribe — the V1 controller's specific route wins). Inserts SubscriptionEntity. |
| POST | api/playersubscriptions/v1/unsubscribe/{playerId} |
REAL | PlayerSubscriptionsController.Unsubscribe — removes the row |
api/PlayersBanned/
| Verb | URL | Status | Handler |
|---|---|---|---|
| POST | api/PlayersBanned/v2/ban |
REAL | InGameModerationController.PlayersBannedV2Ban — persists PlayerBanEntity (room/global ban depending on payload) |
api/quickPlay/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/quickPlay/v1/getandclear |
REAL | QuickPlayController.GetAndClear — reads + drains the caller's pending quick-play invite row |
| POST | api/quickPlay/v1/set |
REAL | QuickPlayController.Set — writes a quick-play invite for the target player |
api/relationships/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/relationships/v2/get |
REAL | Relationships.V2.RelationshipsController.Get — reads the friends/blocked list from RelationshipEntity |
| GET | api/relationships/v2/personaldetails/{playerId} |
WILDCARD | No specific personaldetails route in the v2 controller. Falls into the global {*path} → {} (the namespace api/relationships/ is NOT in the wildcard list, so the global one is the only fallback). |
| POST | api/relationships/v2/addfriend |
REAL | Relationships.V2.RelationshipsController.AddFriend (POST + GET both registered) — upserts the relationship row |
| POST | api/relationships/v2/removefriend |
REAL | Relationships.V2.RelationshipsController.RemoveFriend |
| POST | api/relationships/v2/sendfriendrequest |
REAL | Relationships.V2.RelationshipsController.SendFriendRequest — fires a friend-request notification |
| POST | api/relationships/v2/acceptfriendrequest |
REAL | Relationships.V2.RelationshipsController.AcceptFriendRequest — flips the relationship to mutual |
| POST | api/relationships/v1/favorite |
REAL | Relationships.V2.RelationshipsController.Favorite — sets the IsFavorite flag |
| POST | api/relationships/v1/unfavorite |
REAL | Relationships.V2.RelationshipsController.Unfavorite |
| POST | api/relationships/v1/bulkignoreplatformusers |
STUB | MissingEndpointsController.BulkIgnore → Ack() (platform-block import is a no-op on a private server) |
api/role/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/role/developer/{accountId} |
REAL | RoleController.IsDeveloper — reads the developer-flag from the player's roles |
api/rooms/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/rooms/v1/featuredRoomGroup |
REAL | MissingEndpointsController.FeaturedRooms — returns the top-12 hot AG rooms wrapped in the {Name, FeaturedRooms} shape |
| GET | api/rooms/v3/featured |
REAL | Same handler — MissingEndpointsController.FeaturedRooms (route also registered for v3) |
| GET | api/rooms/v2/myRecent |
REAL | Rooms.V2.RoomsController.MyOtherTabs (api/rooms/v2/myrecent) — legacy route returns an empty list; the active Continue feed is rooms/visitedby/me, which hides private or limited rooms from non-owners |
| GET | api/rooms/v2/mySubscriptions |
REAL | Rooms.V2.RoomsController.MySubscribed (api/rooms/v2/mysubscribed) — rooms the caller has subscribed to |
| GET | api/rooms/v2/baserooms |
REAL | Rooms.V2.RoomsController.BaseRooms — list of "base" world templates / Rec Center rooms |
| GET | api/rooms/v1/filters |
REAL | Rooms.V2.RoomsController.Filters — discovery filter chips |
| GET | api/rooms/v2/search |
WILDCARD | No api/rooms/v2/search route — only api/rooms/v1/search is registered. Lands on the global {*path} → {}. The 2020 client probably falls back gracefully or the watch uses v1/search. STUB-via-global. |
| GET | api/rooms/v2/live |
WILDCARD | No specific route. Global {*path} → {}. The "live rooms" panel will be empty. |
| GET | api/rooms/v1/hot |
REAL | Rooms.V2.RoomsController.Hot — top rooms by HotScore |
| GET | api/rooms/v2/name/{roomId} |
REAL | Rooms.V2.RoomsController.ByName — lookup by room name (NOT id, despite the variable token) |
| GET | api/rooms/v2/{roomId} |
REAL | Rooms.V2.RoomsController.ById |
| GET | api/rooms/v4/details/{roomId} |
REAL | Rooms.V2.RoomsController.Details — full room blob with scenes/permissions/etc. |
| GET | api/rooms/v2/personaldetails/{creatorId} |
REAL | Rooms.V2.RoomsController.PersonalDetails — caller-relative bookmark/cheer flags for the room |
| POST | api/rooms/v1/clone |
REAL | Rooms.V2.RoomsController.Clone — clones the room row + scenes for the caller |
| POST | api/rooms/v1/modify/sceneParent |
REAL | RoomsModerationController.ModifyParentScene — re-parents a scene tree node |
| POST | api/rooms/v1/modify/tags |
REAL | RoomsModerationController.ModifyTags — replaces the room's tag CSV |
| POST | api/rooms/v2/modify |
REAL | RoomsModerationController.Modify — patches mutable room metadata (name, description, image) |
| POST | api/rooms/v2/modifyPermissions |
REAL | RoomsModerationController.ModifyPermissions — updates per-role permission masks |
| POST | api/rooms/v1/roombans/{roomId} |
WILDCARD | Only the GET api/rooms/v1/roombans/{roomId} is implemented; the POST falls into ApiNamespaceStubsController.List() (because api/rooms/ is intentionally NOT in the namespace wildcard list per the comment in that file — actually re-reading: api/rooms/ is excluded, so the global {*path} is what catches this. STUB-via-global.) |
| POST | api/rooms/v2/banfromroom |
REAL | RoomsModerationController.BanFromRoom — inserts RoomBanEntity |
| POST | api/rooms/v1/importroombans |
REAL | RoomsModerationController.ImportRoomBans — bulk insert |
| POST | api/rooms/v1/cheer |
REAL | MissingEndpointsController.CheerRoom — bumps Rooms.CheerCount (also PlayerStateController.CheerRoom at the per-id route api/rooms/v1/cheer/{roomId}) |
| POST | api/rooms/v1/bookmark |
REAL | MissingEndpointsController.BookmarkRoom — upserts/removes a RoomBookmarkEntity based on the Bookmark form field |
| GET | api/rooms/v1/datahistory/{roomId} |
REAL | Rooms.V2.RoomsController.DataHistory — list of historical scene snapshots |
| POST | api/rooms/v1/datahistory/restore |
REAL | Rooms.V2.RoomsController.RestoreDataHistory — rolls a room's scenes back to a snapshot |
| GET | rooms/{roomId}/subrooms |
REAL | Rooms.V2.RoomsController.SubRooms — Studio/2023 subroom list (one synthesised "Home" for single-scene rooms; RoomScenes rows otherwise) |
| GET | rooms/{roomId}/subrooms/{subRoomId} |
REAL | Rooms.V2.RoomsController.SubRoom — one subroom by RoomScene.OrderIndex |
| GET | rooms/{roomId}/subrooms/{subRoomId}/data |
REAL | Rooms.V2.RoomsController.SubRoomData — current SubRoomDataSave for the subroom |
| POST | rooms/{roomId}/subrooms/{subRoomId}/data |
REAL | Rooms.V2.RoomsController.SubRoomData — 2023 save commit. JSON body {UnityAssetId, RoomData:{Filename,Hash,OwnershipProof}, SubRoomData:{Filename,…}}; SubRoomData.Filename (the FileType=1 /upload result) becomes CurrentDataBlobName. 2020 form-urlencoded variant unchanged. See docs/recroom-2023-room-save.md |
| GET | rooms/{roomId}/subrooms/{subRoomId}/saves |
REAL | Rooms.V2.RoomsController.SubRoomSaves — current save + historical blobs, newest first |
| GET | rooms/{roomId}/subrooms/{subRoomId}/saves/{saveId} |
REAL | Rooms.V2.RoomsController.StudioSubRoomSave — Studio save metadata + baked unity_assets bundle list the 2023 client downloads |
| GET | rooms/search_rooms/{query}&skip={n}&take={n} |
REAL | Rooms.V2.RoomsController.SearchRoomsOnly — 2023 play-menu search (IBEOONPEELF.SearchRooms); paging pairs are embedded in the path segment, parsed by ParsePagedPathSegment. Also bare + roomserver/ aliases; same pattern on rooms/hot_rooms, rooms/hot_roomsandplaylists, rooms/search_roomsandplaylists |
| POST | api/rooms/v2/report |
REAL | RoomsModerationController.Report — files a room report |
| GET | api/rooms/v1/modrooms |
REAL | Rooms.V2.RoomsController.MyModerated (route alias api/rooms/v1/modrooms) — rooms the caller moderates |
| GET | api/rooms/v2/myrooms |
REAL | Rooms.V2.RoomsController.MyRooms |
| GET | api/rooms/v2/mybookmarkedrooms |
REAL | Rooms.V2.RoomsController.MyBookmarks (also aliased at api/rooms/v2/mybookmarks) |
| POST | api/rooms/v1/roomRolePermissions |
STUB | MiscStubsController.ValidateRoomRolePermissions → {success:true,error:""} (the watch's local-role assumption is what gets used; no server-side recompute) |
api/royale/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/royale/v1/current |
REAL | RoyaleController.GetCurrent — current Rec Royale season metadata + leaderboard summary |
| POST | api/royale/v2/matchcomplete |
REAL | RoyaleController.MatchComplete — records the match result row + grants rewards |
api/purchase/
| Verb | URL | Status | Handler |
|---|---|---|---|
| POST | api/purchase/v1/initiatepurchase |
REAL | PurchaseController.Initiate — creates a pending PurchaseEntity, returns the purchase id for the next step |
| POST | api/purchase/v1/completepurchase |
REAL | PurchaseController.Complete — finalises the row, grants the inventory/currency |
| POST | api/purchase/v1/processpurchase |
REAL | PurchaseController.Process — middle-step confirmation that the platform-side payment cleared (no real platform integration; just flips state) |
| POST | api/purchase/v1/cancelpurchase |
REAL | PurchaseController.Cancel — marks the pending row cancelled |
| POST | api/purchase/v1/cleanuppending |
REAL | PurchaseController.CleanupPending — sweeps dangling pending rows for the caller |
api/settings/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/settings/v2/get |
REAL | The route api/settings/v2/get doesn't exist as a literal path — the controller registers the bare GET at api/settings/v2. The 2020 client appends /get and lands on the global {*path} → {}. STUB-via-global for the literal /get suffix. (api/settings/v2 bare GET is REAL via Settings.V2.SettingsController.GetSettings reading PlayerSettingEntity rows.) |
api/storefronts/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/storefronts/v1/current |
WILDCARD | No specific route. ApiNamespaceStubsController api/storefronts/{*path} → []. The "current storefront" panel is empty. |
| POST | api/storefronts/v2/buyItem |
REAL | StorefrontsBuyController.BuyItem — debits the caller's currency, grants the item to inventory |
| POST | api/storefronts/v2/buyTier |
REAL | StorefrontsBuyController.BuyTier — purchases a season-pass tier |
| POST | api/storefronts/v2/buyElite |
REAL | StorefrontsBuyController.BuyElite — Elite (premium) purchase |
| GET | api/storefronts/v4/balance/{playerId} |
REAL | StorefrontsController.BalanceByCurrency — actually only api/storefronts/v4/balance/{currencyType:int} is registered, and api/storefronts/v4/balance (no segment) at line 388. The /balance/{playerId} shape lands on whichever of those route templates matches the int constraint. Returns the player's currency balance. |
| POST | api/storefronts/v1/balanceAddType/{type}/{playerId} |
STUB | Only the GET form is registered (AllEndpointsController.StorefrontBalanceAddType → []). The POST falls into ApiNamespaceStubsController api/storefronts/{*path} → []. Currency-add-tier purchases via the v1 path aren't credited. |
| GET | api/storefronts/v2/balance |
REAL | MissingEndpointsController.StorefrontBalanceV2 — returns a single-row balance list [{CurrencyType=2, Balance=0, Platform=0}] (genuine zero balance, but no actual ledger lookup) |
| GET | api/storefronts/v3/giftdropstore/{storefront} |
STUB | ApiNamespaceStubsController.GiftDropStorefront — returns the BaseStorefrontDTO + StoreItems=[] shape with required keys present so the strict deserialiser passes |
| GET | api/storefronts/v1/season/{seasonId} |
STUB | MissingEndpointsController.StorefrontSeason — returns {Tiers=[], Active=false, SeasonType=type} |
| GET | api/storefronts/v1/objectives |
STUB | MissingEndpointsController.StorefrontObjectives → [] |
api/sanitize/
| Verb | URL | Status | Handler |
|---|---|---|---|
| POST | api/sanitize/v1/purifyString |
STUB | AllEndpointsController.Sanitize (registered at api/sanitize/v1 and api/sanitize/v1/text) doesn't catch this exact suffix; falls into ApiNamespaceStubsController.Sanitize which echoes back {Text=text}. Functionally a passthrough — text is NOT actually filtered. |
| POST | api/sanitize/v1/requestIsStringPure |
STUB | Same — ApiNamespaceStubsController.Sanitize returns {Text=text}. The "is this string pure" boolean isn't surfaced; the client likely falls back to "treat as clean". |
api/testcasemanagement/
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET | api/testcasemanagement/v1/testpasssummary |
REAL | TestCaseManagementController.TestPassSummary — reads the test-pass summary table (dev/QA tooling) |
| GET | api/testcasemanagement/v1/testpass/{passId} |
REAL | TestCaseManagementController.GetTestPass |
| GET | api/testcasemanagement/v1/testcase/{caseId} |
REAL | TestCaseManagementController.GetTestCase |
| POST | api/testcasemanagement/v1/testcase/{caseId}/claim |
REAL | TestCaseManagementController.ClaimTestCase — assigns the case to the caller |
| POST | api/testcasemanagement/v1/testcase/{caseId}/unclaim |
REAL | TestCaseManagementController.UnclaimTestCase |
| POST | api/testcasemanagement/v1/testcase/{caseId}/status |
REAL | TestCaseManagementController.SetTestCaseStatus — pass/fail/skip update |
Non-API URLs (other subdomains)
These all live on match.rec.net (locally: match.localhost).
| Verb | URL | Status | Handler |
|---|---|---|---|
| POST | goto/room/{roomName} |
REAL | Match.GoToController.GoToRoom — looks up the room by name, picks/creates a session, returns the connect blob |
| POST | goto/room/{room}/{subRoom} |
REAL | Match.GoToController.GoToSubRoom — same but for nested rooms (Rec Center subrooms) |
| POST | goto/event/{eventId} |
REAL | Match.GoToController.GoToEvent — resolves the event's room + start time, routes the player |
| POST | goto/player/{playerId} |
REAL | Match.GoToController.GoToPlayer — uses PlayerPresenceService to find where the target is and routes the caller there |
| POST | goto/instance/{instanceId} |
REAL | Match.GoToController.GoToInstance — direct join to a specific room instance |
| POST | goto/invite/{inviteId} |
REAL | Match.GoToController.GoToInvite — resolves an invite to its target room/instance |
| POST/GET | matchmake/room/{roomId} |
REAL | Match.GoToController.MatchmakeRoom — the 2023 client's "join this public room" call (navigates by numeric id, not name). Reuses BuildResponseAsync; without it every public-room join 404s and the player bounces to dorm |
| POST/GET | matchmake/room/{roomId}/{subRoomId} |
REAL | Match.GoToController.MatchmakeSubRoom — sub-room form; resolves the RoomScenes row by OrderIndex |
| POST | matchmake/dorm |
REAL | Match.MatchController.MatchmakeDorm — builds the caller's personal-dorm RoomInstance |
| POST | matchmake/none |
REAL | Match.MatchController.MatchmakeNone — returns the caller's current room (or dorm) without changing it |
| POST | player/heartbeat |
REAL | Match.MatchPlayerController.Heartbeat — refreshes the caller's PlayerPresenceService entry; also has a duplicate in ApiNamespaceStubsController.Heartbeat for the api-host case |
CDN host (cdn.rec.net / storage / data)
Served by Controllers/Cdn/CdnController (catch-all that wins routing on every host).
| Verb | URL | Status | Handler |
|---|---|---|---|
| GET/HEAD | unity_assets/{assetId}/{target}/{version} |
REAL | Cdn.CdnController.ServeUnityAsset — Studio baked asset-bundle download. The 2023 client builds this URL itself from a save's UnityAssetId/Target/Version; resolves the owning RoomScene by StudioUnityAssetId, picks the matching {saveId}__bundle_t{target}_v{version}.assetbundle from StudioAssetBundleNamesCsv, streams it from S3. Without it imported Studio geometry never renders |
Catch-all wildcards (informational)
Routes registered as [Route("api/<ns>/{*path}")] in ApiNamespaceStubsController (Controllers/API/Stubs/ApiNamespaceStubsController.cs):
List-typed namespaces (return []):
api/announcement/{*path}api/challenge/{*path}api/checklist/{*path}api/PlayerCheer/{*path}api/catalog/{*path}api/communityboard/{*path}api/consumables/{*path}api/equipment/{*path}api/groups/{*path}api/inventions/{*path}api/Leaderboard/{*path}api/offlineinvite/{*path}api/PlayersBanned/{*path}api/objectives/{*path}api/playersubscriptions/{*path}api/storefronts/{*path}
Object-typed namespaces (return {}):
api/PlayerElo/{*path}api/quickPlay/{*path}api/royale/{*path}
Ack namespaces (return {success:true}):
api/bugreporting/{*path}api/PlayerReporting/{*path}
Sanitize passthrough (returns {Text=<echo>}):
api/sanitize/{*path}(GET + POST)
Notable namespaces NOT in the wildcard list (so unmatched URLs fall through to the global /{*path} returning 200 {}):
api/avatar/api/messages/api/notification/api/playerevents/api/relationships/api/rooms/— explicit comment in the source: "intentionally NOT in this list — RoomsController owns it"api/settings/api/testcasemanagement/api/account/,api/auth/,api/config/,api/players/,api/role/,api/version/,api/photos/,api/images/,api/gameconfigs/,api/playerReputation/,api/role/,api/cards/, etc. (these all have specific controllers covering the URLs the 2020 client actually hits)
Final-fallback catch-alls
If neither a specific route nor a namespace wildcard matches, the request hits one of these last-resort handlers:
Controllers/ApiCatchAllController.cs—[Route("/{*path}")]constrained to API hosts. Returns200 {}with a log line. Mostly a defensive net so the watch never sees a 404.Controllers/GlobalCatchAllController.cs— same shape, applies to non-API hosts.Controllers/Stub/StubController.cs—[Route("/{*path}")]last-resort across all hosts; logs the URL+verb+host so we can see what the client tried but we never wired up.
These are the "MISSING" status entries in the matrix above. The body returned (200 {}) is enough for many client call-sites that tolerate null, but tends to crash strict Util.Deserialize<T> paths the next time the player exercises that feature.