Python Async v1.0.0

RedGIFs API

An asynchronous Python API for RedGIFs videos, images, creator profiles, tags, niches, public collections, lazy API/HTML metadata and direct downloads. Powered by eaf_base_api.

GitHub
⚡ Recommended AI Workflow Official MCP Server: https://mcp.echteralsfake.me/mcp

Supercharge your development by connecting your AI coding assistant (Cursor, VS Code / Copilot, Claude Desktop, Windsurf, Zed, Antigravity). No authentication required · No strict rate limits · Covers all 16 APIs · Always up to date · Completely anonymous (0 logs, no profiling). Connecting this MCP server to your AI assistant is absolutely the recommended way to work on and build with these APIs.

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.

See live validation and shared iterator configuration.

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=1
  • https://api.redgifs.com/v2/gifs/search?tags=Amateur&order=trending&count=5&type=g&page=1
  • https://api.redgifs.com/v2/gifs/search?tags=Amateur&order=latest&count=5&type=g&page=1
  • https://api.redgifs.com/v2/gifs/search?tags=Amateur&order=top&count=5&type=g&page=1
  • https://api.redgifs.com/v2/gifs/search?query=Amateur&order=trending&count=3&type=g&page=1
  • https://api.redgifs.com/v2/users/dainty-blossomx2/search?count=6&page=1&order=new&type=g
  • https://api.redgifs.com/v2/users/dainty-blossomx2/search?count=5&page=1&order=new&type=g
  • https://api.redgifs.com/v2/users/naughtygardengirl/search?count=40&page=1&order=new&type=g
  • https://api.redgifs.com/v2/users/nickiipeach/search?count=6&page=1&order=new&type=g
  • https://api.redgifs.com/v2/users/rubycammilf/search?count=2&page=1&order=new&type=g
  • https://api.redgifs.com/v2/users/naughtygardengirl/search?count=3&order=new&type=g&page=1
  • https://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, legacy happyfrenchindianspinyloach; rich metadata/download sample toughlongtermbanteng. 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.com HD/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-blossomx2 in 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, Collection and immutable Tag records.
  • 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.