---
name: emberglow-torrent-search
description: Search public movie and TV torrent indexers through https://search.emberglow.tech and get magnet links (or download links), sizes, and seeder counts back as JSON, merged across indexers and ranked. Use when you need to find a movie or TV torrent by name (for example public-domain or Creative Commons films and shows).
---

# Torrent search API

`https://search.emberglow.tech` is a read-only JSON API over
[Jackett](https://github.com/Jackett/Jackett). One request searches the
configured indexers live, merges copies of the same torrent, and ranks the
results. The indexers are chosen for **movies and TV**. No API key, no
signup, no pagination.

## Quick start

```bash
curl -s "https://search.emberglow.tech/search?q=big+buck+bunny&category=2000&limit=5"
```

```python
import httpx

resp = httpx.get(
    "https://search.emberglow.tech/search",
    params={"q": "big buck bunny", "category": 2000, "limit": 5},
    timeout=30,
)
resp.raise_for_status()
for r in resp.json()["results"]:
    # Every result has a magnet, or a download link that resolves to one.
    print(r["seeders"], r["title"], r["magnet"] or r["download"])
```

## `GET /search`

| Parameter  | Type                 | Default  | Notes |
|------------|----------------------|----------|-------|
| `q`        | string, 1–200 chars  | required | Free text. Plain words work best; there are no boolean operators or quotes. |
| `limit`    | integer, 1–500       | `100`    | How many results to return, after merging, filtering, and ranking. |
| `category` | integer, repeatable  | all      | Torznab category IDs: **2000 Movies, 5000 TV**, 3000 Audio, 6000 XXX, 8000 Other, 1000 Console, 4000 PC, 7000 Books (subcategories in the table below). `&category=2000&category=5000` means movies **or** TV. Also picks **which indexers** are searched: only those that carry the category, which makes the search faster. |
| `exclude_category` | integer, repeatable | `1000,4000,7000` | Drop results that are **only** in these categories. By default Console, PC, and Books results are left out (that's where cracked-software spam lands). Passing `category` turns the default off; `exclude_category=none` turns exclusion off. A result that's also in a category you want (Movies + PC) is kept. |
| `indexer`  | string, repeatable   | all      | Search only these indexers: `&indexer=yts&indexer=eztv` or `indexer=yts,eztv`. IDs come from `GET /indexers`. Named indexers are always searched, including ones excluded by default and ones `category` wouldn't pick. |
| `timeout`  | number, 1–60         | `6` (`20` for the stream) | Seconds to wait for indexers. The 6 s default keeps `/search` fast; `timeout=20` lifts the cap (see "Speed or completeness"), up to 60. |
| `min_seeders` | integer          | `0`      | Drop results with fewer seeders. Suspicious seed counts never qualify. |
| `exclude_suspicious` | boolean   | `false`  | Drop results with a placeholder seed count (they're ranked last either way). |
| `resolution` | string, repeatable | all     | `2160p`, `1080p`, `720p`, `480p` (parsed from the title). `resolution=2160p,1080p` keeps either. |
| `source`   | string, repeatable   | all      | `remux`, `bluray`, `web-dl`, `webrip`, `web`, `hdtv`, `dvd`, `cam`. |
| `season`   | integer              | all      | Keep this season: its full-season packs **and** its episodes. |
| `episode`  | integer              | all      | Keep this episode number. Use with `season`. |
| `resolve`  | integer, 0–50        | `10` (`20` for the stream) | How many results without a magnet get theirs looked up during the search, best-seeded first (see "Magnets"). `0` turns it off. |

### Categories

Standard Torznab ids. Pass one or more as `category`; a parent (2000) includes
all its subcategories. The tracker counts show how many of the configured
trackers (53 on 2026-10-18) carry each one. Most trackers only tag the
top level, so prefer parents; to narrow by quality, use `resolution` and
`source` instead of subcategories. `GET /indexers` shows each tracker's
categories live.

| ID | Category | Trackers | Default |
|----|----------|----------|---------|
| **2000** | **Movies** | 30 | included |
| 2010 | Movies/Foreign | 6 | |
| 2020 | Movies/Other | 8 | |
| 2030 | Movies/SD | 8 | |
| 2040 | Movies/HD | 11 | |
| 2045 | Movies/UHD | 6 | |
| 2050 | Movies/BluRay | 2 | |
| 2060 | Movies/3D | 10 | |
| 2070 | Movies/DVD | 9 | |
| **5000** | **TV** | 36 | included |
| 5010 | TV/WEB-DL | 1 | |
| 5020 | TV/Foreign | 1 | |
| 5030 | TV/SD | 7 | |
| 5040 | TV/HD | 8 | |
| 5045 | TV/UHD | 4 | |
| 5050 | TV/Other | 4 | |
| 5060 | TV/Sport | 8 | |
| 5070 | TV/Anime | 26 | |
| 5080 | TV/Documentary | 10 | |
| **3000** | **Audio** | 25 | included |
| 3010 | Audio/MP3 | 7 | |
| 3020 | Audio/Video | 11 | |
| 3030 | Audio/Audiobook | 10 | |
| 3040 | Audio/Lossless | 10 | |
| 3050 | Audio/Other | 5 | |
| 3060 | Audio/Foreign | 1 | |
| **6000** | **XXX** | 34 | included |
| 6010 | XXX/DVD | 2 | |
| 6040 | XXX/x264 | 3 | |
| 6045 | XXX/UHD | 2 | |
| 6050 | XXX/Pack | 2 | |
| 6060 | XXX/ImageSet | 8 | |
| 6070 | XXX/Other | 6 | |
| 6080 | XXX/SD | 2 | |
| 6090 | XXX/WEB-DL | 3 | |
| **8000** | **Other** | 25 | included |
| 8010 | Other/Misc | 5 | |
| **1000** | **Console** | 17 | **excluded** |
| 1010 | Console/NDS | 4 | |
| 1020 | Console/PSP | 5 | |
| 1030 | Console/Wii | 5 | |
| 1040 | Console/XBox | 5 | |
| 1050 | Console/XBox 360 | 6 | |
| 1080 | Console/PS3 | 5 | |
| 1090 | Console/Other | 4 | |
| 1110 | Console/3DS | 2 | |
| 1120 | Console/PS Vita | 1 | |
| 1130 | Console/WiiU | 1 | |
| 1180 | Console/PS4 | 5 | |
| **4000** | **PC** | 24 | **excluded** |
| 4010 | PC/0day | 8 | |
| 4020 | PC/ISO | 3 | |
| 4030 | PC/Mac | 6 | |
| 4040 | PC/Mobile-Other | 7 | |
| 4050 | PC/Games | 16 | |
| 4060 | PC/Mobile-iOS | 4 | |
| 4070 | PC/Mobile-Android | 8 | |
| **7000** | **Books** | 24 | **excluded** |
| 7010 | Books/Mags | 3 | |
| 7020 | Books/EBook | 9 | |
| 7030 | Books/Comics | 9 | |
| 7040 | Books/Technical | 1 | |
| 7050 | Books/Other | 2 | |

Excluded categories come back when you pass `category` (say `category=4000`)
or `exclude_category` (`exclude_category=none` for everything).

**Default indexers.** Without `indexer`, a search uses every configured
indexer except those sitting out after repeated failures (see "Choosing
indexers") and those **excluded by default**. Currently `1337x` is excluded:
it takes ~14 s per search and its results carry no magnets. Name it
(`indexer=1337x`) to search it anyway.

**Speed or completeness.** By default `/search` stops waiting after 6 s, which
cuts off the few slowest indexers. Pass `timeout` to wait longer:

```
# default: answers in ~6.6 s
https://search.emberglow.tech/search?q=the+godfather+1972&category=2000

# no 6 s cap: waits up to 20 s, like the stream
https://search.emberglow.tech/search?q=the+godfather+1972&category=2000&timeout=20

# the longest wait allowed
https://search.emberglow.tech/search?q=the+godfather+1972&category=2000&timeout=60
```

Measured on 52 movie and TV queries (see `/benchmarks.md`): the default
answered in a median 6.6 s with a median 331 results per query; waiting up
to 20 s took a median 14.5 s and returned 484. Every query returned results
either way. `/search/stream` gives both: results from the fast indexers within
a second, and the slow ones as they arrive, for up to 20 s (or its own
`timeout`, up to 60). `timeout` doesn't bring back indexers excluded by
default; to search one, name it (`indexer=1337x`), and only the named
indexers are searched then.

**Magnets.** Many indexers don't put magnets in their search results; the
magnet is on each torrent's own page. While a search runs, the API looks up
the magnets of the best-seeded results that lack one (`resolve` of them,
starting as soon as each indexer answers), so the top results usually arrive
with `magnet` set. The rest come with a `download` link that does the same
lookup when opened. `/search` waits at most an extra 0.5 s for lookups still
in flight; the stream sends each one as a `magnet` event.

**Merging and ranking.** The same torrent (same info hash) found by several
indexers comes back once, with every indexer listed in `trackers` and the
best trustworthy seed count. Results are then ranked: plausible seed counts
before placeholders, live torrents before dead ones, more seeders first, and
on a tie, the torrent more indexers carry.

### Response

```json
{
  "query": "big buck bunny",
  "count": 2,
  "results": [
    {
      "title": "Big.Buck.Bunny.2008.1080p.BluRay.x264-GRP",
      "tracker": "ExampleTracker",
      "trackers": ["ExampleTracker", "OtherTracker"],
      "category": "Movies/HD",
      "categories": [2000, 2040],
      "size": 928670754,
      "seeders": 412,
      "peers": 30,
      "health": "good",
      "suspicious": false,
      "release": {
        "resolution": "1080p", "source": "bluray", "codec": "x264", "hdr": false,
        "season": null, "episode": null, "year": 2008, "group": "GRP"
      },
      "published": "2026-08-29T00:00:00Z",
      "infohash": "3f9aac158c7de8dfcab171ea58a17aabdf7fbc93",
      "magnet": "magnet:?xt=urn:btih:3f9aac158c7de8dfcab171ea58a17aabdf7fbc93&dn=...",
      "download": null,
      "details": "https://tracker.example/torrent/12345"
    },
    {
      "title": "Big Buck Bunny (2008) 720p",
      "tracker": "ThirdTracker",
      "trackers": ["ThirdTracker"],
      "seeders": 57,
      "infohash": null,
      "magnet": null,
      "download": "https://search.emberglow.tech/download/WyJ0aGlyZHRyYWNrZXIiLC...",
      "...": "other fields as above"
    }
  ],
  "indexers": [
    { "id": "exampletracker", "name": "ExampleTracker", "results": 1, "error": null },
    { "id": "1337x", "name": "1337x", "results": 0, "error": "excluded by default; search it with indexer=1337x" }
  ]
}
```

| Field       | Meaning |
|-------------|---------|
| `size`      | Bytes, as an integer. Divide by 1024³ for GiB. |
| `tracker`   | The indexer whose copy is shown (the one with the best trustworthy seed count). |
| `trackers`  | Every indexer that returned this torrent. More is a good sign it's genuine. |
| `category`, `categories` | The indexer's category label, and every Torznab category id the result has (merged across trackers). |
| `seeders`   | Peers with the full file. `null` when the indexer doesn't report it. |
| `peers`     | Other peers as the indexer reports them: usually leechers, sometimes leechers plus seeders. Compare within one indexer, not across them. |
| `health`    | `good` (≥ 10 seeders), `low` (1–9), `dead` (0), or `unknown`. |
| `suspicious`| `true` when the seed count is the 32767 placeholder some indexers put on fake or spam listings. Treat with care; these rank last. |
| `release`   | Fields parsed from the title: `resolution`, `source`, `codec`, `hdr`, `season`, `episode`, `year`, `group`. Any can be `null` when the title doesn't say. |
| `published` | ISO 8601 timestamp from the indexer. |
| `infohash`  | Hex info hash, or `null` when the indexer doesn't give one. |
| `magnet`    | Magnet URI: the indexer's own, built from `infohash`, or looked up during the search (see "Magnets"). `null` when none of those worked; then use `download`. |
| `download`  | Set only when `magnet` is `null`: a link on this API that resolves the torrent through the indexer (see `GET /download/{token}`). |
| `details`   | The indexer's own page for the torrent. Useful for showing to a human. |
| `indexers`  | One entry per indexer considered. `error` is `null`, or why it has no results: an error, `timed out after …`, `skipped after …` (repeated failures), `excluded by default; …`, or `not configured`. The others' results are still included. |

## `GET /download/{token}`

Results whose magnet wasn't found during the search come with a `download`
URL instead. Opening it resolves the torrent through the indexer, on the
first request:

- **Default**: a `302` redirect to the `magnet:` URI (a browser hands it to
  the torrent client), or the `.torrent` file itself
  (`Content-Type: application/x-bittorrent`, sent as an attachment),
  depending on what the indexer offers.
- **`?format=json`**: `{"magnet": "magnet:?xt=urn:btih:..."}` either way. For
  indexers that serve `.torrent` files the magnet is built from the file
  (info hash, name, and trackers). Prefer this from code.

```bash
curl -s "https://search.emberglow.tech/download/WyJ0aGlyZHRyYWNrZXIiLC...?format=json"
```

- It costs one live request to the indexer, usually under 1 s (2–4 s for a
  few slow or Cloudflare-protected indexers). Links looked up during a recent
  search, or opened before, answer instantly.
- Tokens are signed: only links this API returned work. Anything else is a
  `404`. If the indexer can't produce the torrent any more, it's a `502`.
- It never takes more than 15 s: an indexer that hasn't produced the torrent
  by then gets a `504` ("didn't answer within 15s; try another result"), so a
  stuck indexer can't hold up whatever is waiting on the link. Move on to the
  next result rather than retrying straight away.
- Only resolve the ones you'll use; don't resolve every result up front.

## `GET /search/stream`

Same parameters as `/search`, but the answer arrives as
[server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html)
(`Content-Type: text/event-stream`) while the search runs, so you can show
results as each indexer answers instead of waiting for the slowest one. The
first event usually arrives in under a second. Because results show up as
they come, the stream waits longer by default: `timeout` defaults to **20 s**
here (6 s for `/search`).

```bash
curl -N "https://search.emberglow.tech/search/stream?q=big+buck+bunny&category=2000"
```

### Events

Each event is an `event:` line, a `data:` line holding one JSON object, and a
blank line. There are four kinds.

**`indexer`**: one per indexer, in the order they finish. Skipped, excluded,
and timed-out indexers get one too, with `error` set and no results.

```
event: indexer
data: {"id": "yts", "name": "YTS", "error": null, "seconds": 0.5, "count": 2, "results": [{...}, {...}]}
```

| Field     | Meaning |
|-----------|---------|
| `id`, `name` | The indexer. |
| `error`   | `null`, or why it has no results (same values as `indexers[].error` above). |
| `seconds` | How long it took; `null` for indexers that weren't searched. |
| `count`   | How many results this event carries. |
| `results` | This indexer's results in the `/search` result format (including `download` links), with your filters applied and ranked, but **not** merged with other indexers'. The same torrent can arrive again from another indexer. |

**`magnet`**: one per result whose magnet the stream looked up (`resolve`
of them, default 20, best-seeded first), as each lookup finishes. Match it
to a result by `download`, the result's `download` link from an `indexer`
event, and fill in the magnet.

```
event: magnet
data: {"download": "https://search.emberglow.tech/download/WyJ0b3JyZW50...", "magnet": "magnet:?xt=urn:btih:...", "infohash": "3f9a...", "error": null}
```

If the lookup failed, `magnet` and `infohash` are `null` and `error` says
why; the `download` link still works for a retry later.

**`done`**: exactly once, last. The same body `/search` returns: the merged,
filtered, ranked list cut to `limit`, plus every indexer's outcome.
**Replace** what you showed from the `indexer` events with this list. It
includes the magnets looked up so far (the stream waits up to 3 s after the
last indexer for lookups in flight), merged by info hash.

```
event: done
data: {"query": "big buck bunny", "count": 42, "results": [...], "indexers": [{"id": "yts", "name": "YTS", "results": 2, "error": null}, ...]}
```

**`error`**: instead of `done` when the search backend is unreachable. The
stream ends after it.

```
event: error
data: {"detail": "Jackett request failed: ..."}
```

Invalid parameters are rejected before the stream starts, with the same `422`
JSON body as `/search`.

### Reading the stream

Browser:

```js
const url = "https://search.emberglow.tech/search/stream?" +
  new URLSearchParams({ q: "big buck bunny", category: 2000 });
const source = new EventSource(url);
source.addEventListener("indexer", (e) => {
  const { name, results } = JSON.parse(e.data);
  showPartial(name, results);            // append as they come
});
source.addEventListener("magnet", (e) => {
  const { download, magnet } = JSON.parse(e.data);
  if (magnet) fillMagnet(download, magnet); // match the result by its download link
});
source.addEventListener("done", (e) => {
  showFinal(JSON.parse(e.data).results); // replace with the merged list
  source.close();                        // otherwise EventSource reconnects
});
source.addEventListener("error", (e) => {
  if (e.data) showError(JSON.parse(e.data).detail);
  source.close();
});
```

Python:

```python
import json
import httpx

params = {"q": "big buck bunny", "category": 2000}
with httpx.stream("GET", "https://search.emberglow.tech/search/stream", params=params, timeout=60) as resp:
    resp.raise_for_status()
    event = None
    for line in resp.iter_lines():
        if line.startswith("event: "):
            event = line[len("event: "):]
        elif line.startswith("data: "):
            data = json.loads(line[len("data: "):])
            if event == "indexer":
                print(f"{data['name']}: {data['count']} results")
            elif event == "magnet" and data["magnet"]:
                print(f"magnet for {data['download']}: {data['magnet']}")
            elif event == "done":
                final = data["results"]
            elif event == "error":
                raise RuntimeError(data["detail"])
```

### Stream or `/search`?

- **Stream** when a person is watching: first results in about a second, and
  slower indexers keep adding results for up to 20 s.
- **`/search`** for agents and repeat queries: answers within ~6 s, and it's
  cached for 10 minutes, so a repeat comes back in milliseconds. Streams are
  never cached and always run a live search.
- Both count against the same rate limit (60 requests/minute per IP); one
  stream is one request.

## Choosing indexers

`GET /indexers` lists every configured indexer with the top-level categories
it carries, its status, and how it has performed. Add `?category=5000` to see
which ones a TV search would use.

```json
{
  "count": 1,
  "indexers": [
    {
      "id": "thepiratebay",
      "name": "The Pirate Bay",
      "language": "en-US",
      "categories": [1000, 2000, 3000, 4000, 5000, 6000, 8000],
      "status": "ok",
      "failures": 0,
      "last_seconds": 0.4,
      "stats": {
        "hours": 24, "searches": 52, "success_rate": 1.0, "hit_rate": 0.83, "errors": 0,
        "timeouts": 0, "p50_seconds": 0.3, "p90_seconds": 0.6,
        "avg_results": 41.3, "last_ok": "2026-10-18T13:12:44+00:00", "last_error": null,
        "result_health": {
          "median_seeders": 12, "max_seeders": 18210, "median_peers": 3,
          "alive": 0.94, "healthy": 0.63, "magnet": 1.0, "suspicious": 0.0
        }
      }
    }
  ]
}
```

- `status`: `ok`; `skipped` (failed or timed out 3 full-length searches in a
  row, so it sits out of default searches for 10 minutes before being tried
  again); or `excluded` (left out of default searches by configuration).
  Name a `skipped` or `excluded` indexer in `indexer` to search it anyway.
- `stats`: how the indexer has done over the last `hours` (default 24, max
  168; set with `?hours=`): `searches`, `success_rate`, `hit_rate` (share of
  searches that found anything), `errors`, `timeouts`, `p50_seconds` and
  `p90_seconds` (response time of successful searches), `avg_results`,
  `last_ok`, and `last_error`. Prefer indexers with a high `success_rate` and
  low `p90_seconds`.
- `stats.result_health`: how good the torrents it returns are:
  `median_seeders`, `max_seeders`, `median_peers`, and the share of its
  results that are `alive` (≥ 1 seeder), `healthy` (≥ 10 seeders), have a
  `magnet` (otherwise they need `download`), or have `suspicious` (placeholder)
  seed counts.
- `last_seconds`: how long its last successful search took. Pick low values
  when speed matters.

## Errors

| Status | Meaning | What to do |
|--------|---------|------------|
| `422`  | Invalid parameter: missing `q`; `limit` or `timeout` out of range; malformed `indexer`, `resolution`, `source`, or `exclude_category`. The `detail` field names the problem. | Fix the request. Don't retry it unchanged. |
| `404`  | `/download/...` with a token this API didn't issue. | Use the `download` URL exactly as returned. |
| `429`  | Rate limited: 60 requests/minute per IP, burst 30. | Wait a few seconds, then retry. |
| `502`  | The search backend is down or restarting, or (for `/download`) the indexer couldn't produce the torrent. | Retry once after ~30 s, then give up and tell the user. |
| `504`  | `/download` only: the indexer didn't produce the torrent within 15 s. | Try another result; this one's indexer is stuck. |
| `200` with `count: 0` | Nothing matched, or every indexer failed. Check `indexers[].error`. | Broaden the query or drop filters. If every indexer errored, the problem is upstream, so retry later. |

## Caching and latency

- Search results are cached for up to 10 minutes (`Cache-Control: public,
  max-age=600`) at Cloudflare and at the origin. Identical URLs return the
  cached result fast. Resolved `/download` links are cached for a day.
- An uncached `/search` returns as soon as every selected indexer has
  answered or been cut off, and within `timeout` (default 6 s), plus at most
  0.5 s for magnet lookups still in flight. Each indexer
  also gets its own cutoff: about 3× its usual response time (at least 5 s),
  never more than `timeout`. One that usually answers in a second and hangs is
  dropped at 5 s. Cut-off indexers are listed in `indexers` with an error like
  `"timed out after 5s (usually answers in 0.8s)"`; their results are simply
  missing from this response. Use an HTTP timeout of at least `timeout` + 10 s.
- More complete: `timeout=20` (or up to 60), or use the stream; see "Speed or
  completeness". Faster: pass a `category`, or name a few fast `indexer`s (low
  `last_seconds` in `GET /indexers`).
- Don't add cache-busting parameters. Results don't change that fast, and each
  uncached request costs every indexer a live search.

## Tips for agents

- Search for distinctive names: title, year, release group.
  `big buck bunny 2008` finds far more than `bunny movie`.
- Looking for a movie or a show? Pass `category=2000` or `category=5000`. It
  is faster and drops unrelated results such as software with the same name.
- For TV, use `season`/`episode` instead of putting `s02e03` in `q`: the
  filter also catches titles written `2x03` or `Season 2`.
- Prefer results with many seeders. A result with 0 seeders won't download.
  `min_seeders=5` drops the dead and nearly dead.
- Prefer results several `trackers` agree on, and treat `suspicious: true`
  as probably fake.
- Sanity-check `size` against what the user expects. Outliers are often fakes
  or the wrong edition.
- Make one request per question and use `limit` to size it; there are no pages.
- Few or no results for something that should exist? Retry once with
  `timeout=20`: the slower indexers it waits for often have it.
- Hand the user the `magnet`; the top results usually have one. When it's
  `null`, resolve that result's `download` with `?format=json` and hand over
  the magnet you get back. Only resolve the results you'll actually use.
- Only use it to find content the user has the right to download.

## Other endpoints

| Endpoint              | Returns |
|-----------------------|---------|
| `GET /indexers`       | Configured indexers with categories, status, and quality stats. See "Choosing indexers". |
| `GET /stats`          | Search latency over time (`?hours=24&bucket=3600&kind=default`): p50/p90/max seconds, average results, timeouts, skips. |
| `GET /health`         | `{"status": "ok"}` when the API is up. |
| `GET /openapi.json`   | Machine-readable OpenAPI schema. |
| `GET /docs`           | Interactive API docs for humans. |
| `GET /benchmarks.md`  | Sample movie/TV queries to try, with measured latency per group. |
| `GET /llms.txt`       | Short index of this site for LLMs (also at `/llm.txt`). |
