Search Subtitles¶
Search for subtitles using various filters including title, IMDb ID, year, and more.
Endpoint¶
GET /api/subtitles/search
Authentication¶
✅ Authentication required - Include your API key in the Authorization header.
Description¶
The search endpoint is the primary way to find subtitles in the SubX database. It supports multiple search criteria that can be combined for precise results.
Query Parameters¶
At least one of query, title, imdb_id, or public_id is required.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | No* | Free-text search across title and description |
title |
string | No* | Exact or partial title match |
imdb_id |
string | No* | IMDb ID (e.g., tt0773262) |
public_id |
string | No* | Subtitle UUID |
year |
integer | No | Movie release or show premiere year (1900-2100) |
video_type |
string | No | Filter by movie or episode |
language |
string | No | Language code (currently focused on Spanish) |
limit |
integer | No | Results limit (1-200, default: 100) |
season |
integer | No | Season number (≥ 1). Only valid when video_type=episode. Returns HTTP 422 if used with any other video_type or without it. |
episode |
integer | No | Episode number (≥ 1). Only valid when video_type=episode. Returns HTTP 422 if used with any other video_type or without it. |
* At least one of these fields is required.
An explicit year filters every supplied criterion. Matching persisted years and
unknown (null) years are included; a different known year is excluded. Without an
explicit parameter, a year recognized in query applies only to the branch produced
by that query. Empty, malformed, or out-of-range values return HTTP 422. Search uses
persisted data only and never calls an external metadata provider. Responses include
nullable year, and cache keys use the fixed search:v3: prefix.
Request Examples¶
Search by Title¶
=== "cURL"
curl -X GET "https://subx-api.duckdns.org/api/subtitles/search?title=Dexter&limit=10" \
-H "Authorization: Bearer {YOUR_API_KEY_HERE}"
import requests
headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
params = {
"title": "Dexter",
"limit": 10
}
response = requests.get(
"https://subx-api.duckdns.org/api/subtitles/search",
headers=headers,
params=params
)
data = response.json()
print(f"Found {data['total']} subtitles")
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
apiKey := "{YOUR_API_KEY_HERE}"
baseURL := "https://subx-api.duckdns.org/api/subtitles/search"
params := url.Values{}
params.Add("title", "Dexter")
params.Add("limit", "10")
req, _ := http.NewRequest("GET", baseURL+"?"+params.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Printf("Found %v subtitles\n", result["total"])
}
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY_HERE}");
var url = "https://subx-api.duckdns.org/api/subtitles/search?title=Dexter&limit=10";
var response = await client.GetAsync(url);
var json = await response.Content.ReadAsStringAsync();
Console.WriteLine(json);
Search by IMDb ID¶
=== "cURL"
curl -X GET "https://subx-api.duckdns.org/api/subtitles/search?imdb_id=tt0773262&limit=20" \
-H "Authorization: Bearer {YOUR_API_KEY_HERE}"
import requests
headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
params = {
"imdb_id": "tt0773262", # Dexter
"limit": 20
}
response = requests.get(
"https://subx-api.duckdns.org/api/subtitles/search",
headers=headers,
params=params
)
data = response.json()
for subtitle in data['items']:
season = subtitle.get('season')
episode = subtitle.get('episode')
if season and episode:
print(f"S{season:02d}E{episode:02d} - {subtitle['title']}")
Search with Multiple Filters¶
=== "cURL"
curl -X GET "https://subx-api.duckdns.org/api/subtitles/search?title=Breaking%20Bad&year=2008&video_type=episode&limit=50" \
-H "Authorization: Bearer {YOUR_API_KEY_HERE}"
import requests
headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
params = {
"title": "Breaking Bad",
"year": 2008,
"video_type": "episode",
"limit": 50
}
response = requests.get(
"https://subx-api.duckdns.org/api/subtitles/search",
headers=headers,
params=params
)
Free-Text Query Search¶
The query parameter performs a broader search across titles and descriptions:
=== "cURL"
curl -X GET "https://subx-api.duckdns.org/api/subtitles/search?query=matrix%20reloaded" \
-H "Authorization: Bearer {YOUR_API_KEY_HERE}"
import requests
headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
params = {"query": "matrix reloaded"}
response = requests.get(
"https://subx-api.duckdns.org/api/subtitles/search",
headers=headers,
params=params
)
Query vs Title
- Use
queryfor broad searches across multiple fields - Use
titlefor exact or partial title matches titleis more precise,queryis more flexible :::
Response¶
Success Response (200 OK)¶
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"video_type": "episode",
"title": "Dexter",
"season": 1,
"episode": 1,
"year": 2006,
"imdb_id": "tt0773262",
"description": "Dexter S01E01 - Dexter",
"uploader_name": "user123",
"posted_at": "2024-01-15T10:30:00Z",
"downloads": 1250
},
{
"id": "660e9500-f39c-52e5-b827-557766551111",
"video_type": "episode",
"title": "Dexter",
"season": 1,
"episode": 2,
"year": null,
"imdb_id": "tt0773262",
"description": "Dexter S01E02 - Crocodile",
"uploader_name": "user456",
"posted_at": "2024-01-16T14:20:00Z",
"downloads": 980
}
],
"total": 2
}
Response Fields¶
Root Object¶
| Field | Type | Description |
|---|---|---|
items |
array | Array of subtitle objects |
total |
integer | Total number of results |
Subtitle Object¶
| Field | Type | Description |
|---|---|---|
id |
string (UUID) | Unique subtitle identifier |
video_type |
string | Type: movie or episode |
title |
string | Title of the movie/show |
season |
integer | null | Season number (for episodes) |
episode |
integer | null | Episode number (for episodes) |
year |
integer | null | Movie release year or parent show's premiere year |
imdb_id |
string | null | IMDb identifier (e.g., tt0773262) |
description |
string | null | Subtitle description/release info |
uploader_name |
string | null | Original uploader username |
posted_at |
string | ISO 8601 timestamp |
downloads |
integer | Total download count |
Status Codes¶
| Code | Description |
|---|---|
| 200 | Success - Results returned (may be empty array) |
| 400 | Bad Request - Missing or invalid parameters |
| 401 | Unauthorized - Invalid or missing API key |
| 422 | Unprocessable Entity - Invalid year, or season/episode used without video_type=episode |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal Server Error |
Error Responses¶
400 Bad Request - Missing Search Criteria¶
422 Unprocessable Entity - Invalid Year¶
{
"detail": [
{
"loc": ["query", "year"],
"msg": "ensure this value is greater than or equal to 1900",
"type": "value_error"
}
]
}
422 Unprocessable Entity - season/episode without video_type=episode¶
401 Unauthorized¶
429 Rate Limit Exceeded¶
When rate limited, the response includes headers to help you retry at the right time:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710700000
X-RateLimit-Window: 60
Retry-After: 45
Advanced Examples¶
Pagination Pattern¶
To implement pagination, use limit combined with multiple requests:
import requests
def search_all_subtitles(title, page_size=100):
"""Fetch all subtitles for a title using pagination."""
headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
all_subtitles = []
offset = 0
while True:
params = {
"title": title,
"limit": page_size
}
response = requests.get(
"https://subx-api.duckdns.org/api/subtitles/search",
headers=headers,
params=params
)
data = response.json()
items = data.get('items', [])
if not items:
break
all_subtitles.extend(items)
# If we got fewer results than the limit, we're done
if len(items) < page_size:
break
return all_subtitles
# Usage
subtitles = search_all_subtitles("Breaking Bad")
print(f"Found {len(subtitles)} total subtitles")
Filter Episodes by Season¶
Use the season (and optionally episode) parameters together with video_type=episode to filter directly at the API level:
import requests
def get_season_subtitles(imdb_id, season_number):
"""Get all subtitles for a specific season using the season filter."""
headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
params = {
"imdb_id": imdb_id,
"video_type": "episode",
"season": season_number,
"limit": 200,
}
response = requests.get(
"https://subx-api.duckdns.org/api/subtitles/search",
headers=headers,
params=params
)
data = response.json()
return data["items"]
# Get all Dexter Season 1 subtitles
season_1 = get_season_subtitles("tt0773262", 1)
print(f"Found {len(season_1)} subtitles for Season 1")
# Get a specific episode
def get_episode_subtitles(title, season_number, episode_number):
"""Get subtitles for a specific episode."""
headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
params = {
"title": title,
"video_type": "episode",
"season": season_number,
"episode": episode_number,
"limit": 50,
}
response = requests.get(
"https://subx-api.duckdns.org/api/subtitles/search",
headers=headers,
params=params
)
return response.json()["items"]
# Get Breaking Bad S02E03 subtitles
ep_subs = get_episode_subtitles("Breaking Bad", 2, 3)
print(f"Found {len(ep_subs)} subtitles for S02E03")
Constraint
season and episode are only valid when video_type=episode. Omitting
video_type=episode or setting it to movie while passing season or
episode returns HTTP 422.
Search with Retry Logic¶
import requests
import time
def search_with_retry(params, max_retries=3):
"""Search with automatic retry on failure."""
headers = {"Authorization": "Bearer {YOUR_API_KEY_HERE}"}
for attempt in range(max_retries):
try:
response = requests.get(
"https://subx-api.duckdns.org/api/subtitles/search",
headers=headers,
params=params,
timeout=10
)
if response.status_code == 429:
# Rate limited - use Retry-After header
retry_after = int(response.headers.get("Retry-After", 60 * (attempt + 1)))
print(f"Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
continue
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
if attempt == max_retries - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(60 * (attempt + 1))
return None
# Usage
results = search_with_retry({"title": "Dexter", "limit": 10})
Rate Limiting¶
The search endpoint has rate limiting to ensure fair usage. See the Rate Limits Guide for details.
Response Headers¶
All responses include rate limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed per window |
X-RateLimit-Remaining |
Requests remaining in the current window |
X-RateLimit-Reset |
Unix timestamp when the window resets |
X-RateLimit-Window |
Window duration in seconds |
Retry-After |
Seconds to wait before retrying (only on 429 responses) |
Best practices:
- Monitor X-RateLimit-Remaining to slow down before hitting the limit
- Use the Retry-After header value on 429 responses instead of hardcoded delays
- Cache search results when possible
- Use specific search criteria to reduce result sets
- Consider the limit parameter to reduce response size
Notes¶
- Search results are sorted by relevance and recency
- The
queryparameter triggers automatic reindexing jobs for new content - Empty results return
{"items": [], "total": 0}(not an error) - IMDb IDs should include the
ttprefix (e.g.,tt0773262) - Season and episode numbers start at 1 (not 0)
Next Steps¶
- Get Subtitle - Retrieve detailed subtitle information
- Download Subtitle - Download subtitle files
- Rate Limits - Understanding rate limits