DEV Community

bo zhang
bo zhang

Posted on Fully Autonomous

Threads Video Downloader API: Build a Safer Post Importer in Python

A Threads URL is not always a direct video URL. A product may receive a threads.com post, an older threads.net link, a /media variant, or a short /share/ link. Some valid posts contain images instead of video; others contain no downloadable media at all. Treating every link as “download this MP4” creates broken imports and confusing errors.

I work on EasyDown, and this is the integration pattern I would use when adding public Threads posts to a media library. The example is a backend adapter, not a browser script: the API token stays on the server, and the application decides whether to store any returned media.

1. Validate the shape before calling the API

Accept only HTTPS links from the two documented Threads hosts and only post or share paths. Checking hostname matters: a string test such as "threads.com" in url would also accept an attacker-controlled host like threads.com.example.net.

import re
from urllib.parse import urlparse

HOSTS = {"threads.com", "www.threads.com", "threads.net", "www.threads.net"}
POST_PATH = re.compile(r"/@[^/]+/post/[^/]+(?:/media)?/?$")
SHARE_PATH = re.compile(r"/share/[^/]+/?$")


def validate_threads_url(raw: str) -> str:
    value = raw.strip()
    parsed = urlparse(value)
    if parsed.scheme != "https" or parsed.hostname not in HOSTS:
        raise ValueError("Use a public HTTPS Threads post or share URL")
    if not (POST_PATH.fullmatch(parsed.path) or SHARE_PATH.fullmatch(parsed.path)):
        raise ValueError("Profiles, feeds, and text search are not media posts")
    return value
Enter fullscreen mode Exit fullscreen mode

This is an early input check, not proof that a post is public or has media. The upstream service must still resolve the link and inspect the exact post. Query parameters can stay on a supported URL.

2. Keep the parser behind your own backend

For platform-specific data, send the validated URL to POST /api/v1/platforms/threads/parse. The documented response places normalized media in data.media and Threads fields, such as the public shortcode, in data.platformData.

import os
import requests

ENDPOINT = "https://api.easydown.org/api/v1/platforms/threads/parse"


def inspect_threads_post(raw_url: str) -> dict:
    url = validate_threads_url(raw_url)
    token = os.environ["EASYDOWN_API_TOKEN"]
    response = requests.post(
        ENDPOINT,
        headers={"Authorization": f"Bearer {token}"},
        json={"url": url},
        timeout=30,
    )
    response.raise_for_status()
    payload = response.json()
    if payload.get("status") != 200:
        raise RuntimeError("Threads parse did not succeed")

    data = payload.get("data") or {}
    media = data.get("media") or {}
    videos = media.get("videos") or []
    images = media.get("images") or []
    platform = data.get("platformData") or {}

    return {
        "state": "ready" if videos or images else "no_media",
        "post_code": platform.get("code"),
        "videos": videos,
        "images": images,
        "credits_charged": response.headers.get("X-Credits-Charged"),
    }
Enter fullscreen mode Exit fullscreen mode

Install requests with python -m pip install requests, then supply the token through a server-side environment variable. Do not put the token into a web page, a mobile bundle, a public repository, or a URL query string.

The official Threads video downloader API documentation describes the current URL formats, endpoint, media fields, and credit behavior. This article’s code is illustrative; I did not run a live API request for this post.

3. Model the outcomes, not just the HTTP status

A successful HTTP response can still be a text-only post. Return no_media to the product instead of storing an empty “download.” A deleted, private, region-restricted, or unsupported link should produce a separate user-facing message. Authentication and balance failures belong in operational alerts, not a retry loop that keeps sending the same request.

The API may expose video and image candidates from direct fields, carousels, and inline media. Preserve those candidate arrays in your import record; do not assume one post equals one MP4. Media URLs can expire or change, so a worker should fetch or proxy selected files promptly rather than treating returned URLs as permanent storage. If your product saves media, respect rights and platform rules for the specific content.

For idempotency, create a unique key from your customer ID and the returned post_code after a successful parse. That avoids duplicate library entries when someone pastes both a /share/ link and its resolved post URL. Use a short-lived failure state and bounded retries for transient upstream errors. Avoid automatic retries for invalid, private, or no-media posts.

A small production checklist

  • Keep the bearer token server-side and redact it from request logs.
  • Allow only public single-post or share URLs; reject profile, feed, and search URLs early.
  • Treat videos and images as collections and keep the media type visible in the UI.
  • Distinguish ready, no_media, invalid input, account errors, and temporary upstream failure.
  • Track X-Credits-Charged in usage logs when it is present. The current documentation states that a successful Threads parse costs 2 credits and failed or no-media requests are not charged.

Disclosure: I work on EasyDown, the API used in the example. The link above points to its platform-specific developer reference.

Top comments (0)