This commit is contained in:
Khrysse 2025-06-22 11:56:30 +02:00
commit 5f102a7820
10 changed files with 635 additions and 0 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
CLIENT_NAME="YourAPINme (Dev)/1.0"
VRCHAT_API_BASE="https://api.vrchat.cloud/api/1"

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
@"
__pycache__/
*.pyc
venv/
*.log
"@ | Out-File -Encoding utf8 .gitignore
.env
data/*

112
README.md Normal file
View File

@ -0,0 +1,112 @@
<img src="./data/static/logo.png" width="300" style="margin-top: 10px;" draggable="false">
A lightweight, fast, and secure **FastAPI** proxy server for the [VRChat API](https://vrchat.community/getting-started).
Designed to handle authentication, token management, and provide cached access to public and private VRChat data endpoints.
---
## 🚀 Features
- **Seamless VRChat account authentication** with support for 2FA (email & TOTP)
- Automated token storage and expiry management (configurable cache duration)
- Public API endpoints to fetch VRChat groups & users info
- Private API endpoints secured by VRChat auth tokens
- Written in Python with FastAPI and HTTPX for async requests
- Auto environment setup with virtual environment creation & dependency installation
- Ready to deploy on any server with Python 3.8+ (tested with YunoHost)
---
## 💡 Why this project?
VRChats official API requires complex login flows and token management that can be cumbersome to implement for your apps or bots.
**K-API** simplifies this by handling authentication and caching internally, exposing easy REST endpoints for your applications.
---
## 🛠️ Getting Started
### Requirements
- Python 3.8 or higher installed globally
- Git (optional)
### Installation & Running
Clone this repository:
```bash
git clone https://git.kvs.fyi/kryscau/VRChatAPI.git
# You can use the mirror with Github with: https://github.com/kryscau/VRChatAPI.git
cd VRChatAPI
```
Run the included Python bootstrap script to create and activate the virtual environment, install dependencies, authenticate your VRChat account, and start the server:
```bash
python run.py
```
> This script will prompt for your VRChat username, password, and 2FA code if required.
> Tokens are stored securely and refreshed automatically every 30 days.
### Access the API
- Public endpoint example:
`GET /api/public/groups/{group_id}`
Returns info about a VRChat group.
- Private endpoint example (requires authentication or specific permission in VRChat [join group, bans perms, ...]):
`GET /api/private/users/{user_id}`
Returns private user data accessible with your token.
Explore interactive docs at:
`http://127.0.0.1:8000/docs`
---
## 🔒 Security & Privacy
- Your VRChat credentials and tokens are stored **locally** in JSON files inside the `data/auth/` directory.
- No credentials or tokens are ever sent to third-party servers.
- Use HTTPS and proper firewall rules when deploying publicly.
---
## 📁 Project Structure
```
/app
/api # FastAPI route modules (public/private)
/data/auth # Token storage (auto-generated)
/prelaunch # Authentication helper scripts
run.py # Python bootstrapper script
requirements.txt # Python dependencies
README.md # This file
```
---
## 🤝 Contribution
Feel free to open issues or submit pull requests.
Feature requests and bug reports are welcome!
---
## ⚡ License
MIT License © 2025 Kryscau (K-API)
---
## 🚀 Next Steps
- Add WebSocket support for real-time VRChat events
- Implement caching layers with Redis or similar
- Dockerize for easy container deployments
- Add OAuth support for multi-user API proxies
---
Made with ❤️ by [Kryscau](https://kryscau.github.io).

162
app/api/vrchat_groups.py Normal file
View File

@ -0,0 +1,162 @@
from fastapi import APIRouter, HTTPException
import httpx
import json
from app.env import CLIENT_NAME, API_BASE, TOKEN_FILE
router = APIRouter()
def load_token():
if not TOKEN_FILE.exists():
return None
with open(TOKEN_FILE, "r") as f:
return json.load(f)
@router.get("/groups/{group_id}")
async def get_groups(group_id: str):
token = load_token()
if not token:
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
auth_cookie = token.get("auth_cookie")
if not auth_cookie:
raise HTTPException(status_code=401, detail="Auth cookie missing in token")
headers = {"User-Agent": CLIENT_NAME}
cookies = {"auth": auth_cookie}
params = {
"includeRoles": "true",
"purpose": "group"
}
url = f"{API_BASE}/groups/{group_id}"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers=headers, cookies=cookies, params=params)
if r.status_code != 200:
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch group info: {r.text}")
return r.json()
@router.get("/groups/{group_id}/instances")
async def get_groups_instances(group_id: str, user_id: str):
token = load_token()
if not token:
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
auth_cookie = token.get("auth_cookie")
if not auth_cookie:
raise HTTPException(status_code=401, detail="Auth cookie missing in token")
headers = {"User-Agent": CLIENT_NAME}
cookies = {"auth": auth_cookie}
url = f"{API_BASE}/groups/{group_id}/instances"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers=headers, cookies=cookies)
if r.status_code != 200:
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch groups instances info: {r.text}")
return r.json()
@router.get("/groups/{group_id}/posts")
async def get_groups_posts(group_id: str):
token = load_token()
if not token:
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
auth_cookie = token.get("auth_cookie")
if not auth_cookie:
raise HTTPException(status_code=401, detail="Auth cookie missing in token")
headers = {"User-Agent": CLIENT_NAME}
cookies = {"auth": auth_cookie}
params = {
"n": "10",
"offset": "0",
"publicOnly": False
}
url = f"{API_BASE}/groups/{group_id}/posts"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers=headers, cookies=cookies, params=params)
if r.status_code != 200:
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch groups posts info: {r.text}")
return r.json()
@router.get("/groups/{group_id}/bans")
async def get_groups_bans(group_id: str):
token = load_token()
if not token:
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
auth_cookie = token.get("auth_cookie")
if not auth_cookie:
raise HTTPException(status_code=401, detail="Auth cookie missing in token")
headers = {"User-Agent": CLIENT_NAME}
cookies = {"auth": auth_cookie}
params = {
"n": "51",
"offset": "0"
}
url = f"{API_BASE}/groups/{group_id}/bans"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers=headers, cookies=cookies, params=params)
if r.status_code != 200:
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch groups bans info: {r.text}")
return r.json()
@router.get("/groups/{group_id}/roles")
async def get_groups_roles(group_id: str):
token = load_token()
if not token:
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
auth_cookie = token.get("auth_cookie")
if not auth_cookie:
raise HTTPException(status_code=401, detail="Auth cookie missing in token")
headers = {"User-Agent": CLIENT_NAME}
cookies = {"auth": auth_cookie}
url = f"{API_BASE}/groups/{group_id}/roles"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers=headers, cookies=cookies)
if r.status_code != 200:
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch groups roles info: {r.text}")
return r.json()
@router.get("/groups/{group_id}/members")
async def get_groups_members(group_id: str):
token = load_token()
if not token:
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
auth_cookie = token.get("auth_cookie")
if not auth_cookie:
raise HTTPException(status_code=401, detail="Auth cookie missing in token")
headers = {"User-Agent": CLIENT_NAME}
cookies = {"auth": auth_cookie}
params = {
"n": "25",
"offset": "0"
}
url = f"{API_BASE}/groups/{group_id}/members"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers=headers, cookies=cookies, params=params)
if r.status_code != 200:
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch groups members info: {r.text}")
return r.json()

108
app/api/vrchat_users.py Normal file
View File

@ -0,0 +1,108 @@
from fastapi import APIRouter, HTTPException
import httpx
import json
from app.env import CLIENT_NAME, API_BASE, TOKEN_FILE
router = APIRouter()
def load_token():
if not TOKEN_FILE.exists():
return None
with open(TOKEN_FILE, "r") as f:
return json.load(f)
@router.get("/users/{user_id}")
async def get_user(user_id: str):
token = load_token()
if not token:
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
auth_cookie = token.get("auth_cookie")
if not auth_cookie:
raise HTTPException(status_code=401, detail="Auth cookie missing in token")
headers = {"User-Agent": CLIENT_NAME}
cookies = {"auth": auth_cookie}
url = f"{API_BASE}/users/{user_id}"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers=headers, cookies=cookies)
if r.status_code != 200:
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch user info: {r.text}")
return r.json()
@router.get("/users/{user_id}/friends/status")
async def get_user_friend_status(user_id: str):
token = load_token()
if not token:
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
auth_cookie = token.get("auth_cookie")
if not auth_cookie:
raise HTTPException(status_code=401, detail="Auth cookie missing in token")
headers = {"User-Agent": CLIENT_NAME}
cookies = {"auth": auth_cookie}
url = f"{API_BASE}/users/{user_id}/friendStatus"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers=headers, cookies=cookies)
if r.status_code != 200:
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch user friend status info: {r.text}")
return r.json()
@router.get("/users/{user_id}/worlds")
async def get_user_worlds(user_id: str):
token = load_token()
if not token:
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
auth_cookie = token.get("auth_cookie")
if not auth_cookie:
raise HTTPException(status_code=401, detail="Auth cookie missing in token")
headers = {"User-Agent": CLIENT_NAME}
cookies = {"auth": auth_cookie}
params = {
"releaseStatus": "public",
"sort": "updated",
"order": "descending",
"userId": user_id,
"n": "100",
"offset": "0"
}
url = f"{API_BASE}/worlds"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers=headers, cookies=cookies, params=params)
if r.status_code != 200:
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch user worlds info: {r.text}")
return r.json()
@router.get("/users/{user_id}/groups")
async def get_user_groups(user_id: str):
token = load_token()
if not token:
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
auth_cookie = token.get("auth_cookie")
if not auth_cookie:
raise HTTPException(status_code=401, detail="Auth cookie missing in token")
headers = {"User-Agent": CLIENT_NAME}
cookies = {"auth": auth_cookie}
url = f"{API_BASE}/users/{user_id}/groups"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers=headers, cookies=cookies)
if r.status_code != 200:
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch user groups info: {r.text}")
return r.json()

8
app/env.py Normal file
View File

@ -0,0 +1,8 @@
from dotenv import load_dotenv
import os
from pathlib import Path
load_dotenv(dotenv_path=Path(__file__).parent.parent / ".env")
CLIENT_NAME = os.getenv("CLIENT_NAME", "default-client-name")
API_BASE = os.getenv("VRCHAT_API_BASE", "https://api.vrchat.cloud/api/1")
TOKEN_FILE = Path(os.getenv("TOKEN_FILE", "data/auth/account.json"))

32
app/main.py Normal file
View File

@ -0,0 +1,32 @@
from fastapi import FastAPI
from app.api.vrchat_users import router as users
from app.api.vrchat_groups import router as groups
app = FastAPI(
title="K-API",
description="""
K-API is a fast, secure, and lightweight proxy API for VRChat.
It handles authentication via VRChats official API, including 2FA support,
and provides cached endpoints for user and group information retrieval.
This project is designed for developers who want a hassle-free way
to integrate VRChat data into their apps without managing sessions or tokens manually.
Features:
- Automatic token management with 2FA handling
- Public and private VRChat data endpoints
- Response caching for performance
- Easy deployment on self-hosted servers (YunoHost compatible)
Built with FastAPI and async HTTPX for high performance and reliability.
""",
swagger_ui_parameters={"defaultModelsExpandDepth": -1},
docs_url="/docs",
redoc_url=None,
openapi_url="/openapi.json",
contact={"name": "Kryscau", "url": "https://vrchat.com/home/user/usr_323befe7-edbc-46fe-af9d-560f7e6b290c", "email": "kryscau@kvs.fyi" }
)
prefix = "/api"
app.include_router(users, prefix=prefix, tags=["Users"])
app.include_router(groups, prefix=prefix, tags=["Groups"])

View File

@ -0,0 +1,142 @@
import httpx
import base64
import json
from datetime import datetime, timedelta, timezone
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).resolve().parent.parent))
from env import CLIENT_NAME, API_BASE, TOKEN_FILE
def save_token(data):
data["created_at"] = datetime.now(timezone.utc).isoformat()
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(TOKEN_FILE, "w") as f:
json.dump(data, f, indent=4)
def load_token():
if not TOKEN_FILE.exists():
return None
with open(TOKEN_FILE, "r") as f:
data = json.load(f)
created = datetime.fromisoformat(data.get("created_at", "2000-01-01T00:00:00+00:00"))
if datetime.now(timezone.utc) - created > timedelta(days=30):
print("⚠️ Token expired. Reconnection required.")
return None
return data
def verify_auth_cookie(auth_cookie):
cookies = {"auth": auth_cookie}
headers = {"User-Agent": CLIENT_NAME}
with httpx.Client(base_url=API_BASE, cookies=cookies, headers=headers) as client:
r = client.get("/auth")
return r.status_code == 200 and r.json().get("ok", False)
def login():
print("🔐 Connecting to VRChat")
manual_username = input("Username: ")
password = input("Password: ")
creds = f"{manual_username}:{password}"
b64 = base64.b64encode(creds.encode()).decode()
auth_header = f"Basic {b64}"
headers = {
"Authorization": auth_header,
"User-Agent": CLIENT_NAME
}
with httpx.Client(base_url=API_BASE, headers=headers) as client:
# 1 - First GET call to /auth/user with Basic Auth
r = client.get("/auth/user")
if r.status_code != 200:
print("❌ Connection failed:", r.text)
return None
data = r.json()
# 2 - Check 2FA
if "requiresTwoFactorAuth" in data:
mfa_types = data["requiresTwoFactorAuth"]
print(f"🔐 2FA required: {mfa_types}")
# The Authorization header is removed for the following request
client.headers.pop("Authorization", None)
if "otp" in mfa_types:
code = input("Code 2FA (TOTP): ")
verify_endpoint = "/auth/twofactorauth/verify"
elif "emailOtp" in mfa_types:
code = input("Code 2FA (email): ")
verify_endpoint = "/auth/twofactorauth/emailotp/verify"
else:
print("❌ Unknown 2FA type:", mfa_types)
return None
r2 = client.post(verify_endpoint, json={"code": code})
if r2.status_code != 200 or not r2.json().get("verified", False):
print("❌ 2FA verification failed:", r2.text)
return None
print("✅ 2FA verified!")
# 3 - Repeat a GET /auth/user without Basic Auth to confirm the session
r3 = client.get("/auth/user")
if r3.status_code != 200:
print("❌ Failed to fetch user data after 2FA:", r3.text)
return None
data = r3.json()
# 4 - Retrieve auth cookie
auth_cookie = None
for cookie in client.cookies.jar:
if cookie.name == "auth":
auth_cookie = cookie.value
break
if not auth_cookie:
print("❌ Auth cookie not found after login.")
return None
# 5 - Check cookie
if not verify_auth_cookie(auth_cookie):
print("❌ Auth cookie invalid.")
return None
print("✅ Connected and verified.")
# 6 - Prepare the information to be backed up
display_name = data.get("displayName", manual_username)
user_id = data.get("id", "")
return {
"manual_username": manual_username,
"displayName": display_name,
"user_id": user_id,
"auth": b64,
"auth_cookie": auth_cookie
}
def get_or_create_token():
token = load_token()
if token:
print("🔑 Found saved token, verifying...")
if verify_auth_cookie(token.get("auth_cookie", "")):
print("🟢 Token already valid.")
return token
else:
print("⚠️ Saved token invalid, need to login again.")
new_token = login()
if new_token:
save_token(new_token)
return new_token
return None
if __name__ == "__main__":
token_data = get_or_create_token()
if token_data:
print("🔓 Auth ready. Token stored.")
else:
print("❌ Unable to obtain a valid token.")

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
fastapi
uvicorn[standard]
httpx
orjson
python-dotenv

56
run.py Normal file
View File

@ -0,0 +1,56 @@
import os
import sys
import subprocess
import venv
VENV_DIR = "venv"
REQ_FILE = "requirements.txt"
PORT = "8000"
# INFORMATIONS : This file does not require “venv” to run, but make sure you have Python installed globally (py run.py on Windows, for example).
# INFORMATIONS : This file does not require “venv” to run, but make sure you have Python installed globally (py run.py on Windows, for example).
# INFORMATIONS : This file does not require “venv” to run, but make sure you have Python installed globally (py run.py on Windows, for example).
# INFORMATIONS : This file does not require “venv” to run, but make sure you have Python installed globally (py run.py on Windows, for example).
# INFORMATIONS : This file does not require “venv” to run, but make sure you have Python installed globally (py run.py on Windows, for example).
def create_venv():
print("Creating virtual environment...")
venv.create(VENV_DIR, with_pip=True)
def run_in_venv(cmd):
if os.name == "nt":
# Windows
python_bin = os.path.join(VENV_DIR, "Scripts", "python.exe")
else:
# Unix
python_bin = os.path.join(VENV_DIR, "bin", "python")
full_cmd = [python_bin] + cmd
result = subprocess.run(full_cmd)
if result.returncode != 0:
print(f"Command {cmd} failed.")
sys.exit(result.returncode)
def install_requirements():
print("Installing dependencies...")
run_in_venv(["-m", "pip", "install", "--upgrade", "pip"])
run_in_venv(["-m", "pip", "install", "-r", REQ_FILE])
def main():
if not os.path.exists(VENV_DIR):
create_venv()
install_requirements()
else:
print("Virtual environment found.")
print("Running VRChat authentication script...")
run_in_venv(["app/prelaunch/vrchat_auth.py"])
print("Starting FastAPI server...")
HOST = "0.0.0.0"
RELOAD_FLAG = "--reload"
run_in_venv(["-m", "uvicorn", "app.main:app", "--host", HOST, "--port", PORT] + ([RELOAD_FLAG] if RELOAD_FLAG else []))
if __name__ == "__main__":
main()