AviSearch

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:

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

RouteLoaderReturns
/s/{w|a|i}?o=<mask>&s=<text>stringSearch results for PC (w), Android (a) or iOS (i), up to 1000.
/random?o=<mask>&platform=<p>&s=truestring100 random avatars.
/marketplace?platform=<p>&o=<mask>&s=truestringMarketplace avatars.
/latest?platform=<p>&s=truestringThe most recently added avatars.
/moreby/{index}stringMore avatars by the author of item index in your last list.
/similar/{index}?s=truestringAvatars with similar names to item index.
/favorites/{token}stringYour favourites (binary only, includes the slot count).
/favorite/{index}/addstringAdds item index of your last list to favourites.
/favorite/{index}/removeimageRemoves favourite index; returns the status PNG.
/supporter/upgrade?flags=0..15imageRecords supporter flags (8 = VRC+, more favourite slots); returns the status PNG.
/ping/{w|a|i}imageStatus 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)

BitsMeaning
0Include marketplace avatars (hidden by default).
1–5Allow 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–15Styles: human, realistic, anime, cartoon, furry, animal, objects, fantasy, scifi, popfashion. Any selected style matches; none selected means all.
16–17Sort: 0 relevance, 1 newest, 2 oldest.
18–20Minimum performance on the requested platform: 0 all, 1 VeryPoor+, 2 Poor+, 3 Medium+, 4 Good+, 5 Excellent.

Mask calculator

Mask: 0

Try it as JSON

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.

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.

The token comes from the display name alone, so anyone who knows a player's name can read or change that player's favourites. Don't store anything sensitive in them.

Errors & limits

Errors are JSON: {"error":"message","status_code":404}. World routes only use these status codes:

StatusWhen
400Search shorter than 3 characters, invalid mask, platform, token, or full favourite slots.
404Session expired or index out of range. /similar returns {"error":"no vector found for this avatar"} when nothing matches.
429Rate limited (240 requests per minute per IP). A Retry-After header gives the wait in seconds.
500Server error.
The world switches to its fallback host on any other status code. Keep a reverse proxy or CDN in front of this server from adding challenges, redirects or 5xx pages of its own.

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

  1. 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/add and /favorite/0…N/remove.
  2. Search through a VRCUrlInputField prefilled with https://your-host/s/w?o=<mask>&s=. The player types after s=, so keep s as the last parameter.
  3. 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.
  4. 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.
  5. Keep the session alive. Ping with VRCImageDownloader every 10–20 seconds.
  6. Show avatars with VRCAvatarPedestal.SwitchAvatar(id), or set blueprintId and call SetAvatarUse(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}");
    }
}