Official Python SDK for the IndReelForge Social Media API. Publish videos, photos, text and documents to Instagram, YouTube, TikTok, Facebook, X (Twitter), LinkedIn, Threads, Pinterest, Reddit, Bluesky, Google Business Profile, Discord, Telegram, Slack, Mastodon & WordPress with a single API call.
pip install indreelforgeRequires Python 3.8+. The only dependency is requests.
from indreelforge import IndReelForge
client = IndReelForge(api_key="your-api-key-here")
result = client.upload_video(
user="my-profile",
platforms=["instagram", "tiktok", "youtube"],
video_path="./my-video.mp4",
title="Check out this video!",
)
print(result["data"]["upload_id"])client = IndReelForge(
api_key="your-api-key", # Required. Dashboard → API Keys
base_url="https://api.indreelforge.com", # Optional (default)
timeout=120, # Optional, seconds (default 120)
session=None, # Optional requests.Session
headers={"X-Trace": "abc"}, # Optional extra headers
)
# Or as a context manager, which closes the HTTP session afterwards:
with IndReelForge(api_key="your-api-key") as client:
print(client.get_me())Every request sends Authorization: Apikey <key> and an explicit User-Agent: indreelforge-python/<version>. (The API sits behind Cloudflare, which blocks generic agents such as Python-urllib; if you call the REST API without this SDK, set your own User-Agent.)
All upload_* methods accept:
| Argument | API field | Description |
|---|---|---|
user |
user |
Profile name (required) |
platforms |
platform[] |
instagram, youtube, tiktok, facebook, x, linkedin, threads, pinterest, reddit, bluesky, google_business, discord, telegram, slack, mastodon, wordpress (also exported as PLATFORMS) |
title |
title |
Title / caption |
description |
description |
Long description |
first_comment |
first_comment |
First comment |
schedule |
schedule |
ISO-8601 datetime to publish later |
timezone |
timezone |
IANA timezone for schedule |
add_to_queue |
add_to_queue |
Put the post in the profile's next free queue slot |
async_upload |
async_upload |
False = wait (up to ~50 s) for the publish result |
request_id |
request_id |
Your own correlation id (see get_request_status) |
idempotency_key |
Idempotency-Key header |
Retries with the same key never double-post |
platform_options / **kwargs |
any | Platform-specific fields, sent as-is (see below) |
# From a local file
client.upload_video(
user="my-profile",
platforms=["instagram", "tiktok", "youtube", "facebook"],
video_path="./video.mp4",
title="My video",
first_comment="#socialmedia",
schedule="2026-12-01T10:00:00Z",
youtube_privacy="public",
youtube_tags="tutorial,tech",
idempotency_key="video-2026-12-01",
)
# From a public URL (the server downloads it)
client.upload_video(
user="my-profile",
platforms=["tiktok"],
video_url="https://cdn.example.com/clip.mp4",
)Local files, public URLs, or both — up to 10 in total.
client.upload_photos(
user="my-profile",
platforms=["instagram", "facebook", "pinterest"],
photo_paths=["./photo1.jpg"],
photo_urls=["https://cdn.example.com/photo2.jpg"],
title="Photo carousel",
platform_options={"pinterest_board_id": "1234567890"},
)client.upload_text(
user="my-profile",
platforms=["x", "threads", "bluesky", "linkedin"],
text="Just shipped a new feature!", # `title=` still works as in 1.0
)client.upload_document(
user="my-profile",
document_path="./deck.pdf", # PDF, PPT, PPTX, DOC, DOCX (max 100 MB)
title="Q4 results",
visibility="PUBLIC",
target_linkedin_page_id="12345", # optional: post as a company page
)By default an upload returns immediately and publishes in the background. With async_upload=False the server waits up to about 50 seconds and returns the outcome:
res = client.upload_text(user="my-profile", platforms=["bluesky"], text="hi", async_upload=False)
if res.get("sync"):
print(res["data"]["status"], res["data"]["platforms"], res["data"]["platform_post_ids"])
else:
# Took longer than the wait window: poll instead
print(client.get_upload_status(res["data"]["upload_id"]))Pass any platform-specific API field through platform_options (or as extra keyword arguments). Lists are sent as repeated fields, booleans as "true"/"false", dicts as JSON.
client.upload_photos(
user="my-profile",
platforms=["tiktok", "google_business", "reddit"],
photo_urls=["https://cdn.example.com/menu.jpg"],
title="New menu is live",
platform_options={
"privacy_level": "PUBLIC_TO_EVERYONE", # TikTok
"auto_add_music": True,
"gbp_location_id": "locations/123", # Google Business Profile
"gbp_cta_type": "LEARN_MORE",
"gbp_cta_url": "https://example.com",
"reddit_subreddit": "food", # Reddit
},
)Use the platform helper methods below to look up page, board and location ids.
Per-platform text overrides (every platform). <platform>_title and <platform>_description replace the shared title / description for that platform only, e.g. x_title, linkedin_description, mastodon_title, wordpress_description.
Mastodon
| Field | Values |
|---|---|
mastodon_visibility |
public (default), unlisted, private or direct |
mastodon_spoiler_text |
Content warning shown before the post |
mastodon_sensitive |
True to mark media as sensitive. Omit it otherwise: the server treats any non-empty value, including "false", as true |
mastodon_alt_text |
Alt text for attached media (defaults to the title) |
mastodon_language |
ISO 639 language code, e.g. "en" |
WordPress
| Field | Values |
|---|---|
wordpress_status |
publish (default) or draft |
wordpress_categories |
Numeric category IDs: "3,7" or [3, 7] |
wordpress_tags |
Numeric tag IDs: "12,15" or [12, 15] |
client.upload_text(
user="my-profile",
platforms=["mastodon", "wordpress", "slack"],
text="Release notes",
description="Full changelog for 1.1 …",
platform_options={
"mastodon_title": "Release notes are out (short version for Mastodon)",
"mastodon_visibility": "unlisted",
"mastodon_language": "en",
"wordpress_status": "draft",
"wordpress_categories": [3, 7],
"wordpress_tags": "12,15",
},
)Discord, Telegram, Slack, Mastodon and WordPress connect with credentials instead of an OAuth login. connect_with_credentials(profile, platform, credentials=None, **fields) sends them to the API, which verifies them live with the platform before saving them (encrypted). Connecting again replaces the stored credentials. Pass the fields as a dict, as keyword arguments, or both (keyword arguments win):
| Platform | Fields (all required) |
|---|---|
discord |
webhook_url — channel webhook URL |
telegram |
bot_token, chat_id — token from @BotFather; public channel @username or numeric chat ID |
slack |
webhook_url — incoming webhook URL (Slack can only verify it on the first post) |
mastodon |
instance_url, access_token — e.g. https://mastodon.social; token with write:statuses and write:media |
wordpress |
site_url, username, app_password — self-hosted site, a user who can publish, an application password |
from indreelforge import IndReelForgeError
res = client.connect_with_credentials("my-profile", "telegram", bot_token="123456789:AA…", chat_id="@mychannel")
print(res["platform_username"])
try:
client.connect_with_credentials(
"my-profile", "mastodon", {"instance_url": "https://mastodon.social", "access_token": "bad"}
)
except IndReelForgeError as e:
if e.status_code == 400:
# e.code: "MISSING_FIELD" | "FIELD_TOO_LONG" | "INVALID_CREDENTIALS"
print(e.code, (e.body.get("details") or {}).get("field"), e)Other failures: 404 (profile not found) and 502 (the platform could not be reached to verify the details). The required fields per platform are also exported as CREDENTIAL_PLATFORMS.
| Area | Method | Endpoint |
|---|---|---|
| Upload | upload_video(user, platforms, video_path=None, ..., video_url=None) |
POST /api/upload |
upload_photos(user, platforms, photo_paths=None, ..., photo_urls=None) |
POST /api/upload_photos |
|
upload_text(user, platforms, title=None, ..., text=None) |
POST /api/upload_text |
|
upload_document(user, document_path, title=None, ...) |
POST /api/upload_document |
|
| Account | get_me() |
GET /api/me |
| Uploads | get_upload_status(upload_id) |
GET /api/upload/:id/status |
get_request_status(request_id) |
GET /api/uploadposts/status?request_id= |
|
list_uploads(limit=None, page=None, status=None) |
GET /api/uploads |
|
get_upload(upload_id) |
GET /api/uploads/:id |
|
delete_upload(upload_id) |
DELETE /api/uploads/:id |
|
retry_upload(upload_id) |
POST /api/uploads/:id/retry |
|
get_platform_limits() |
GET /api/platform-limits |
|
| Profiles | get_profiles() |
GET /api/profiles |
create_profile(name) |
POST /api/profiles |
|
delete_profile(name) |
DELETE /api/profiles/:name |
|
connect_with_credentials(profile, platform, credentials=None, **fields) |
POST /api/profiles/:name/connect/:platform/credentials |
|
| Schedule | get_scheduled_posts() |
GET /api/schedule |
update_scheduled_post(job_id, scheduled_date=None, timezone=None, title=None, caption=None) |
PATCH /api/schedule/:jobId |
|
cancel_scheduled_post(job_id) |
DELETE /api/schedule/:jobId |
|
| Queue | get_queue_settings(profile) |
GET /api/queue/settings |
update_queue_settings(profile, slots=None, days_of_week=None, timezone=None, max_posts_per_slot=None) |
POST /api/queue/settings |
|
get_queue_preview(profile, count=None) |
GET /api/queue/preview |
|
get_next_queue_slot(profile) |
GET /api/queue/next-slot |
|
| Platform helpers | get_facebook_pages(profile) |
GET /api/platforms/facebook/pages |
get_linkedin_pages(profile) |
GET /api/platforms/linkedin/pages |
|
get_pinterest_boards(profile) |
GET /api/platforms/pinterest/boards |
|
get_google_business_locations(profile) |
GET /api/platforms/google-business/locations |
|
| Analytics | get_analytics(range="30d") |
GET /api/analytics/overview |
get_live_analytics(profile, platforms=None) |
GET /api/analytics/live/:profile |
|
get_post_analytics(upload_id, platform=None) |
GET /api/analytics/post/:upload_id |
|
get_total_impressions(profile, platform=None, period=None, start_date=None, end_date=None) |
GET /api/analytics/total-impressions/:profile |
|
| FFmpeg | get_ffmpeg_usage() |
GET /api/ffmpeg/usage |
list_ffmpeg_jobs() |
GET /api/ffmpeg/jobs |
|
get_ffmpeg_job(job_id) |
GET /api/ffmpeg/jobs/:jobId |
|
| Plans | get_plans() |
GET /api/plans |
Examples:
me = client.get_me() # plan + monthly usage
failed = client.list_uploads(status="failed", limit=10)["uploads"]
if failed:
client.retry_upload(failed[0]["id"])
client.update_queue_settings(
"my-profile",
slots=["09:00", "17:30"],
days_of_week=[1, 2, 3, 4, 5],
timezone="Europe/London",
)
client.upload_video("my-profile", ["youtube"], "./v.mp4", add_to_queue=True)
posts = client.get_scheduled_posts()["scheduled_posts"]
client.update_scheduled_post(posts[0]["id"], scheduled_date="2026-12-24T09:00:00Z")Every API failure raises IndReelForgeError:
| Attribute | Description |
|---|---|
status_code (alias status) |
HTTP status (0 = network error, 408 = client-side timeout) |
code |
API error code (error_code / code), e.g. MISSING_VIDEO, or NETWORK_ERROR / TIMEOUT |
body |
Parsed response body (dict) or raw text |
str(err) |
Human-readable message from the API |
from indreelforge import IndReelForge, IndReelForgeError
try:
client.upload_video(user="my-profile", platforms=["tiktok"], video_path="./v.mp4")
except IndReelForgeError as e:
print(f"API error {e.status_code} ({e.code}): {e}", e.body)
except FileNotFoundError as e:
print(f"File not found: {e}")Invalid arguments (for example neither video_path nor video_url, or more than 10 photos) raise ValueError before any request is made.
IndReelForge runs a hosted Model Context Protocol server, so Claude, ChatGPT, Cursor and other agents can publish posts, check upload status and read analytics with your API key. No Python is needed for this.
Hosted URL (Streamable HTTP) — for clients that support remote MCP servers with headers:
- URL:
https://api.indreelforge.com/mcp - Header:
Authorization: ApiKey your-api-key
stdio clients (Claude Desktop, Cursor) — use the bridge from the Node.js package (requires Node.js 18+):
{
"mcpServers": {
"indreelforge": {
"command": "npx",
"args": ["-y", "indreelforge", "mcp"],
"env": {
"INDREELFORGE_API_KEY": "your-api-key"
}
}
}
}See the Node.js SDK README for all MCP options.
| Platform | Video | Photo | Text | Document | Scheduling |
|---|---|---|---|---|---|
| ✓ | ✓ | - | - | ✓ | |
| YouTube | ✓ | - | - | - | ✓ |
| TikTok | ✓ | ✓ | - | - | ✓ |
| ✓ | ✓ | ✓ | - | ✓ | |
| X (Twitter) | ✓ | ✓ | ✓ | - | ✓ |
| ✓ | ✓ | ✓ | ✓ | ✓ | |
| Threads | ✓ | ✓ | ✓ | - | ✓ |
| ✓ | ✓ | - | - | ✓ | |
| ✓ | ✓ | ✓ | - | ✓ | |
| Bluesky | ✓ | ✓ | ✓ | - | ✓ |
| Google Business Profile | - | ✓ | ✓ | - | ✓ |
| Discord | ✓ | ✓ | ✓ | - | ✓ |
| Telegram | ✓ | ✓ | ✓ | - | ✓ |
| Slack | - | - | ✓ | - | ✓ |
| Mastodon | ✓ | ✓ | ✓ | - | ✓ |
| WordPress | ✓ | ✓ | ✓ | - | ✓ |
Discord, Telegram, Slack, Mastodon and WordPress are connected with credentials; the others connect through an OAuth login in the dashboard (or an app password for Bluesky).
1.1 is backward compatible: existing method names, positional arguments and return values keep working.
- The class is now
IndReelForge;InReelForgeis still exported as an alias. - The exception is
IndReelForgeError;InReelForgeErroris an alias, soexcept InReelForgeErrorstill works. - Uploads are always sent as
multipart/form-data, and file handles are closed after each request. upload_textsends your text astext(the API treatstextandtitlethe same).
python -m venv .venv
.venv/bin/pip install -e ".[test]"
.venv/bin/python -m pytestMIT