RedGIFs API getting started
Version 1.0.0. Distribution: unofficial-api-for-redgifs. Import: redgifs_api.
Requires Python 3.12+, eaf-base-api>=4.2.6, and selectolax.
Install from PyPI:
pip install unofficial-api-for-redgifs
FFmpeg is not required for direct MP4/JPG transfers;
ffprobe is used for live media validation.
import asyncio
from base_api import BaseCore
from base_api.modules.config import RuntimeConfig
from redgifs_api import Client, DownloadConfigRAW
async def main():
runtime = RuntimeConfig()
runtime.request_delay = 2
runtime.pages_concurrency = 1
runtime.videos_concurrency = 2
async with BaseCore(runtime) as core, Client(core) as client:
video = await client.get_video(
"https://www.redgifs.com/watch/hotpinkacrobatickitfox",
load_api=True, load_html=True,
)
print(video.title, video.duration, video.tags)
print(await video.get_qualities())
await video.download(DownloadConfigRAW(quality="best", path="./"))
if __name__ == "__main__":
asyncio.run(main())
Client() owns its core and closes it on context exit or close().
Client(core) leaves that core caller-owned; retain both contexts until all object
operations and streams finish. Runtime requests and downloads use the supplied backend.
An ordinary anonymous public session token is reused per client and refreshed once after
HTTP 401. API authorization headers are not copied to CDN requests or global headers.
Videos and images have independent api and html sources. API loading defaults on;
HTML loading defaults off. Display title requires HTML. Users, niches and collections
have an api source only. No account, separate Model/Channel, or Gallery class exists.
Validation is incomplete: the final 2026-11-10 run had 38 passed / 12 failed / 0 skipped, with all terminal failures caused by HTTP 429. Earlier runs also demonstrated upstream pagination omissions. Pacing is configurable, not a guarantee that quotas have reset. See validation, Client, source loading, and shared configuration.
Client — RedGIFs API
Client(core: BaseCore | None = None) constructs typed objects sharing a configured
backend. The internal _api argument is private. There is no base_url, account login,
get_model, get_channel, get_playlist, or generic get_videos method.
Use async with Client() as client or await client.close(). Only an internally created
core is closed. Injected cores remain caller-owned.
Media and collection getters
async def get_video(url: str, load_html: bool = False, load_api: bool = True) -> Video: ...
async def get_image(url: str, *, load_api: bool = True, load_html: bool = False) -> Image: ...
async def get_user(url: str, *, load_api: bool = True) -> User: ...
async def get_niche(url: str, *, load_api: bool = True) -> Niche: ...
async def get_collection(username: str, collection_id: str, *, load_api: bool = True) -> Collection: ...
Video/image getters accept a stable ID or supported watch/embed/CDN URL; getters validate
the requested object type. User and niche getters accept an identifier or the corresponding
/users/<username> or /niches/<id> URL. get_collection() takes an owner (username or
user URL) and a separate folder ID, not one arbitrary collection URL.
Setting all available source flags false creates a deferred object. Users, niches and
collections accept load_api only. See source loading.
Video search
def search(query: str = "", *, tags: list[str] | None = None,
sort: Sort | str = Sort.TRENDING, pages=1, start_page=1,
count=40, verified=False, iterator_config=None): ...
Returns ScrapeStream[Video]. Accepts trending, latest, or top. A blank query is
allowed for a tag/general listing; no client-side substring matching is imposed.
verified=True sends the site's verified filter. Verified-only trending may be empty
even when latest/top returns verified items. Do not substitute sorts silently.
Image and creator search
def search_images(query="", *, tags=None, pages=1, start_page=1,
count=40, iterator_config=None): ...
def search_users(query="", *, pages=1, start_page=1, count=40,
verified=False, iterator_config=None): ...
search_images() returns ScrapeStream[Image] with latest ordering; it has no sort or
verified argument. search_users() returns ScrapeStream[User]; it has no tag/sort
argument. Creators, studios and models use the site's User resource and flags.
Directories
def get_niches(*, pages=1, start_page=1, count=40, iterator_config=None): ...
async def get_tags(*, trending: bool = False) -> list[Tag]: ...
Niches are streamed as typed objects. Their detail loader fetches description/cover
because directory cards omit those values. Tags are an awaited in-memory list of typed
Tag(name, count) records, not ScrapeResult values.
Listing defaults
pages is a positive integer or "all"; start_page is a positive, one-based index;
count is an integer from 1 to 100. Streams fetch page 1 for live page metadata before
applying the requested range. Valid beyond-last ranges yield nothing; invalid resources
still raise. Default results are API-loaded, in original page/card order, with terminal
page/item errors raised. An explicit IteratorConfig is honored. Discovery/authentication
failures propagate directly. See iteration.
Video — RedGIFs API
Video(url=..., core=...) is a keyword-only BaseMedia dataclass. Prefer
await client.get_video(...). Construction normalizes identity without fetching;
loaders return mappings committed atomically through the shared media contract.
Fields
| Field | Type | Source | Meaning |
|---|---|---|---|
url |
str |
identity | Canonical www watch URL, or canonical profile/niche/collection URL. |
core |
BaseCore |
identity | Shared caller-configurable backend; omitted from to_dict() by default. |
video_id |
str |
identity | Stable lowercase media ID; retained on Image for interface compatibility. |
title |
str \| None |
html | HTML display title generated by the site; API-only access leaves this unloaded. |
page_description |
str \| None |
html | HTML page description, kept even after API description takes precedence. |
html_content_url |
str \| None |
html | Content URL from the matching HTML structured object; video HTML may point to silent media. |
html_publish_date |
str \| None |
html | Original HTML date string; currently malformed, not a normalized date. |
html_metadata |
dict \| None |
html | Matching SocialMediaPosting JSON-LD, including the target VideoObject/ImageObject. |
description |
str \| None |
api, html | API description when API is loaded, even when null/empty; otherwise HTML description. |
thumbnail_url |
str \| None |
api, html | Thumbnail/profile image URL supplied by the selected source. |
embed_url |
str \| None |
api, html | Embed URL from API, or HTML when API has not loaded. |
duration |
float \| None |
api | Duration in seconds; null on images. |
length |
str \| None |
api | Duration seconds as text; null if absent. |
publish_date |
str \| None |
api | UTC ISO 8601 string normalized from API Unix seconds. |
views |
int \| None |
api | Site-reported integer view count. |
likes |
int \| None |
api | Site-reported integer like count. |
tags |
list[str] \| None |
api | Site tag names; preserve returned spelling. |
width |
int \| None |
api | API-reported original width in pixels. |
height |
int \| None |
api | API-reported original height in pixels. |
has_audio |
bool \| None |
api | Site-reported flag, not a container probe; known inaccurate legacy values are preserved. |
verified |
bool \| None |
api | Site-reported uploader/profile verification flag. |
published |
bool \| None |
api | Site-reported publication flag. |
author |
str \| None |
api | Uploader username. |
author_link |
str \| None |
api | Canonical uploader /users/ URL; use get_author() for a typed User. |
poster_url |
str \| None |
api | Poster image URL. |
preview_video_url |
str \| None |
api | Optional vthumbnail clip; may appear only on listing cards. |
timeline_url |
str \| None |
api | Optional timeline image URL; may be absent from detail responses. |
content_url |
str \| None |
api | HD direct media URL, falling back to SD if HD is absent. |
sources |
dict[str, str] \| None |
api | Actual hd/sd/silent rendition labels mapped to exact URLs, including signed queries. |
available_qualities |
list[str] \| None |
api | Available rendition labels; use get_qualities() for a copied list. |
niche_ids |
list[str] \| None |
api | Related niche IDs; use get_niches() for typed details. |
sexuality |
list[str] \| None |
api | Site classification strings. |
content_type |
str \| None |
api | Site content classification, when supplied. |
average_color |
str \| None |
api | Site-provided average color string. |
gallery_id |
str \| None |
api | Optional image gallery membership ID; enumeration is not implemented. |
media_type |
int \| None |
api | 1 for Video, 2 for Image. |
hls_urls |
dict[str, str] \| None |
api | HD/SD byte-range HLS playlist URLs for inspection; not accepted by download(). |
raw_metadata |
dict \| None |
api | Original matched API record, preserving additional site fields. |
API values take precedence for description, thumbnail_url, and embed_url, including
known None values. HTML-only title, page_description, and raw structured metadata
remain available after both sources load. Zero counts remain zero; absent values are
nullable and unloaded values raise DataNotLoadedError.
has_audio is a site flag, not a file probe. Legacy happyfrenchindianspinyloach reports
false while its direct MP4 contains audible AAC. HTML publication dates are malformed;
use API publish_date for normalized UTC dates. Optional card preview/timeline URLs may
be absent from the detail endpoint. No FPS, codec, dislikes or comment count is fabricated.
Relationships and qualities
async def get_author(*, load_api: bool = True) -> User | None: ...
async def get_niches() -> list[Niche]: ...
async def get_qualities() -> list[str]: ...
async def get_download_url(quality="best", *, refresh: bool = False) -> str: ...
async def download(configuration: DownloadConfigRAW) -> bool: ...
get_author() loads author_link if necessary and returns a typed profile, or None
when the source has no author. Missing/deleted profiles retain their loading error.
get_niches() loads typed API details for all supplied niche IDs.
get_qualities() returns a copied list of hd, sd and optional silent labels.
get_download_url() accepts those exact labels plus best, half, and worst.
An absent exact rendition or numeric resolution raises ValueError. Refresh requests
fresh public detail metadata for the selected URL without replacing the model's already
committed source snapshot. Downloads always refresh and preserve signed URL queries.
download() requires DownloadConfigRAW, returns a boolean, and uses the shared raw
downloader. It copies the caller configuration before filename changes. The default
filename uses an already-loaded title, otherwise the stable ID; it does not fetch HTML
just to name a file. With no_title=True, path is the exact filename. The decorated
download entry point wraps ordinary selection/transport errors in DownloadFailed while
preserving their causes and cancellation.
See downloads, source loading, and shared media.
Image — RedGIFs API
Image inherits Video and its field/source interface. Use
await client.get_image(id_or_url, load_api=True, load_html=False).
The stable identity attribute remains video_id; media_type is 2, and duration and
length are normally null. Width/height, tags, author, source URLs, poster, publication
metadata and optional gallery_id remain available. See the field table.
HTML loading validates a matching SocialMediaPosting containing an ImageObject.
Video HTML is rejected even in HTML-only mode. Conversely, get_video() rejects images.
Use load_api=False, load_html=True, both flags true, or both false for independent,
combined or deferred loading. Inherited load_sources() supports either load order.
from redgifs_api import Client, DownloadConfigRAW
async def save_image():
async with Client() as client:
image = await client.get_image("deliciousfrontelephant", load_html=True)
print(image.video_id, image.title, image.gallery_id)
await image.download(DownloadConfigRAW(quality="hd", path="./"))
The default output extension comes from the supplied image rendition URL (observed JPG),
not .mp4. With no_title=True, the caller supplies the complete filename. Use hd,
sd or supported presets, according to image.available_qualities. There is no automatic
video conversion, gallery expansion or animated-image conversion.
client.search_images() and user.images() return image result streams. Public
collections may mix Video and Image instances while preserving listing order.
User — RedGIFs API
Use await client.get_user(username_or_user_url, load_api=True).
User is a keyword-only BaseMedia dataclass with an api loader. The public API
uses one User domain for creators, models and studios; inspect flags instead of
inventing separate resource types.
Fields
| Field | Type | Source | Meaning |
|---|---|---|---|
url |
str |
identity | Canonical www watch URL, or canonical profile/niche/collection URL. |
core |
BaseCore |
identity | Shared caller-configurable backend; omitted from to_dict() by default. |
username |
str |
identity | Canonical lowercase profile owner username. |
name |
str \| None |
api | Site display name. |
description |
str \| None |
api | Site profile/niche/collection description, or null when absent. |
thumbnail_url |
str \| None |
api | Thumbnail/profile image URL supplied by the selected source. |
creation_date |
str \| None |
api | Profile creation time as UTC ISO 8601. |
followers |
int \| None |
api | Reported follower count. |
following |
int \| None |
api | Reported following count. |
video_count |
int \| None |
api | Raw gifs count; this site can include images in that counter. |
published_count |
int \| None |
api | Raw publishedGifs count; includes images and can exceed publicly listed records. |
collection_count |
int \| None |
api | Reported publishedCollections count. |
views |
int \| None |
api | Site-reported integer view count. |
likes |
int \| None |
api | Site-reported integer like count. |
verified |
bool \| None |
api | Site-reported uploader/profile verification flag. |
studio |
bool \| None |
api | Studio/profile classification flag. |
status |
str \| None |
api | Site account status. |
profile_url |
str \| None |
api | Site profileUrl value, which may be an external social/creator URL. |
links |
list[dict] \| None |
api | Site-provided outbound-link metadata; not independently resolved. |
raw_metadata |
dict \| None |
api | Original matched API record, preserving additional site fields. |
video_count and published_count preserve the site's gifs/publishedGifs values;
those counts can include images and records missing from public tabs. profile_url
is the site's possibly external profile/social URL, while url is the canonical user
page. Full API detail is retained in raw_metadata.
Streams
def videos(*, pages=1, start_page=1, count=40, sort=ProfileSort.NEW,
tags=None, iterator_config=None) -> ScrapeStream[Video]: ...
def images(*, pages=1, start_page=1, count=40,
iterator_config=None) -> ScrapeStream[Image]: ...
def collections(*, pages=1, start_page=1, count=40,
iterator_config=None) -> ScrapeStream[Collection]: ...
videos() selects the actual video tab (type=g), accepts new/top ordering and
optional tag names. images() selects the image tab (type=i) with new ordering;
it has no tag/sort argument. collections() selects published collections for this owner.
All three are lazy shared result streams; unwrap every result and close partial streams.
async def profile_videos(client):
user = await client.get_user("nickiipeach")
async with user.videos(pages="all", count=40) as results:
return [result.unwrap() async for result in results]
A missing/unavailable profile must not become an empty successful User. Upstream equal-time ordering can repeat/omit IDs across pages, and public counts can be stale. See iteration evidence and validation status.
Niche — RedGIFs API
Use await client.get_niche(id_or_niche_url, load_api=True) or stream
client.get_niches(). Niche has an api source only. Directory cards omit some
metadata, so API loading fetches the actual niche detail endpoint even for directory items.
Fields
| Field | Type | Source | Meaning |
|---|---|---|---|
url |
str |
identity | Canonical www watch URL, or canonical profile/niche/collection URL. |
core |
BaseCore |
identity | Shared caller-configurable backend; omitted from to_dict() by default. |
niche_id |
str |
identity | Stable niche slug. |
name |
str \| None |
api | Site display name. |
description |
str \| None |
api | Site profile/niche/collection description, or null when absent. |
thumbnail_url |
str \| None |
api | Thumbnail/profile image URL supplied by the selected source. |
cover_url |
str \| None |
api | Niche cover image URL. |
video_count |
int \| None |
api | Raw niche gifs counter; can include video and image records. |
subscribers |
int \| None |
api | Reported niche subscriber count. |
tags |
list[str] \| None |
api | Site tag names; preserve returned spelling. |
preferences |
list[str] \| None |
api | Niche preference/classification strings. |
raw_metadata |
dict \| None |
api | Original matched API record, preserving additional site fields. |
url is a canonical /niches/<id> route. Niche video_count retains the raw gifs
counter and may include image records. The type exposes no owner getter or subscriber
mutation operations.
videos
def videos(*, pages=1, start_page=1, count=40, sort="new", iterator_config=None): ...
Returns ScrapeStream[Video]. Ordering is new or top; it selects the niche video
filter and has no tags, shuffle or image-tab argument. Use iteration options.
The small duo-chastity niche was reconciled at five expected/actual IDs across pages
of 2/2/1, including beyond-last behavior.
Collection — RedGIFs API
Use await client.get_collection(username, collection_id, load_api=True).
The getter takes two identity inputs. A user URL may be supplied as the owner, but a
collection URL alone is not a supported getter signature. user.collections() streams
this owner's published collections.
Fields
| Field | Type | Source | Meaning |
|---|---|---|---|
url |
str |
identity | Canonical www watch URL, or canonical profile/niche/collection URL. |
core |
BaseCore |
identity | Shared caller-configurable backend; omitted from to_dict() by default. |
username |
str |
identity | Canonical lowercase profile owner username. |
collection_id |
str |
identity | Stable folderId; supplied separately to Client.get_collection(). |
name |
str \| None |
api | Site display name. |
description |
str \| None |
api | Site profile/niche/collection description, or null when absent. |
thumbnail_url |
str \| None |
api | Thumbnail/profile image URL supplied by the selected source. |
content_count |
int \| None |
api | Stored contentCount, which may differ from public listing total. |
publish_date |
str \| None |
api | UTC ISO 8601 string normalized from API Unix seconds. |
published |
bool \| None |
api | Site-reported publication flag. |
raw_metadata |
dict \| None |
api | Original matched API record, preserving additional site fields. |
Collections have an api loader only. Stored content_count is not a promise that
all those records appear in the public item listing; the latter has its own total.
items
def items(*, pages=1, start_page=1, count=40, iterator_config=None): ...
Returns a result stream containing actual typed Video and Image instances in the
server's mixed-media order. There is no .videos() alias, sort, tag or media-type filter
on Collection. Separate boosted/promoted arrays are excluded.
from redgifs_api import Image
async def collection_items(client):
collection = await client.get_collection("weirdjack", "d7ddef5fb0")
async with collection.items(pages="all", count=100) as results:
async for result in results:
item = result.unwrap()
print("image" if isinstance(item, Image) else "video", item.video_id)
The final live run matched all 246 public records for this example in pages of
100/100/46, including two images on the last page. Stored content_count was 258;
the API does not disclose the 12 additional IDs or their absence reasons.
The backend returns HTTP 404 beyond the last collection item page. The provider first reads page 1 metadata and clamps the range, so a valid beyond-last request yields nothing while an unavailable collection still raises.
Tag — RedGIFs API
Tag is a frozen, slotted dataclass, not a BaseMedia object.
| Field | Type | Meaning |
|---|---|---|
name |
str |
Site tag spelling |
count |
int \| None |
Reported record count; nullable if absent |
await client.get_tags(trending=False) returns list[Tag] from the full directory.
trending=True selects the trending directory. Neither result is paginated or wrapped
in ScrapeResult. Tags do not have a .videos() method or source loader.
async def tagged_results(client):
tags = await client.get_tags()
selected = next(tag for tag in tags if tag.name == "Amateur")
async with client.search(tags=[selected.name], count=5) as results:
return [result.unwrap() async for result in results]
Counts are reported by the site and do not guarantee exhaustive retrievability through ranked search; search caps and availability remain server-owned.
Sorting enums — RedGIFs API
Import from redgifs_api.modules.sorting or redgifs_api.sorting.
Both enums inherit StrEnum; supported literal values are accepted by their methods.
Sort
| Member | Value | Method |
|---|---|---|
Sort.TRENDING |
trending |
Client.search() (default) |
Sort.LATEST |
latest |
Client.search() |
Sort.TOP |
top |
Client.search() |
ProfileSort
| Member | Value | Methods |
|---|---|---|
ProfileSort.NEW |
new |
User.videos(), Niche.videos() (default) |
ProfileSort.TOP |
top |
User.videos(), Niche.videos() |
latest and new are not interchangeable. Image search always uses latest;
user image streams always use new. Creator/niche directories and collection items
expose no sort argument. There are no duration, date, quality, alphabet, top7 or top28
enums. Unknown enum values raise ValueError before stream work starts.
Verified-only trending can be empty while latest/top return records; ordering is not silently changed to work around that site behavior.
Source loading — RedGIFs API
Video and Image declare independent api and html loaders through the shared
BaseMedia interface. API loading defaults on, HTML off. Users, niches and collections
have only api; requesting html on them is a loader configuration error.
| Mode | Arguments / operation | Result |
|---|---|---|
| API only | load_api=True, load_html=False |
API fields; title remains unloaded |
| HTML only | load_api=False, load_html=True |
HTML title/page fields; API-only fields remain unloaded |
| Both | load_api=True, load_html=True |
Combined fields with declared API precedence |
| Deferred | Both flags false | Identity only, no metadata request |
| API then HTML | load_sources("api"), then load_sources("html") |
Same precedence as combined loading |
| HTML then API | Reverse load order | Same final precedence |
from redgifs_api import Client
async def inspect_sources():
async with Client() as client:
video = await client.get_video("hotpinkacrobatickitfox", load_api=False)
print(video.to_dict())
await video.load_fields("duration")
assert video.loaded_sources == {"api"}
await video.load_sources("html")
print(video.title, video.page_description, video.duration)
load_fields() selects enough sources to cover unloaded requested fields; it may load
other fields from the same response. API-only fields include duration, publication date,
counts, dimensions, flags and rendition URLs. HTML-only fields include display title,
page description and raw HTML metadata. description, thumbnail_url, and embed_url
prefer API even when an API value is explicitly null. HTML-only information is retained
in its separate fields rather than overwritten.
Synchronous attribute reads never fetch. Unloaded fields raise DataNotLoadedError;
a loaded absent field is None. to_dict() omits unloaded fields by default and avoids
network requests; include_unloaded=True includes them as null, and include_core=True
includes the backend. Inspect loaded_sources, source_state(), source_errors,
is_field_loaded() and unloaded_fields() separately for loading state.
Successful sources stay committed. Failed loaders preserve their causes in shared
MediaLoadError/MediaLoadErrors; no partial mapping is applied. Loading one source
fails independently of a successful sibling. There is no provider force=True reload
argument. get_download_url(refresh=True) and downloads refresh the selected direct URL;
this does not overwrite an already committed metadata snapshot.
HTML parsing matches the requested stable ID and expected VideoObject/ImageObject.
Unrelated structured data or a challenge/directory page is not an empty successful media
object. Malformed HTML dates remain raw; normalized UTC dates come from API Unix seconds.
See Video fields and
shared media loading.
Searching and filtering — RedGIFs API
Video search supports query, tag names, trending/latest/top, verified-only filtering
and the common page options. Free text is passed as the site's query parameter; no
literal substring assertion or local filter is imposed. Multiple tags are comma-joined.
Use exact returned tag names for classification filters.
from redgifs_api import Client, sorting
async def find_videos():
async with Client() as client:
async with client.search(
query="Amateur", tags=["Amateur"], sort=sorting.Sort.TOP,
pages=2, start_page=1, count=20,
) as results:
return [result.unwrap() async for result in results]
| Entry point | Filters and ordering | Items |
|---|---|---|
Client.search() |
query, tags, sort, verified | Video |
Client.search_images() |
query, tags; latest ordering | Image |
Client.search_users() |
query, verified; no exposed sort/tags | User |
User.videos() |
tags, new/top ordering | Video |
User.images() |
new ordering; no exposed filters | Image |
Niche.videos() |
new/top ordering | Video |
Collection.items() |
Stored mixed-media order; no filters | Video or Image |
No upload-date, duration, numeric-resolution, alphabet or account/search-history filters
are exposed. Do not copy filter enums from another provider. Server ranking and caps
can limit accessible results; pages="all" means all server-reported remaining pages,
not an unrestricted full-site export. Latest rankings can change during pagination.
Verified-only trending returned zero records in investigation while verified latest/top were nonempty. The provider preserves the requested sort and genuine empty responses. HTTP 429 is a failure, not an empty search. The final paced run's search gates remain blocked by those rate limits; see validation.
Iteration and results — RedGIFs API
Search, profile tabs, niche directories/videos and public collections return lazy
ScrapeStream objects. Each item is a ScrapeResult; call .unwrap() to surface errors.
Tag directory getters instead return an awaited plain list.
| Option | Meaning |
|---|---|
pages=1 |
One page, the default |
pages=N |
Positive number of pages starting at start_page, clamped to the last page |
pages="all" |
All server-reported remaining pages |
start_page=1 |
Positive one-based requested first page |
count=40 |
Records requested per page; integer 1–100 |
iterator_config=None |
API-loaded objects, original page/card order, raised terminal page/item failures |
The stream first reads page 1 to discover pagination, even for another start page.
Valid beyond-last ranges end cleanly. Collection beyond-last endpoints return HTTP 404;
page-1 discovery distinguishes exhaustion from an unavailable resource. No result-length
heuristic drops a partial final page or invents another page for exact multiples.
Invalid booleans/nonpositive page options and invalid counts raise ValueError.
from redgifs_api import make_iterator_config
async def deferred_items(client):
user = await client.get_user("nickiipeach", load_api=False)
config = make_iterator_config(
load_specific_sources=(), order="original",
page_error_mode="raise", item_error_mode="raise",
max_page_concurrency=1, max_item_concurrency=2,
)
async with user.videos(pages="all", count=40, iterator_config=config) as results:
async for result in results:
video = result.unwrap()
print(video.video_id, video.loaded_sources)
Listing video/image cards supply the API loader's seed data, avoiding a detail request
per item. Deferred iteration does not commit those metadata fields until explicitly
loaded. Niche detail loading makes its own API request for omitted directory fields.
An explicit IteratorConfig controls sources/fields, ordering, concurrency and terminal
failure policy. For example, load_specific_sources=(), load_specific_fields=("title",)
loads HTML titles. The default is API rather than another provider's HTML source.
The shared Helper handles item/page work. Discovery/authentication failures raise
directly; subsequent failures follow iterator policy. Use async with or
await stream.aclose() when breaking early to cancel pending work. Injected cores must
remain open until all streams and object operations finish. Completion ordering may be
requested explicitly; default ordering preserves the returned page/card sequence.
Separate promoted/boosted arrays are excluded. There is no automatic deduplication, pinned-card insertion or missing-record manufacture. Server counts do not establish completeness by themselves.
Reconciled boundaries and upstream limits
| Example | Expected / actual public IDs | Evidence |
|---|---|---|
| Collection weirdjack/d7ddef5fb0 | 246 / 246 | Final run; 100/100/46, including two images |
| Niche duo-chastity | 5 / 5 | Final run; 2/2/1 |
| User nickiipeach videos | 17 / 17 | Earlier run; six per page, partial final page; plus one image |
| User rubycammilf videos | 4 / 4 | Earlier run; two per page, exact multiple; plus six images |
| User naughtygardengirl videos | 76 / 76 | Earlier run; plus one image versus stored published count 78 |
| User dainty-blossomx2, count=40 | 10 / 10 | Earlier single-page comparison |
| Same user, count=5 or 6 | 10 slots / 9 unique IDs | Earlier failing completeness gates; omitted/repeated IDs |
All ten dainty-blossomx2 uploads share timestamp 1782281354. Equal-time ordering differs
at page boundaries: count 6 repeated activetiredaustralianfreshwatercrocodile and omitted
icyhappyhamadryad; count 5 did the reverse during diagnosis. These cases remain failing
completeness assertions. A single page of 40 is an observed workaround for this small
profile, not a general guarantee. Quotas block final profile revalidation; fixing rate
limits does not resolve the independently observed unstable ordering.
Downloading — RedGIFs API
Use DownloadConfigRAW with Video.download() or Image.download().
Every transfer uses the supplied BaseCore.legacy_download(); no second downloader is
implemented. Direct MP4/JPG downloading does not need FFmpeg.
from pathlib import Path
from redgifs_api import Client, DownloadConfigRAW
async def download_video():
output = Path("./downloads")
output.mkdir(parents=True, exist_ok=True)
async with Client() as client:
video = await client.get_video("hotpinkacrobatickitfox")
config = DownloadConfigRAW(
quality="sd", path=output / "sample.mp4", no_title=True,
allow_multipart=True, max_workers=3,
callback=lambda done, total: print(done, total),
)
assert await video.download(config)
Rendition labels
| Selector | Behavior |
|---|---|
hd |
Exact HD rendition; missing label raises |
sd |
Exact SD rendition; missing label raises |
silent |
Exact silent rendition, when supplied; never a fallback from another selector |
best |
HD, falling back to SD |
worst |
SD, falling back to HD |
half |
Lower middle of the available SD/HD labels; SD when both exist |
The provider rejects numeric resolution requests and strings such as 1080p; site
labels do not guarantee one fixed resolution. Use await video.get_qualities() to
inspect actual labels. Absent exact labels raise ValueError from get_download_url();
ordinary download failures, including selection failures, are wrapped in DownloadFailed.
Paths, refresh and configuration
Default path is an output directory. An already-loaded HTML title names the MP4;
otherwise the stable ID does. No implicit HTML request is made for naming. Image
extensions follow their actual source URL. no_title=True makes path the exact filename.
The configuration is copied before path changes; callbacks and stop events retain their
caller identity. Do not reuse the same output path for concurrent downloads.
Downloads refresh public detail URLs on every call, retaining CDN casing and signed
queries. get_download_url(refresh=True) refreshes a URL without replacing committed
source metadata. RAW configuration supports multipart workers, retries, chunk size,
read timeout, resume-state path and cancellation. RAW returns bool, not an HLS report.
The provider does not implement remux, metadata-tag writing or thumbnail embedding.
Verified complete output
The final paced run completed 23 MP4 matrix downloads covering four videos and all exposed labels plus best/half/worst, two JPG renditions, concurrent different-video transfers and a byte-for-byte repeat, and two CLI downloads. ffprobe checked complete files, dimensions, duration and audio.
| Sample | HD | SD | Silent | Duration |
|---|---|---|---|---|
| hotpinkacrobatickitfox | 1080×1920, audio | 480×854, audio | 1080×1920, no audio | 6.467 s |
| ivoryhauntingblackbird | 1080×1920, audio | 480×854, audio | 1080×1920, no audio | 7.767 s |
| toughlongtermbanteng | 1080×1920, audio | 480×854, audio | 1080×1920, no audio | 31.400 s |
| happyfrenchindianspinyloach | 1280×640, audio | 640×320, audio | Unavailable | 15.000 s |
| Image deliciousfrontelephant | 1200×1600 JPG | 800×1067 JPG | Unavailable | Still image |
The legacy video's API hasAudio=false disagrees with its audible AAC track;
has_audio preserves that reported value and is not a container probe.
HLS boundary
video.hls_urls exposes actual HD/SD playlist URLs when the site reports HLS.
Observed playlists use EXT-X-MAP and byte ranges into fMP4 files. The shared HLS
implementation drops those byte ranges; the provider does not claim a working HLS
transfer and rejects DownloadConfigHLS. Direct MP4 renditions provide verified
complete transfers. See validation.
Live validation — RedGIFs API
Commands below run from the unofficial-api-for-redgifs checkout, not the Server repository.
Run date: 2026-11-10. Linux; CPython 3.15.2; current local eaf-base-api 4.2.6.
Live fixtures reuse independent production/reference sessions with a two-second
request delay per core and a two-second initial bounded retry delay. All tests use real
RedGIFs requests. No mocked transport, recorded responses, account
cookies, skipped tests, or downloaded media are committed.
Status: incomplete because of rate limits and upstream pagination
Exact command (from the repository root):
.venv/bin/python -m pytest -q -s --tb=short
12 failed, 38 passed, 104 warnings in 363.26s (0:06:03). Exit status 1; zero skipped tests. All 12 terminal failures are
RequestRetriesExhausted caused by HTTP 429 after four attempts. The final paced run
completed all 50 tests. No further RedGIFs requests were made after this run.
The failures cover image search, all three video-search sorts, query/user/tag checks, five complete-profile parameters, partial/profile-filter checks, and the single-page profile check. In compound tests, assertions after the failed request were not reached; those later assertions are not counted as verified in the final run.
https://api.redgifs.com/v2/gifs/search?tags=Amateur&order=latest&count=3&type=i&page=1https://api.redgifs.com/v2/gifs/search?tags=Amateur&order=trending&count=5&type=g&page=1https://api.redgifs.com/v2/gifs/search?tags=Amateur&order=latest&count=5&type=g&page=1https://api.redgifs.com/v2/gifs/search?tags=Amateur&order=top&count=5&type=g&page=1https://api.redgifs.com/v2/gifs/search?query=Amateur&order=trending&count=3&type=g&page=1https://api.redgifs.com/v2/users/dainty-blossomx2/search?count=6&page=1&order=new&type=ghttps://api.redgifs.com/v2/users/dainty-blossomx2/search?count=5&page=1&order=new&type=ghttps://api.redgifs.com/v2/users/naughtygardengirl/search?count=40&page=1&order=new&type=ghttps://api.redgifs.com/v2/users/nickiipeach/search?count=6&page=1&order=new&type=ghttps://api.redgifs.com/v2/users/rubycammilf/search?count=2&page=1&order=new&type=ghttps://api.redgifs.com/v2/users/naughtygardengirl/search?count=3&order=new&type=g&page=1https://api.redgifs.com/v2/users/dainty-blossomx2/search?count=40&page=1&order=new&type=g
An earlier 43-test run returned 41 passed / 2 failed before the suite was expanded
to 50 cases. Its two failures were retained completeness assertions for
test_complete_profile[dainty-blossomx2-6] and [dainty-blossomx2-5]: independent
live pages and a single-page snapshot demonstrated ten slots but only nine unique IDs.
No assertion was skipped or relaxed to count duplicate slots as complete. The final
rate limits prevent reaching these completeness assertions again, not their removal.
Outstanding conditions: the RedGIFs request quota must permit these public search and profile requests again, and equal-timestamp pagination needs stable upstream ordering or a stable cursor. No reset time was established during this run. No session rotation to evade quotas, protection bypasses, or retries until a lucky page order were used to conceal either blocker.
An earlier full run before pacing returned 6 failed / 44 passed: two upstream profile completeness failures, three HTTP 429 failures, and one incorrect test assumption that the site's audio flag guaranteed absence of an audio track. The legacy file was measured with ffprobe and ffmpeg (audible AAC; mean -15.2 dB, maximum 0.0 dB). Its expected actual audio is now asserted explicitly while preserving the inaccurate site flag. Rate-limit failures were not skipped; the final paced suite was rerun after cooldown.
The shared backend emits Tenacity's initial-parameter deprecation warning. This is
reported as a dependency warning; the shared project was not altered.
Source and capability checks
The list below describes implemented coverage exercised across the recorded runs. Source, host, direct-download, CLI, collection and niche checks pass in the final run. Search and user-listing gates are blocked by the listed HTTP 429 responses in that run. The image test completes both rendition downloads before failing at image search.
- Video samples:
hotpinkacrobatickitfox,ivoryhauntingblackbird, legacyhappyfrenchindianspinyloach; rich metadata/download sampletoughlongtermbanteng. Representative watch URL:https://www.redgifs.com/watch/hotpinkacrobatickitfox. - Independent API, independent HTML, combined loading, both source load orders, deferred fields, serialization without fetching, explicit type mismatches and unavailable video responses are exercised. API dates/duration/tags/author/dimensions/ renditions are compared to the correct live detail object.
- HTML titles/page descriptions/raw dates remain available; normalized publication dates, statistics, original dimensions and media URLs prefer API data. Empty and missing descriptions remain distinct from unloaded fields.
- Image sample:
https://www.redgifs.com/watch/deliciousfrontelephant, with API/HTML/ combined/deferred loading and complete HD/SD JPG downloads. - Live page hosts:
redgifs.com,www.redgifs.com; watch and embed routes. Live CDN identity inputs:media.redgifs.comHD/SD/silent MP4 and HD/SD/thumbnail JPG. No language variants or proxy hosts are accepted. - Search: query, tag filter, trending/latest/top, verified videos with latest ordering, verified creator search; image search, complete and trending tag directories.
- Users, image/video tabs, public collection directory/details/mixed item streams, niche directory/details/video streams and video author/niche relationships.
- Partial stream closure, deferred iterator models, original ordering, profile tag/top filters and caller-owned core/session preservation.
- Two CLI invocations download real files: direct URL argument and UTF-8 URL file.
Completed media downloads
Each reported file is a full download through Video.download(DownloadConfigRAW(...))
or Image.download(...), inspected with ffprobe for format, dimensions, duration and
audio. Callbacks are exercised and caller configuration is preserved. Source URLs are
refreshed before downloads. All samples also download best, half, and worst;
SD is selected for the lower-middle/worst selectors and HD for best when both exist.
Legacy happyfrenchindianspinyloach reports hasAudio=false but its real MP4 has
audible AAC audio; actual audio is measured and asserted independently of that flag.
Silent is explicit. Absent silent and unsupported numeric quality requests are rejected.
| Video ID | Rendition | Bytes | Dimensions | Seconds | Audio |
|---|---|---|---|---|---|
| hotpinkacrobatickitfox | hd | 6145756 | 1080x1920 | 6.467 | True |
| hotpinkacrobatickitfox | sd | 1201410 | 480x854 | 6.467 | True |
| hotpinkacrobatickitfox | silent | 6057605 | 1080x1920 | 6.467 | False |
| ivoryhauntingblackbird | hd | 5456747 | 1080x1920 | 7.767 | True |
| ivoryhauntingblackbird | sd | 944146 | 480x854 | 7.767 | True |
| ivoryhauntingblackbird | silent | 5375433 | 1080x1920 | 7.767 | False |
| toughlongtermbanteng | hd | 12195647 | 1080x1920 | 31.400 | True |
| toughlongtermbanteng | sd | 3013502 | 480x854 | 31.400 | True |
| toughlongtermbanteng | silent | 11614962 | 1080x1920 | 31.400 | False |
| happyfrenchindianspinyloach | hd | 4616356 | 1280x640 | 15.000 | True |
| happyfrenchindianspinyloach | sd | 1432872 | 640x320 | 15.000 | True |
| Image ID | Rendition | Bytes | Dimensions |
|---|---|---|---|
| deliciousfrontelephant | hd | 320398 | 1200x1600 |
| deliciousfrontelephant | sd | 122777 | 800x1067 |
The concurrent check downloads different videos simultaneously, then repeats one SD download and compares its complete bytes. It validates durations independently to catch cross-video source leakage. CLI files are additionally probed. No signed source URLs or session tokens are included in this report.
HLS: actual HD/SD playlists for hotpinkacrobatickitfox are fetched and checked for
EXT-X-MAP, EXT-X-BYTERANGE, and EXT-X-ENDLIST. HLS downloads remain unsupported:
the current shared downloader discards required byte ranges. No HLS download or remux
success is claimed. RAW returns a boolean; HLS reports and metadata tag export are not
advertised by this integration.
Complete listing comparisons
Each successful full comparison checks every intended page and the entire ordered ID sequence against an independent live JSON reader. The current first-party profile and collection JavaScript identifies these JSON responses as the rendered tab data; the investigation notes record the relevant assets. No recommendation/boosted arrays are mixed into the intended listing.
- collection weirdjack/d7ddef5fb0: public expected=246 actual=246; stored contentCount=258; delta=12
- niche duo-chastity: expected=5 actual=5; niche gifs=5
The final paced run reconfirms the collection and niche comparisons above. Earlier successful full-profile comparisons recorded:
naughtygardengirl: expected/actual video IDs 76/76; one public image; stored published count 78.nickiipeach: expected/actual video IDs 17/17; one image; stored published count 18.rubycammilf: expected/actual video IDs 4/4; six images; stored published count 10.dainty-blossomx2in a single page: expected/actual 10/10.
These earlier profile comparisons were not reverified in the final paced run because of HTTP 429. They are retained as dated evidence, not counted as current passing gates.
The collection matches all 246 public records in pages of 100/100/46, including two
images on the final page. Its stored contentCount=258 is 12 higher. The API supplies
neither those additional IDs nor reasons for the discrepancy; none are invented.
naughtygardengirl exposes 76 videos and one image versus 78 stored published records:
one record is not publicly accounted for, with no API reason disclosed. nickiipeach
(17 videos plus one image) and rubycammilf (four videos plus six images) reconcile their
stored published counts. rubycammilf covers an exact multiple at two videos per page;
nickiipeach covers a partial final page at six per page. Start-page ranges and
beyond-last behavior are exercised. Collections discover page 1 to avoid mistaking a
beyond-last HTTP 404 for an unavailable collection.
Outstanding profile completeness gate
For dainty-blossomx2, all ten uploads have timestamp 1782281354.
| Requested page size | Slots | Unique IDs | Repeated ID | Missing versus single page |
|---|---|---|---|---|
| 6 | 10 | 9 | activetiredaustralianfreshwatercrocodile | icyhappyhamadryad |
| 5 | 10 | 9 | icyhappyhamadryad | activetiredaustralianfreshwatercrocodile |
| 40 (one page) | 10 | 10 | none | none |
Exact failing endpoint family:
https://api.redgifs.com/v2/users/dainty-blossomx2/search?count=5&page=1&order=new&type=g
and page 2; likewise count 6. The page responses themselves overlap despite unchanged
content, no promoted cards, and total=10. The IDs can change as ties reorder.
The library preserves the raw sequence rather than hiding duplicates or manufacturing
missing records. Requests of count 40 are a verified workaround for this small profile.
Completion needs stable upstream tie ordering/cursors; a single-page workaround does
not prove reliable pagination for all larger profiles. The failing gates remain visible.
Packaging
Exact command:
.venv/bin/python -m build --outdir dist
Wheel and sdist build successfully. The sdist includes README, LICENSE, py.typed and
the public package; the wheel includes README content in metadata, the AGPL license,
py.typed, public exports and the CLI entry point. LICENSE matches the XVideos reference
byte for byte. Nothing was published, pushed, or wired into Porn Fetch.
Gallery enumeration, private/account actions and personalized/recommendation feeds are not implemented. These are documented capability boundaries, not successful test gates.
Errors and troubleshooting — RedGIFs API
redgifs_api.modules.errors re-exports shared BotDetection, DownloadFailed,
NetworkError, NotFound, ProxyError, ResourceGone, UnknownNetworkError, and
VideoUnavailable. Original HTTP failures and their causes are preserved; not every
HTTP failure is translated to a provider alias.
BaseMedia wraps ordinary loader failures in MediaLoadError or MediaLoadErrors.
An unavailable resource must not become an empty successful object. Catch the outer
loading wrapper, then inspect the original cause or use is_resource_gone().
from base_api import MediaLoadError, MediaLoadErrors, is_resource_gone
async def fetch_video(client, url):
try:
return await client.get_video(url)
except (MediaLoadError, MediaLoadErrors) as error:
if is_resource_gone(error):
print("Resource unavailable:", url)
raise
| Symptom | Meaning / action |
|---|---|
ValueError before fetching |
Unsupported host/route/identifier, invalid enum, page range or count |
DataNotLoadedError |
Explicitly await load_fields/load_sources; API-only title remains unloaded |
| Wrong media type | get_video rejects images; get_image rejects videos, including HTML-only loading |
| HTTP 404 / 410 | Missing/deleted resource; preserve cause, do not fabricate metadata |
| HTTP 401 | Anonymous API session refreshes once through ordinary public authentication |
| HTTP 429 / RequestRetriesExhausted | Respect bounded backend retry/backoff and Retry-After; wait for quotas to permit requests, configure pacing/concurrency |
| HTTP 403 or challenge | Access failure; the provider does not solve CAPTCHA or bypass access protection |
| Missing ID-matched structured data | HTML loader fails rather than merging another video, recommendations or a directory page |
| ScrapeResult error | Unwrap or configure shared iterator policy intentionally; discovery errors raise directly |
| Duplicate IDs and missing upload across pages | Upstream equal-timestamp ordering; no deduplication can recover a genuinely omitted ID |
| Published count exceeds listing | Counts may include images or unavailable records; compare exact intended tabs |
| Null/empty API description overwrites HTML description | Declared API precedence; HTML text stays in page_description |
| Malformed HTML date | Preserved in html_publish_date; use normalized API publish_date |
| has_audio false but audible MP4 | Known legacy API metadata error; inspect files when audio decisions matter |
| Exact rendition missing | get_download_url raises ValueError; download wraps the cause in DownloadFailed |
| Numeric quality / HLS config | Unsupported by this provider; choose actual RAW rendition labels |
The final run's 12 failures were all HTTP 429 after four attempts, despite two-second per-core pacing. No quota-reset time was established. More immediate full-suite retries are not evidence of success. Earlier two-page profile completeness failures persist as an independent upstream ordering issue. See validation.
asyncio cancellation and DownloadCancelled propagate. Close streams with context
managers and keep an injected core alive for their work. Do not swallow cancellation
inside broad retry loops. Tenacity's initial deprecation warning comes from the shared
backend; it is not itself an extraction failure.
CLI — RedGIFs API
Run redgifs_api or python -m redgifs_api. Exactly one of --download and --file
is required; the URL is not a positional argument.
redgifs_api -h
redgifs_api --download https://www.redgifs.com/watch/hotpinkacrobatickitfox --quality sd --output ./
python -m redgifs_api --file urls.txt --quality worst --output ./
| Flag | Default | Meaning |
|---|---|---|
-d, --download |
— | One supported video URL or stable ID |
-f, --file |
— | UTF-8 file with one video URL/ID per line; blank lines ignored |
-o, --output |
./ |
Output directory; unloaded titles use stable IDs as filenames |
-q, --quality |
best |
best, half, worst, hd, sd or silent |
-h, --help |
— | Print help without network requests |
File inputs are processed sequentially. The CLI loads API metadata and uses
DownloadConfigRAW; it closes the owned client/core after completion or an exception.
It has no --info, --no-title, image mode, account flags, numeric resolution or HLS
switches. Use the Python API for injected RuntimeConfig, exact filenames, callbacks
and cancellation. Failed downloads propagate and give a nonzero process exit.
Both direct-argument and file-input paths completed real downloads in the final suite, with output verified by ffprobe. The library's broader live suite is still incomplete; see validation.
Supported platforms and hosts — RedGIFs API
Requires Python 3.12+. Live checks ran on Linux with CPython 3.15.2 and local
eaf-base-api 4.2.6, curl-cffi 0.17.0 and selectolax 1.0.2. Other platforms and Python
versions were not live-verified. ffprobe is needed for live file validation, not for
normal transfers. There is no runtime FFmpeg requirement for direct MP4/JPG downloads.
Accepted hosts and routes
| Input | Behavior |
|---|---|
redgifs.com, www.redgifs.com |
Verified first-party page hosts; canonical objects use https://www.redgifs.com |
/watch/<id>, /ifr/<id> on those hosts |
Video/image identity; HTML/API loaders validate the requested media type |
media.redgifs.com MP4/JPG media URLs |
Identity input resolves through the API; actual source URLs retain casing and queries |
/users/<username> on a page host |
User getter; plain username also accepted |
/niches/<id> on a page host |
Niche getter; plain niche ID also accepted |
api.redgifs.com |
Internal public API origin, not a page-host input alias |
CDN suffixes such as -mobile, -silent, -poster, -small, -medium, -large
and -timeline normalize to the stable media ID. HLS fMP4 identities use .m4s;
this acceptance does not implement HLS downloading. Parsed IDs normalize to lowercase;
returned CDN paths are not lowercased or stripped of signatures.
No language/proxy variants, arbitrary subdomains, legacy thumbnail hosts or lookalike domains are accepted. The client exposes no base_url/locale route option. HTTP(S) input is normalized to the canonical HTTPS object identity. Each getter still requires the proper resource route; a directory is not an individual media object.
Publication and current hosted documentation/CI status are not asserted by local validation. The latest suite has 38 passing and 12 rate-limited failures; see validation for actual capability coverage.
Changelog — RedGIFs API
0.1.0 — initial local implementation
- Async
Client,Video,Image,User,Niche,Collectionand immutableTagrecords. - Independent API/HTML media loaders, deferred fields, typed relationships and raw metadata.
- Video/image/creator search, full/trending tags, niches and public collections.
- Shared numeric-page result streams with count/range control, source selection and caller-controlled ordering/concurrency/error policy.
- Public anonymous API authentication through BaseCore; no mutable global auth/CDN headers.
- Shared RAW MP4/JPG transfers, exact HD/SD/silent labels, best/half/worst selectors, source refresh, cancellation, callbacks, filename handling and download/file CLI modes.
- Verified page/CDN hosts and lowercase content identity while preserving source URL casing.
- 23 complete MP4 matrix downloads, two JPG renditions, concurrent/repeated transfers and CLI checks; complete collection 246/246 and niche 5/5 in the final run.
- Final live suite on 2026-11-10: 38 passed / 12 failed / 0 skipped in 363.26 seconds. All final failures were HTTP 429. Earlier equal-timestamp profile page comparisons had omissions/duplicates, and those assertions remain intact.
- Matching HTML and searchable MCP documentation following the shared documentation layout.
HLS URLs are inspectable but HLS downloads, gallery enumeration, private/account actions and personalized recommendation feeds are not implemented. No claim is made that the full validation gate has passed. See getting started and validation.