API & world integration
Base URL: https://your-host. Every route is a plain GET
with no headers or authentication, because VRChat's string and image loaders cannot send any.
Overview
There are two families of endpoints:
- World routes are what the VRChat world calls. They return a compact binary avatar list by default
(add
format=jsonto see the same data as JSON while debugging) and remember the list per client, so follow-up routes can refer to avatars by their position. - Web API routes return regular JSON for websites and tools, with cursor pagination.
Search matches substrings of the avatar name, author name and description, case-insensitively, including Unicode.
Every word needs at least 3 characters, and all words must match. A full avtr_… id returns that avatar.
World routes
| Route | Loader | Returns |
|---|---|---|
/s/{w|a|i}?o=<mask>&s=<text> | string | Search results for PC (w), Android (a) or iOS (i), up to 1000. |
/random?o=<mask>&platform=<p>&s=true | string | 100 random avatars. |
/marketplace?platform=<p>&o=<mask>&s=true | string | Marketplace avatars. |
/latest?platform=<p>&s=true | string | The most recently added avatars. |
/moreby/{index} | string | More avatars by the author of item index in your last list. |
/similar/{index}?s=true | string | Avatars with similar names to item index. |
/favorites/{token} | string | Your favourites (binary only, includes the slot count). |
/favorite/{index}/add | string | Adds item index of your last list to favourites. |
/favorite/{index}/remove | image | Removes favourite index; returns the status PNG. |
/supporter/upgrade?flags=0..15 | image | Records supporter flags (8 = VRC+, more favourite slots); returns the status PNG. |
/ping/{w|a|i} | image | Status PNG; keeps your session alive. |
platform is standalonewindows, android or ios.
The trailing s=true on some routes is accepted and ignored.
Filter mask (o)
| Bits | Meaning |
|---|---|
| 0 | Include marketplace avatars (hidden by default). |
| 1–5 | Allow content warnings: sexually suggestive, adult language, graphic violence, excessive gore, extreme horror.
With a bit clear, avatars carrying that warning are hidden, so o=0 is the safe default. |
| 6–15 | Styles: human, realistic, anime, cartoon, furry, animal, objects, fantasy, scifi, popfashion. Any selected style matches; none selected means all. |
| 16–17 | Sort: 0 relevance, 1 newest, 2 oldest. |
| 18–20 | Minimum performance on the requested platform: 0 all, 1 VeryPoor+, 2 Poor+, 3 Medium+, 4 Good+, 5 Excellent. |
Binary format (v3)
World list routes return this format, which the world's CustomParser reads. It is far cheaper for Udon
to parse than JSON. All integers are little-endian.
byte version 0x03
byte headerFlags bit 0: a slot count follows
uint16 slots only when headerFlags bit 0 is set (favourites)
repeat until end of body:
byte idKind 0x01 = 16 raw UUID bytes follow, 0x00 = XOR string follows
… id
xorString name
xorString authorName
byte flags bit 0 hasImposters, bit 1 isMarketplace
byte tagBits bit 0 content_sex, 1 content_adult, 2 content_violence, 3 content_horror, 4 content_gore
byte[3] performance PC, Android, iOS: 0 VeryPoor, 1 Poor, 2 Medium, 3 Good, 4 Excellent, 5 None
byte reserved 0
byte[2] padding 0
xorString = uint16 length, byte offset, length bytes of UTF-8 where
byte[j] ^= KEY[(j + offset) % 5], KEY = AB 7F C3 19 88
An empty result is the two header bytes 03 00.
Sessions & index routes
VRChat worlds cannot build URLs at runtime, so follow-up actions use pre-generated URLs that carry only an index,
such as /moreby/12. The server remembers the last list it sent each client and resolves the index against it.
- A session belongs to the TLS connection that fetched a list. Other connections from the same IP, such as the image loader, can use it only while it is the only live session on that IP. Players sharing an IP with someone else need their own connection to stay open.
- Sessions expire after 30 seconds without a request. Call
/ping/{w|a|i}at least every 20 seconds while the world is open. - Every list response (search, random, latest, marketplace, more-by, similar, favourites) replaces the remembered list.
- An unknown or expired session, or an index outside the list, returns
404with{"error":"Session expired, search again"}.
Favourites
The favourites link is /favorites/{token}, where the token is the reversed uppercase hex of the UTF-8 bytes of
displayName + ":" + base64(displayName) with = removed. The world generates it, the player pastes it into a
URL field once, and the world stores it with persistence.
- Loading the link links your session to your favourites. Its binary header carries your slot count (25, or 50 with VRC+).
/favorite/{i}/adduses itemiof your last list. It returns400when your slots are full./favorite/{i}/removeuses the favourite's position in the list as it was loaded. Positions don't shift after a removal.
Errors & limits
Errors are JSON: {"error":"message","status_code":404}. World routes only use these status codes:
| Status | When |
|---|---|
| 400 | Search shorter than 3 characters, invalid mask, platform, token, or full favourite slots. |
| 404 | Session expired or index out of range. /similar returns {"error":"no vector found for this avatar"} when nothing matches. |
| 429 | Rate limited (240 requests per minute per IP). A Retry-After header gives the wait in seconds. |
| 500 | Server error. |
Web API
GET /search?search=<text>&o=<mask>&limit=<1-1000>
avtr.icu-style search across all platforms. Returns a JSON array of avatars. limit defaults to 1000.
GET /api/avatars?search=&platform=&o=&limit=<1-100>&cursor=
Cursor-paginated listing: results come in relevance order when searching, newest additions first otherwise.
Pass nextCursor back as cursor to get the next page. Sort bits in o are ignored here.
{
"items": [ /* avatars */ ],
"nextCursor": "cDoxMjM0",
"hasMore": true
}
Avatar object
{
"id": "avtr_4ba6da08-c025-442a-ae91-ec4041312780",
"name": "aa",
"authorName": "-Aeternam",
"hasImposters": false,
"isMarketplace": false,
"authorId": null,
"description": null,
"tags": ["content_violence"],
"styleTags": null,
"contentWarnings": null,
"platforms": ["standalonewindows"],
"performanceRating": { "standalonewindows": "Good", "android": "None", "ios": null },
"thumbnailImageUrl": null,
"imageUrl": null,
"releaseStatus": null,
"createdAt": null,
"updatedAt": null
}
null means unknown. A performance rating of "None" means there is no build for that platform.
World routes with format=json return only id, name, authorName, hasImposters,
isMarketplace, tags (content warnings) and performanceRating.
World setup
- Bake every URL at build time.
new VRCUrl(...)only works in the editor. Generate arrays for each mask your UI can produce, plus/moreby/0…N,/similar/0…N?s=true,/favorite/0…N/addand/favorite/0…N/remove. - Search through a
VRCUrlInputFieldprefilled withhttps://your-host/s/w?o=<mask>&s=. The player types afters=, so keepsas the last parameter. - Untrusted URLs. This domain isn't on VRChat's allowlist. Players must enable Allow Untrusted URLs, and you should
show a hint when a load fails with
Not trusted url hit. - Rate limits. VRChat allows one string load and one image load every 5 seconds. Fetch a full list once and page through it in the world.
- Keep the session alive. Ping with
VRCImageDownloaderevery 10–20 seconds. - Show avatars with
VRCAvatarPedestal.SwitchAvatar(id), or setblueprintIdand callSetAvatarUse(Networking.LocalPlayer).
UdonSharp example
A minimal search panel that uses the JSON form. For large lists, parse the binary format instead.
using UdonSharp;
using UnityEngine;
using VRC.SDK3.Components;
using VRC.SDK3.Data;
using VRC.SDK3.StringLoading;
using VRC.SDKBase;
using VRC.Udon.Common.Interfaces;
[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class AvatarSearchPanel : UdonSharpBehaviour
{
// Prefill the input field's URL with: https://your-host/s/w?o=0&format=json&s=
public VRCUrlInputField searchField;
public VRCAvatarPedestal[] pedestals;
public void OnSearchSubmitted()
{
VRCStringDownloader.LoadUrl(searchField.GetUrl(), (IUdonEventReceiver)this);
}
public override void OnStringLoadSuccess(IVRCStringDownload result)
{
if (!VRCJson.TryDeserializeFromJson(result.Result, out DataToken json) || json.TokenType != TokenType.DataList)
{
return;
}
DataList avatars = json.DataList;
for (int i = 0; i < pedestals.Length; i++)
{
bool hasAvatar = i < avatars.Count;
pedestals[i].gameObject.SetActive(hasAvatar);
if (hasAvatar && avatars[i].DataDictionary.TryGetValue("id", TokenType.String, out DataToken id))
{
pedestals[i].SwitchAvatar(id.String);
}
}
}
public override void OnStringLoadError(IVRCStringDownload result)
{
Debug.LogWarning($"Avatar search failed ({result.ErrorCode}): {result.Error}");
}
}