Guide
1. Getting Started
AniRena is a torrent index focused on anime, manga, audio, and related media. You can browse and download torrents without an account. An account is required to upload torrents, post to groups, or use the API.
The navigation bar at the top provides access to the main areas of the site:
- Home — the torrent listing and search page.
- Upload — submit a new torrent (requires login).
- Guide — this page.
- Stats — site-wide statistics (torrents, peers, uploads over time).
- Groups — release groups directory.
- RSS — RSS feed of recent uploads, filterable by category.
Your account menu (top-right corner when logged in) opens a profile panel where you can adjust settings, manage security options, and access your API key.
2. Browsing & Searching
The home page lists all torrents ordered by upload date. Use the search bar at the top to filter results.
Basic search
Type any words into the search bar and press Enter (or click the search icon). Results are ranked by relevance when a query is active.
Search operators
The following operators can be combined with a regular query:
| Operator | Example | Effect |
|---|---|---|
user:"name" | user:"SubsPlease" | Show only torrents uploaded by that user. |
Clicking an uploader name in the torrent list automatically runs a user search for you.
Categories & sub-categories
Use the category selector (the grid icon next to the search bar) to restrict results to one category. Available categories are:
- Anime
- Manga/Manhwa/Comic
- Audio
- Literature
- Live Action
- Pictures
- Software
- Hentai
- Other
Each category has sub-categories (e.g. Anime to RAW, Sub/Audio, Music Video) selectable inside the category modal.
Sorting & filters
The column headers on the torrent list are clickable to sort by that column (ascending or descending). Available sort columns: date, name, size, completed downloads. Note: seeders and leechers are live counts fetched from Redis and cannot be used for sorting.
Language filter
Use the language selector (flag icon) to show only torrents tagged with a specific language.
RSS feed
The RSS feed at /rss provides the latest uploads. Append ?category=anime (or any other category slug) to filter the feed. Most torrent clients support RSS auto-downloading directly from this URL.
3. Downloading Torrents
Click any torrent name to open its detail panel. From there you can:
- Download .torrent — saves the .torrent file directly. The direct URL is
/torrents/{id}.torrent - Magnet link — opens directly in your torrent client via the magnet URI protocol. The URL is
/torrents/{id}/magnet
The detail panel also shows the torrent description, file list, tracker list, and seeder / leecher counts.
Legacy download links
Old AniRena download links are still supported and automatically redirect to the correct .torrent file using the legacy ID: /dl/{old_id}
Recommended BitTorrent clients
Any modern BitTorrent client works. The clients below are recommended and fully support BitTorrent v2 / hybrid torrents:
4. Creating an Account
Registration
Click Sign Up in the navigation bar. Choose a username, provide an email address, and set a password (minimum length enforced). You must read and accept the site terms before your account is created.
Email activation
After signing up, a verification email is sent to your address. Click the link in the email to activate your account. If you did not receive it, use the Activate your account link on the login page to request a new code.
Password recovery
If you forget your password, click Forgot password on the sign-in page and enter your email address. A recovery link will be sent to you. The link is single-use and expires after a short time.
5. Uploading Torrents
Navigate to Upload in the navigation bar. You must be logged in with an active, non-banned account. The upload page has two tabs:
Upload tab — submit an existing .torrent file
Drag-and-drop or select a .torrent file. Once loaded, fill in the fields:
| Field | Required | Description |
|---|---|---|
| Torrent file | Yes | The .torrent file to upload. |
| Name | No | Override the torrent display name. If left blank the name embedded in the torrent file is used. |
| Category | Yes | The content category (Anime, Manga, Audio, etc.). |
| Sub-category | No | A more specific type within the category (e.g. RAW, Sub/Audio). |
| Languages | No | One or more language tags describing the content language. |
| Group | No | Associate this release with a group you are a member of. |
| Description | No | Markdown-formatted description shown on the torrent detail page (max 65535 characters). |
| Private | No | Sets the private flag in the torrent, disabling DHT/PEX. Useful for tracker-only torrents. |
| Announce URL | No | Override or add the primary tracker announce URL. |
| Extra trackers | No | Read from the torrent file. Cannot be modified during upload — use the Create tab to set trackers when building a new torrent. |
| Comment | No | Override the torrent comment field embedded in the file. |
Your torrent must include at least one AniRena tracker URL in its announce list (any tier). The site checks this on upload and will reject torrents that do not include an AniRena tracker. If you created the torrent without adding the AniRena tracker first, upload it and then re-download it from the site — the downloaded file will have the correct trackers injected automatically.
Create tab — build a new torrent
The Create tab lets you generate a new .torrent from scratch by specifying file paths, tracker URLs, and other torrent parameters directly in the browser. The resulting torrent is submitted with the same metadata fields as above.
Moderation
Uploads are automatically checked against a list of banned content patterns (names, filenames, descriptions). Torrents that match a banned pattern will be rejected. Duplicate torrents (same info hash) are also rejected.
6. Your Account
Click your username in the top-right corner to open the profile panel. It is organised into collapsible sections:
Settings
Change the UI theme, font size, colour scheme, interface language, and torrent-related display preferences. Changes are saved automatically.
Password
Enter your current password and the new password twice. A verification code is sent to your registered email address and must be entered to confirm the change. If two-factor authentication is enabled, your TOTP code is also required.
Two-Factor Authentication (2FA)
Enable TOTP-based two-factor authentication using any authenticator app (e.g. Google Authenticator, Aegis, Bitwarden). When enabling 2FA:
- Scan the QR code (or enter the secret manually) in your authenticator app.
- Enter the 6-digit code shown in your app to confirm setup.
- Save the recovery codes shown — these are one-time codes to regain access if you lose your device.
To disable 2FA, enter your current TOTP code and confirm.
Active Sessions
View all currently active login sessions including browser, OS, IP address, and last-seen time. Click Revoke on any session you do not recognise. You can also revoke all sessions at once to sign out every device.
API Key
Generate a personal API key used to upload torrents programmatically via the AniRena API. Click Generate Key to create one — the full key is shown once immediately after generation. Store it securely; it will not be shown in full again. Use Revoke to permanently invalidate the key.
Delete Account
Requesting account deletion starts a 30-day grace period. Your account is disabled immediately and permanently deleted after 30 days. You can cancel the deletion at any time within that window by signing in and clicking Cancel deletion.
7. AniRena API
AniRena provides a JSON API that lets you upload torrents programmatically using a personal API key. The API applies the same rules as the web interface: ban checks, rate limits, and site-mode restrictions all apply. Every API upload is recorded in the audit log.
Authentication
The API uses a two-step authentication flow. First exchange your permanent API key for a short-lived bearer token, then pass that token in the Authorization header for every API request.
Your API key is available under Your Account > API Key. Keep it secret — anyone with the key can obtain bearer tokens and upload on your behalf. If compromised, revoke it immediately and generate a new one.
Step 1 — Get a bearer token
/api/v1/auth/tokenSend a POST request to the token endpoint with your API key in the Authorization header. No request body is required.
Authorization: ApiKey <your-api-key>
Token response
{
"token": "<bearer-token>",
"token_type": "Bearer",
"expires_in": 3600
}Token lifetime
Bearer tokens stay valid for up to 3600 seconds from when they were issued and may be reused for every call until they expire. When a token expires, mint a new one via POST /api/v1/auth/token. Every response still echoes the current token in the X-New-Token header for backward compatibility.
X-New-Token: <next-bearer-token>
Single-request login (with 2FA)
/api/v1/auth/loginAuthenticate with your username or email and password in one request and receive a bearer token directly. If your account has 2FA enabled, include your current authenticator code in totp_code (or a recovery code in recovery_code). Optionally set new_api_key to true to also mint a brand-new permanent API key in the same response.
Request body
{
"login": "username or email",
"password": "your-password",
"totp_code": "123456", // required if 2FA is enabled (6 digits)
"recovery_code": "", // alternative to totp_code
"new_api_key": false // set true to also mint a new API key
}Token response
{
"ok": true,
"token": "<bearer-token>",
"token_type": "Bearer",
"expires_in": 3600,
"api_key": "<new-api-key>" // only present when new_api_key was true
}The bearer token works exactly like one obtained from /api/v1/auth/token. The api_key field is only returned when new_api_key is true — store it immediately, as it is shown only once and replaces any previous key.
Example — log in and (optionally) get a new API key
# pip install requests import requests BASE_URL = "https://www.anirena.com" # One request: authenticate (with 2FA if enabled) and get a bearer token. # Set new_api_key=True to also receive a brand-new permanent API key. resp = requests.post( f"{BASE_URL}/api/v1/auth/login", json={ "login": "your-username", # username or email "password": "your-password", "totp_code": "123456", # omit if 2FA is not enabled "new_api_key": True, # optional }, ) resp.raise_for_status() data = resp.json() token = data["token"] # use as: Authorization: Bearer <token> if "api_key" in data: print("New API key — store it now:", data["api_key"])
// Built-in fetch — requires Node.js 18+ const BASE_URL = "https://www.anirena.com"; // One request: authenticate (with 2FA if enabled) and get a bearer token. // Set new_api_key:true to also receive a brand-new permanent API key. const resp = await fetch(`${BASE_URL}/api/v1/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ login: "your-username", // username or email password: "your-password", totp_code: "123456", // omit if 2FA is not enabled new_api_key: true, // optional }), }); const data = await resp.json(); const token = data.token; // use as: Authorization: Bearer <token> if (data.api_key) console.log("New API key — store it now:", data.api_key);
// Requires: curl extension (enabled by default in PHP 8+) <?php define("BASE_URL", "https://www.anirena.com"); // One request: authenticate (with 2FA if enabled) and get a bearer token. // Set new_api_key => true to also receive a brand-new permanent API key. $ch = curl_init(BASE_URL . "/api/v1/auth/login"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ "login" => "your-username", // username or email "password" => "your-password", "totp_code" => "123456", // omit if 2FA is not enabled "new_api_key" => true, // optional ]), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Content-Type: application/json"], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); $token = $data["token"]; // use as: Authorization: Bearer <token> if (isset($data["api_key"])) echo "New API key — store it now: " . $data["api_key"];
// Cargo.toml: // serde_json = "1" // reqwest = { version = "0.12", features = ["blocking", "json"] } const BASE_URL: &str = "https://www.anirena.com"; fn main() { let client = reqwest::blocking::Client::new(); // One request: authenticate (with 2FA if enabled) and get a bearer token. // Set new_api_key:true to also receive a brand-new permanent API key. let data: serde_json::Value = client .post(format!("{BASE_URL}/api/v1/auth/login")) .json(&serde_json::json!({ "login": "your-username", // username or email "password": "your-password", "totp_code": "123456", // omit if 2FA is not enabled "new_api_key": true, // optional })) .send().unwrap() .json().unwrap(); let token = data["token"].as_str().unwrap(); // Authorization: Bearer <token> if let Some(key) = data["api_key"].as_str() { println!("New API key — store it now: {key}"); } }
Step 2 — Upload a torrent
/api/v1/torrentsSend a plain JSON POST request with your bearer token in the Authorization header.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
torrent | string | Yes | Base64-encoded contents of the .torrent file. |
category | string | Yes | Category slug: anime, manga, audio, literature, live, pictures, software, hentai, other. |
name | string | No | Override the torrent display name. |
sub_category | string | No | Sub-category slug (e.g. raw, sub-audio). Must belong to the chosen category. |
languages | string[] | No | Array of BCP 47 language codes (e.g. en, ja). |
group_id | string | No | UUID of a group you are a member of to associate this release with. |
description | string | No | Markdown-formatted release description (max 65535 characters). |
comment | string | No | Override the torrent embedded comment field. |
is_private | boolean | No | Set to true to enable the private flag in the torrent. |
comments_enabled | boolean | No | Allow comments on this torrent. Defaults to true (enabled). |
anime_id | string | No | UUID of an anime entry to link to this torrent. Obtain the UUID via GET /api/v1/anime/search. Returns 400 if the UUID does not match any known entry. |
announce | string | No | Override or add the primary announce URL. |
trackers | string | No | Newline-separated list of additional tracker URLs. A blank line between URLs creates a new tracker tier. |
test | boolean | No | Set to true to perform a dry run: the request is fully validated but the torrent is not saved. Use this to verify your payload is correct before submitting for real. |
Dry-run success response — 200 OK
{
"ok": true,
"test": true,
"name": "My Torrent Title",
"info_hash_v1": "aabbccddeeff...",
"info_hash_v2": null
}Available language codes
abaaafaksqamarar-001anhyasavaeayazbmbaeubebnbhbibsbrbgmyyuecachcenyzhzh-HKzh-Hanszh-SGzh-TWcucvkwcocrhrcsdadvnlnl-BEdzenen-USeoeteefofjfilfifrfr-CAffgllgkadede-ATelgnguhthahehzhihohuisioigidiaieiuikgaitjajvklknkrkskkkmkirwrnkvkgkokjkukylolalvlilnltlulbmkmgmsmlmtgvmimrmhmnnanvngnendsenonbnnocorojomospipsfaplptpt-BRpaqurormrusmsgsascgdsrsr-Latnsniisdsiskslsonrsteses-419es-MXsuswsssvtltytgtatttethbotitotstntrtktwukuruguzvevivowacyfywoxhyiyozazuExample request
# pip install requests import base64, pathlib, requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://www.anirena.com" # Step 1: exchange API key for a short-lived bearer token auth = requests.post( f"{BASE_URL}/api/v1/auth/token", headers={"Authorization": f"ApiKey {API_KEY}"}, ) auth.raise_for_status() token = auth.json()["token"] # Step 2: upload — plain JSON with the bearer token torrent_b64 = base64.b64encode(pathlib.Path("file.torrent").read_bytes()).decode() resp = requests.post( f"{BASE_URL}/api/v1/torrents", json={ "torrent": torrent_b64, "category": "anime", "sub_category": "raw", "languages": ["ja"], "description": "# My Release\n\nRelease notes here.", "is_private": False, }, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() data = resp.json() token = resp.headers.get("X-New-Token", token) # save for next request print(data["id"], data["name"]) # torrent UUID and title
// Built-in modules only — requires Node.js 18+ (for global fetch) const fs = require("fs"); const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://www.anirena.com"; // Step 1: exchange API key for a short-lived bearer token let authResp = await fetch(`${BASE_URL}/api/v1/auth/token`, { method: "POST", headers: { Authorization: `ApiKey ${API_KEY}` }, }); let { token } = await authResp.json(); // Step 2: upload — plain JSON with the bearer token const torrentB64 = fs.readFileSync("file.torrent").toString("base64"); const resp = await fetch(`${BASE_URL}/api/v1/torrents`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ torrent: torrentB64, category: "anime", sub_category: "raw", languages: ["ja"], description: "# My Release\n\nRelease notes here.", is_private: false, }), }); const data = await resp.json(); token = resp.headers.get("x-new-token") ?? token; // save for next request console.log(data.id, data.name); // torrent UUID and title
// Requires: curl extension (enabled by default in PHP 8+) <?php define("API_KEY", "YOUR_API_KEY"); define("BASE_URL", "https://www.anirena.com"); // Step 1: exchange API key for a short-lived bearer token $ch = curl_init(BASE_URL . "/api/v1/auth/token"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: ApiKey " . API_KEY], ]); $token = json_decode(curl_exec($ch), true)["token"]; curl_close($ch); // Step 2: upload — plain JSON with the bearer token $torrentB64 = base64_encode(file_get_contents("file.torrent")); $respHeaders = []; $ch = curl_init(BASE_URL . "/api/v1/torrents"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ "torrent" => $torrentB64, "category" => "anime", "sub_category" => "raw", "languages" => ["ja"], "description" => "# My Release\n\nRelease notes here.", "is_private" => false, ]), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADERFUNCTION => function($ch, $h) use (&$respHeaders) { $respHeaders[] = $h; return strlen($h); }, CURLOPT_HTTPHEADER => [ "Content-Type: application/json", "Authorization: Bearer " . $token, ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); foreach ($respHeaders as $h) { // save new token for next request if (stripos($h, "X-New-Token:") === 0) $token = trim(substr($h, strlen("X-New-Token:"))); } echo $data["id"] . " " . $data["name"]; // torrent UUID and title
// Cargo.toml: // base64 = "0.22" // serde_json = "1" // reqwest = { version = "0.12", features = ["blocking", "json"] } use base64::{engine::general_purpose::STANDARD as B64, Engine}; const API_KEY: &str = "YOUR_API_KEY"; const BASE_URL: &str = "https://www.anirena.com"; fn main() { let client = reqwest::blocking::Client::new(); // Step 1: exchange API key for a short-lived bearer token let auth: serde_json::Value = client .post(format!("{BASE_URL}/api/v1/auth/token")) .header("Authorization", format!("ApiKey {API_KEY}")) .send().unwrap().json().unwrap(); let mut token = auth["token"].as_str().unwrap().to_string(); // Step 2: upload — plain JSON with the bearer token let torrent_b64 = B64.encode(std::fs::read("file.torrent").unwrap()); let resp = client .post(format!("{BASE_URL}/api/v1/torrents")) .header("Authorization", format!("Bearer {token}")) .json(&serde_json::json!({ "torrent": torrent_b64, "category": "anime", "sub_category": "raw", "languages": ["ja"], "description": "# My Release\n\nRelease notes here.", "is_private": false })) .send().unwrap(); if let Some(t) = resp.headers().get("x-new-token") { token = t.to_str().unwrap().to_string(); // save for next request } let data: serde_json::Value = resp.json().unwrap(); println!("{} {}", data["id"], data["name"]); // torrent UUID and title }
Success response — 200 OK
{
"ok": true,
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Torrent Title",
"info_hash_v1": "aabbccddeeff...",
"info_hash_v2": null
}Error responses
| HTTP status | Meaning |
|---|---|
400 | Invalid request body or missing required field. |
401 | Missing, expired, or already-rotated bearer token. Re-authenticate via POST /api/v1/auth/token. |
403 | Account banned, disabled, or IP blocked. |
409 | Duplicate torrent — same info hash already exists. |
422 | Torrent file could not be parsed or failed validation (banned pattern, invalid structure). |
429 | Rate limit exceeded. Retry after the window resets. |
503 | Site is in maintenance or read-only mode. |
Rate limiting
API uploads are subject to a configurable rate limit separate from the web interface. The limit and window are set by the site administrator. When the rate limit is exceeded the API returns 429 Too Many Requests. The limit is per API key.
Generating torrent files with torrent-builder
torrent-builder is an open-source CLI tool built on top of libtorrent-rasterbar that lets you create BitTorrent v1, v2, and hybrid .torrent files from the command line. It pairs perfectly with the AniRena upload API — generate the file locally, then POST it directly to the tracker. cantalupo555/torrent-builder.
Building from source
Requires CMake ≥ 3.28.3 and libtorrent-rasterbar ≥ 2.0.11. Clone the repository and build with CMake:
# Install system dependencies sudo apt-get install build-essential cmake libtorrent-rasterbar-dev # Clone & build git clone https://github.com/cantalupo555/torrent-builder.git cd torrent-builder mkdir build && cd build cmake .. && cmake --build .
# Install dependencies via Homebrew brew install cmake libtorrent-rasterbar # Clone & build git clone https://github.com/cantalupo555/torrent-builder.git cd torrent-builder mkdir build && cd build cmake .. && cmake --build .
Key flags
| Field | Description |
|---|---|
--path | Path to the file or directory to package (required). |
--output | Output .torrent filename (required). |
--version | BitTorrent format — 1 = v1, 2 = v2, 3 = hybrid (default: 3). |
--tracker | Add a tracker announce URL. Repeat the flag to add multiple trackers. |
--comment | Embed a metadata comment string into the torrent. |
--private | Set the private flag to restrict distribution to the listed trackers only. |
--piece-size | Piece size in KB (16–32768). Leave unset for automatic selection. |
-i | Launch step-by-step interactive configuration mode. |
End-to-end workflow: build → upload
The examples below build a hybrid torrent with torrent-builder, then authenticate with the AniRena API and upload the result in a single script.
# pip install requests import base64, subprocess, requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://www.anirena.com" # Step 1: build the torrent with torrent-builder # --version 1=v1 2=v2 3=hybrid (default) subprocess.run([ "./torrent-builder/build/torrent_builder", "--path", "/data/my_release", "--output", "my_release.torrent", "--version", "3", # hybrid "--tracker", "udp://open.tracker.gg:6969/announce", "--comment", "My Release", "--creator", "--creation-date", ], check=True) # Step 2: authenticate token = requests.post( f"{BASE_URL}/api/v1/auth/token", headers={"Authorization": f"ApiKey {API_KEY}"}, ).json()["token"] # Step 3: upload torrent_b64 = base64.b64encode(open("my_release.torrent", "rb").read()).decode() resp = requests.post( f"{BASE_URL}/api/v1/torrents", json={ "torrent": torrent_b64, "category": "anime", "sub_category": "raw", "languages": ["ja"], "comments_enabled": True, }, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() data = resp.json() print(data["id"], data["name"])
// Built-in modules — Node.js 18+ const fs = require("fs"); const { execFileSync } = require("child_process"); const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://www.anirena.com"; // Step 1: build the torrent (--version 1=v1, 2=v2, 3=hybrid) execFileSync("./torrent-builder/build/torrent_builder", [ "--path", "/data/my_release", "--output", "my_release.torrent", "--version", "3", "--tracker", "udp://open.tracker.gg:6969/announce", "--comment", "My Release", "--creator", "--creation-date", ]); // Step 2: authenticate let { token } = await (await fetch(`${BASE_URL}/api/v1/auth/token`, { method: "POST", headers: { Authorization: `ApiKey ${API_KEY}` }, })).json(); // Step 3: upload const torrentB64 = fs.readFileSync("my_release.torrent").toString("base64"); const resp = await fetch(`${BASE_URL}/api/v1/torrents`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ torrent: torrentB64, category: "anime", sub_category: "raw", languages: ["ja"], comments_enabled: true, }), }); const data = await resp.json(); console.log(data.id, data.name);
// PHP 8+ with curl and proc_open <?php define("API_KEY", "YOUR_API_KEY"); define("BASE_URL", "https://www.anirena.com"); // Step 1: build the torrent (version: 1=v1, 2=v2, 3=hybrid) exec(implode(" ", array_map("escapeshellarg", [ "./torrent-builder/build/torrent_builder", "--path", "/data/my_release", "--output", "my_release.torrent", "--version", "3", "--tracker", "udp://open.tracker.gg:6969/announce", "--comment", "My Release", "--creator", "--creation-date", ]))); // Step 2: authenticate $ch = curl_init(BASE_URL . "/api/v1/auth/token"); curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: ApiKey " . API_KEY]]); $token = json_decode(curl_exec($ch), true)["token"]; curl_close($ch); // Step 3: upload $torrentB64 = base64_encode(file_get_contents("my_release.torrent")); $respHeaders = []; $ch = curl_init(BASE_URL . "/api/v1/torrents"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ "torrent" => $torrentB64, "category" => "anime", "sub_category" => "raw", "languages" => ["ja"], "comments_enabled" => true, ]), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADERFUNCTION => function($ch, $h) use (&$respHeaders) { $respHeaders[] = $h; return strlen($h); }, CURLOPT_HTTPHEADER => ["Content-Type: application/json", "Authorization: Bearer " . $token], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); echo $data["id"] . " " . $data["name"];
// Cargo.toml: // base64 = "0.22" // serde_json = "1" // reqwest = { version = "0.12", features = ["blocking", "json"] } use base64::{engine::general_purpose::STANDARD as B64, Engine}; use std::process::Command; const API_KEY: &str = "YOUR_API_KEY"; const BASE_URL: &str = "https://www.anirena.com"; fn main() { // Step 1: build the torrent (--version 1=v1, 2=v2, 3=hybrid) Command::new("./torrent-builder/build/torrent_builder") .args([ "--path", "/data/my_release", "--output", "my_release.torrent", "--version", "3", "--tracker", "udp://open.tracker.gg:6969/announce", "--comment", "My Release", "--creator", "--creation-date", ]) .status().expect("torrent_builder failed"); let client = reqwest::blocking::Client::new(); // Step 2: authenticate let auth: serde_json::Value = client .post(format!("{BASE_URL}/api/v1/auth/token")) .header("Authorization", format!("ApiKey {API_KEY}")) .send().unwrap().json().unwrap(); let token = auth["token"].as_str().unwrap().to_string(); // Step 3: upload let torrent_b64 = B64.encode(std::fs::read("my_release.torrent").unwrap()); let resp: serde_json::Value = client .post(format!("{BASE_URL}/api/v1/torrents")) .header("Authorization", format!("Bearer {token}")) .json(&serde_json::json!({ "torrent": torrent_b64, "category": "anime", "sub_category": "raw", "languages": ["ja"], "comments_enabled": true, })) .send().unwrap().json().unwrap(); println!("{} {}", resp["id"], resp["name"]); }
Searching torrent metadata
/api/v1/torrents/searchSend a plain JSON POST request to retrieve torrent listings with the same search and filter options available on the website. The .torrent file itself is not returned — use the normal download route for that.
# pip install requests (token already obtained — see upload example) resp = requests.post( f"{BASE_URL}/api/v1/torrents/search", json={"q": "Sword Art Online", "category": "anime", "per_page": 25}, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() data = resp.json() token = resp.headers.get("X-New-Token", token) # save for next request for t in data["torrents"]: print(t["title"], "-", t["magnet"])
// token already obtained — see upload example const resp = await fetch(`${BASE_URL}/api/v1/torrents/search`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ q: "Sword Art Online", category: "anime", per_page: 25 }), }); const data = await resp.json(); token = resp.headers.get("x-new-token") ?? token; // save for next request data.torrents.forEach(t => console.log(t.title, "-", t.magnet));
// token already obtained — see upload example $respHeaders = []; $ch = curl_init(BASE_URL . "/api/v1/torrents/search"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ "q" => "Sword Art Online", "category" => "anime", "per_page" => 25 ]), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADERFUNCTION => function($ch, $h) use (&$respHeaders) { $respHeaders[] = $h; return strlen($h); }, CURLOPT_HTTPHEADER => [ "Content-Type: application/json", "Authorization: Bearer " . $token, ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); foreach ($respHeaders as $h) { // save new token for next request if (stripos($h, "X-New-Token:") === 0) $token = trim(substr($h, strlen("X-New-Token:"))); } foreach ($data["torrents"] as $t) { echo $t["title"] . " - " . $t["magnet"] . "\n"; }
// client and token already obtained — see upload example let resp = client .post(format!("{BASE_URL}/api/v1/torrents/search")) .header("Authorization", format!("Bearer {token}")) .json(&serde_json::json!({"q": "Sword Art Online", "category": "anime", "per_page": 25})) .send().unwrap(); if let Some(t) = resp.headers().get("x-new-token") { token = t.to_str().unwrap().to_string(); // save for next request } let data: serde_json::Value = resp.json().unwrap(); for t in data["torrents"].as_array().unwrap() { println!("{} - {}", t["title"], t["magnet"]); }
Search parameters
| Field | Type | Required | Description |
|---|---|---|---|
q | string | No | Free-text search. Supports group:slug, group:"Name", user:name prefixes. |
category | string | No | Category slug (e.g. "anime"). |
sub_category | string | No | Sub-category slug (e.g. "raw"). |
languages | string[] | No | Array of BCP 47 language codes (e.g. en, ja). |
sort | string | No | Sort field: date (default), size, seeders, leechers, completed, title. |
order | string | No | Sort direction: desc (default) or asc. |
page | integer | No | Page number, starting at 1 (default 1). |
per_page | integer | No | Results per page, 1–250 (default 50). |
hide_adult | boolean | No | Exclude adult-category torrents. Defaults to true for regular users. |
show_dead | boolean | No | When false (default), torrents older than the dead-torrent grace period that have no active seeders are excluded. Set to true to include them. |
Response
{
"total": 1234,
"page": 1,
"per_page": 50,
"total_pages": 25,
"from": 1,
"to": 50,
"torrents": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "My Release Title",
"info_hash_v1": "aabbccddeeff...",
"info_hash_v2": null,
"size_fmt": "1.4 GB",
"completed": 42,
"seeders": 10,
"leechers": 3,
"languages": ["ja", "en"],
"comment_count": 7,
"created_at": "2024-01-15 12:34",
"cat_slug": "anime",
"sub_slug": "raw",
"group_name": null,
"uploader": "username",
"magnet": "magnet:?xt=urn:btih:..."
}
]
}comment_count — Number of non-deleted comments on this torrent.
Search rate limiting
Search requests are subject to a separate configurable rate limit (default 60 requests per 60 seconds per API key). Exceeding the limit returns 429 Too Many Requests. Staff accounts are exempt.
Get torrent details
/api/v1/torrent/{id}Fetch the full metadata for a single torrent — including fields the search endpoint omits such as the markdown description, the embedded .torrent comment, the file list with per-file sizes, and the full tracker tier layout. Live seeder and leecher counts are read from the tracker when available.
Response
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "My Release Title",
"info_hash_v1": "aabbccddeeff...",
"info_hash_v2": null,
"size_fmt": "1.4 GB",
"completed": 42,
"seeders": 10,
"leechers": 3,
"ext_seeders": 128,
"ext_leechers": 14,
"created_at": "2024-01-15 12:34",
"torrent_created": "2024-01-15 12:30",
"created_by_client": "mktorrent 1.1",
"cat_name": "Anime",
"cat_slug": "anime",
"sub_name": "Raw",
"sub_slug": "raw",
"group_name": null,
"group_slug": null,
"uploader": "username",
"uploader_id": "...",
"description": "# My Release\n\nRelease notes here.",
"comment": "",
"is_private": false,
"magnet": "magnet:?xt=urn:btih:...",
"languages": [
{ "code": "ja", "name": "Japanese", "country_code": "jp" }
],
"tracker_tiers": [
{ "tier": 0, "urls": ["udp://tracker.example.org:6969/announce"] }
],
"files": [
{ "path": "My Release/episode-01.mkv", "size": 1503238553 }
],
"comments_enabled": true,
"comments_locked": false,
"comment_count": 7
}seeders, leechers — Live counts from the in-house tracker; both report 0 when the tracker store has no entry for this info hash or is unreachable.
ext_seeders, ext_leechers — Highest seeder and leecher counts reported by any single external tracker scraped for this torrent. Trackers tracking the same swarm overlap, so the maximum is used rather than the sum; both report 0 when no tracker has scrape data for this info hash.
Error responses
| HTTP status | Meaning |
|---|---|
400 | The torrent id must be a 36-character dashed UUID or a 32-character bare hex string. |
401 | Missing, expired, or already-rotated bearer token. Re-authenticate via POST /api/v1/auth/token. |
404 | Torrent not found. |
429 | Rate limit exceeded. Retry after the window resets. |
503 | Site is in maintenance or read-only mode. |
Fetching torrent comments
/api/v1/torrents/{id}/commentsRetrieve paginated comments for a torrent. The number of comments per page is controlled by the COMMENT_PER_PAGE setting in the server's .env file (default 20). Only torrents with comments enabled will return results — all others return 403.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number, starting at 1 (default 1). |
# pip install requests (token already obtained — see upload example) TORRENT_ID = "550e8400-e29b-41d4-a716-446655440000" resp = requests.get( f"{BASE_URL}/api/v1/torrents/{TORRENT_ID}/comments", params={"page": 1}, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() data = resp.json() token = resp.headers.get("X-New-Token", token) # save for next request for c in data["comments"]: print(c["username"], "-", c["body"])
// token already obtained — see upload example const TORRENT_ID = "550e8400-e29b-41d4-a716-446655440000"; const resp = await fetch(`${BASE_URL}/api/v1/torrents/${TORRENT_ID}/comments?page=1`, { headers: { Authorization: `Bearer ${token}` }, }); const data = await resp.json(); token = resp.headers.get("x-new-token") ?? token; // save for next request data.comments.forEach(c => console.log(c.username, "-", c.body));
// token already obtained — see upload example $torrentId = "550e8400-e29b-41d4-a716-446655440000"; $respHeaders = []; $ch = curl_init(BASE_URL . "/api/v1/torrents/{$torrentId}/comments?page=1"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADERFUNCTION => function($ch, $h) use (&$respHeaders) { $respHeaders[] = $h; return strlen($h); }, CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $token], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); foreach ($respHeaders as $h) { // save new token for next request if (stripos($h, "X-New-Token:") === 0) $token = trim(substr($h, strlen("X-New-Token:"))); } foreach ($data["comments"] as $c) { echo $c["username"] . " - " . $c["body"] . "\n"; }
// client and token already obtained — see upload example let torrent_id = "550e8400-e29b-41d4-a716-446655440000"; let resp = client .get(format!("{BASE_URL}/api/v1/torrents/{torrent_id}/comments?page=1")) .header("Authorization", format!("Bearer {token}")) .send().unwrap(); if let Some(t) = resp.headers().get("x-new-token") { token = t.to_str().unwrap().to_string(); // save for next request } let data: serde_json::Value = resp.json().unwrap(); for c in data["comments"].as_array().unwrap() { println!("{} - {}", c["username"], c["body"]); }
Response
{
"torrent_id": "550e8400-e29b-41d4-a716-446655440000",
"page": 1,
"per_page": 20,
"total": 45,
"total_pages": 3,
"comments": [
{
"id": "...",
"user_id": "...",
"username": "uploader",
"role": "user",
"author_banned": false,
"body": "Great release!",
"created_at": "2024-01-15 12:34:00",
"edited_at": null,
"edited_by_username": null,
"deleted_at": null
}
]
}The body field is an empty string when the comment author is banned or the comment has been deleted. The author_banned flag indicates which case applies.
Error responses
| HTTP status | Meaning |
|---|---|
401 | Missing, expired, or already-rotated bearer token. Re-authenticate via POST /api/v1/auth/token. |
403 | Comments are disabled for this torrent. |
404 | Torrent not found. |
503 | Site is in maintenance or read-only mode. |
Searching anime entries
/api/v1/anime/search?q=<query>Look up anime entries by title to obtain their UUID. The UUID can be passed as anime_id in the upload body to link a torrent to an anime entry at upload time, or used with PUT /api/torrents/{id}/anime after upload. No authentication is required. Subject to the same rate limit as torrent search (default 60 requests per 60 seconds per IP).
# pip install requests (no authentication required)
resp = requests.get(
f"{BASE_URL}/api/v1/anime/search",
params={"q": "Sword Art Online", "page": 1, "per_page": 10},
)
resp.raise_for_status()
for item in resp.json()["results"]:
print(item["id"], "-", item["title"])// No authentication required
const resp = await fetch(
`${BASE_URL}/api/v1/anime/search?q=${encodeURIComponent("Sword Art Online")}&page=1&per_page=10`
);
const data = await resp.json();
data.results.forEach(item => console.log(item.id, "-", item.title));// No authentication required
$ch = curl_init(BASE_URL . "/api/v1/anime/search?" . http_build_query([
"q" => "Sword Art Online", "page" => 1, "per_page" => 10,
]));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
foreach ($data["results"] as $item) {
echo $item["id"] . " - " . $item["title"] . "\n";
}// No authentication required
let data: serde_json::Value = reqwest::blocking::get(
format!("{BASE_URL}/api/v1/anime/search?q=Sword+Art+Online&page=1&per_page=10")
).unwrap().json().unwrap();
for item in data["results"].as_array().unwrap() {
println!("{} - {}", item["id"], item["title"]);
}Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Title search string (required). Matched against title and synonyms. |
page | integer | No | Page number, starting at 1 (default 1). |
per_page | integer | No | Results per page, 1–50 (default 10). |
Response
{
"total": 42,
"page": 1,
"per_page": 10,
"total_pages": 5,
"results": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Sword Art Online",
"anime_type": "TV",
"episodes": 25,
"status": "FINISHED",
"season": "FALL",
"season_year": 2012,
"picture": "https://cdn.myanimelist.net/images/anime/...",
"thumbnail": "https://cdn.myanimelist.net/images/anime/...",
"duration_secs": 1440
}
]
}Listing and retrieving groups
/api/v1/groupsReturns a paginated list of public groups (enabled and not locked). Requires Bearer token authentication.
/api/v1/groups/{id_or_slug}Returns a single public group by its numeric ID or slug. Returns 404 if the group is disabled or locked.
Query parameters (list only)
| Field | Type | Required | Description |
|---|---|---|---|
q | string | No | Filter by group name (optional, substring match). |
page | integer | No | Page number (default 1). |
per_page | integer | No | Results per page, 1–100 (default 20). |
sort | string | No | Sort column: name | slug | members | torrents | created (default name). |
order | string | No | Sort direction: asc or desc (default asc). |
Response (list)
{
"total": 12,
"page": 1,
"per_page": 20,
"total_pages": 1,
"groups": [
{
"id": 1,
"name": "SubsPlease",
"slug": "subsplease",
"subdomain_slug": "subsplease",
"description": "Weekly simulcast batches.",
"owner": "admin",
"member_count": 42,
"torrent_count": 1337,
"created_at": "2024-01-15 12:34"
}
]
}Response (single)
{
"id": 1,
"name": "SubsPlease",
"slug": "subsplease",
"subdomain_slug": "subsplease",
"description": "Weekly simulcast batches.",
"owner": "admin",
"member_count": 42,
"torrent_count": 1337,
"created_at": "2024-01-15 12:34"
}Error responses
| HTTP status | Meaning |
|---|---|
401 | Missing, expired, or already-rotated bearer token. Re-authenticate via POST /api/v1/auth/token. |
404 | Group not found or not publicly accessible. |
429 | Rate limit exceeded. Retry after the window resets. |
503 | Site is in maintenance or read-only mode. |
8. Donations
If you would like to support AniRena and help cover the costs of hosting our servers and services, you are welcome to send a donation to one of the following cryptocurrency wallets:
bc1qy2h3ddq6ak5damvnf4r5vu3ydehhxrcq8gllwn0xCbaFe03832F95F86AF2536d52710e78C63b62Cd33ucetj2XDGHQg9PVRPMxerNi7c6kX7GJkjQNg9yjwGegLbpt61yX3RjGtB1Ef8vgVz6Hr6baQsTjVkAny donation, big or small, is greatly appreciated and goes directly towards keeping AniRena running. Thank you for your support!
9. Software
AniRena Player is a free desktop app that lets you stream video directly from torrents indexed on this site — no need to wait for the full download to finish. Just paste a magnet link or open a .torrent file and playback starts as soon as enough data is available.
Both builds are fully standalone — every dependency is bundled inside the executable. No installer, no runtime to set up, just download and run.
Installer (.exe). Updates itself in-app.
Disk image (.dmg) for Apple Silicon Macs (M1 and newer). Updates itself in-app.
Portable single file, no install needed. The only Linux format with in-app auto-update.
Install: sudo apt install ./<file>.deb — updates via apt or a fresh download, not in-app.
Install: sudo dnf install ./<file>.rpm — updates via dnf or a fresh download, not in-app.
Sideload on 64-bit ARM Android devices (most modern phones / tablets). Updates by downloading a fresh APK.
Older versions
Sideload on 32-bit ARM Android devices (older phones / tablets). Updates by downloading a fresh APK.