TikTok API Python Tutorial: OAuth, Upload, Read Videos
To use the TikTok API from Python, call TikTok's official endpoints with the requests library. Exchange the Login Kit code at open.tiktokapis.com/v2/oauth/token/, upload video through the Content Posting API with FILE_UPLOAD chunks or PULL_FROM_URL, and read profiles and videos with the user info and video list endpoints. Adeli publishes to TikTok through TikTok API for Business in one POST.
You use the TikTok API from Python by calling TikTok's official HTTP endpoints with the requests library. There are three pieces: Login Kit's OAuth flow for a user access token, the Content Posting API to upload video, and the user info and video list endpoints (often called the Display API) to read a profile and its videos. This tutorial has working Python for each, written against TikTok's documented requests.
Everything below is TikTok for Developers. TikTok API for Business is a separate portal with its own tokens; TikTok API for Business vs. Developers explains which one you need.
Is there an official TikTok Python library?
No official Python SDK covers TikTok for Developers, so you call it with requests. TikTok's docs for Login Kit, the Content Posting API, and the Display API are curl examples, and each one maps to a requests.get or requests.post. The requests docs, from the library created by Kenneth Reitz (release 2.34.2, May 2026), describe it like this:
"Requests is an elegant and simple HTTP library for Python, built for human beings."
Source: Requests documentation
With plain requests, a new TikTok field is one more string in fields, with no SDK release to wait for. Before you search PyPI, know two things:
- TikTok's official SDK is for the Business API. TikTok's tiktok-business-api-sdk (
pip install tiktok-business-api-sdk-official) covers Marketing API areas such as authentication, campaign creation, reporting, and Business Center. It doesn't document Login Kit or the Content Posting API. - Scraping libraries aren't the TikTok API. Packages that read TikTok by imitating its web app skip OAuth and the account owner's consent. They break when TikTok changes its app, and they can't publish on a customer's behalf.
What do you need before you start?
- Python 3 and
requests.pip install requests. - A TikTok for Developers app with Login Kit and the Content Posting API added. Our guide on how to get TikTok API access covers the review.
- Scopes.
user.info.basicfor the profile,user.info.statsfor counts,video.listto read videos, andvideo.publish(direct post) orvideo.upload(drafts) to post. - A registered redirect URI. HTTPS, static, with no query string or fragment, per TikTok's Login Kit for Web guide.
Keep secrets in environment variables:
import os
import secrets
import urllib.parse
import requests
CLIENT_KEY = os.environ["TIKTOK_CLIENT_KEY"]
CLIENT_SECRET = os.environ["TIKTOK_CLIENT_SECRET"]
REDIRECT_URI = "https://yourapp.example/tiktok/callback"
API = "https://open.tiktokapis.com/v2"Step 1: Send the user to TikTok's authorize page
Build the authorize URL with your client key, scopes, response_type=code, the redirect URI, and a random state.
def authorize_url() -> tuple[str, str]:
state = secrets.token_urlsafe(16)
params = {
"client_key": CLIENT_KEY,
"scope": "user.info.basic,user.info.stats,video.list,video.publish",
"response_type": "code",
"redirect_uri": REDIRECT_URI,
"state": state,
}
return "https://www.tiktok.com/v2/auth/authorize/?" + urllib.parse.urlencode(params), stateStore state in the user's session. When TikTok redirects back, check that the returned state matches before you use the code. That's TikTok's CSRF protection. If a scope is toggleable, the user can deny it and still grant the others, so read the granted scope from the token response rather than assuming.
Step 2: Exchange the code for an access token
POST the code to https://open.tiktokapis.com/v2/oauth/token/ as form data, not JSON. TikTok's token management guide requires Content-Type: application/x-www-form-urlencoded, which requests sets when you pass data=:
def exchange_code(code: str) -> dict:
resp = requests.post(
f"{API}/oauth/token/",
data={
"client_key": CLIENT_KEY,
"client_secret": CLIENT_SECRET,
"code": code, # URL-decoded, which your web framework does for you
"grant_type": "authorization_code",
"redirect_uri": REDIRECT_URI,
},
timeout=30,
)
resp.raise_for_status()
return resp.json()The response carries access_token, expires_in, open_id, refresh_token, refresh_expires_in, scope, and token_type. The access token lasts 24 hours and the refresh token lasts 365 days, so refresh on a schedule:
def refresh(refresh_token: str) -> dict:
resp = requests.post(
f"{API}/oauth/token/",
data={
"client_key": CLIENT_KEY,
"client_secret": CLIENT_SECRET,
"grant_type": "refresh_token",
"refresh_token": refresh_token,
},
timeout=30,
)
resp.raise_for_status()
return resp.json() # save BOTH tokens: the refresh token can changecode_verifier (PKCE) is only for mobile and desktop apps, per TikTok's guide. Our TikTok API access token post covers the Business API token and the errors that mean a token is dead.
Step 3: Upload a video with FILE_UPLOAD chunks
Initialize the post, then PUT the file to upload_url in order, one chunk at a time. TikTok's media transfer guide sets the chunk rules:
- Chunks are 5 MB to 64 MB. The final chunk can be up to 128 MB, to absorb the leftover bytes.
- Files under 5 MB go in one piece, with
chunk_sizeequal to the file size. total_chunk_countisvideo_size // chunk_size, from 1 to 1,000 chunks.- Each PUT returns 206 until the last one, which returns 201.
First ask TikTok which privacy levels this creator allows. Your app must use one of them:
def creator_info(token: str) -> dict:
resp = requests.post(
f"{API}/post/publish/creator_info/query/",
headers={"Authorization": f"Bearer {token}",
"Content-Type": "application/json; charset=UTF-8"},
timeout=30,
)
resp.raise_for_status()
return resp.json()["data"] # privacy_level_options, max_video_post_duration_sec, ...Then initialize and upload:
CHUNK = 10 * 1024 * 1024 # 10 MB
def upload_video(token: str, path: str, title: str, privacy_level: str) -> str:
size = os.path.getsize(path)
chunk_size = min(size, CHUNK)
count = size // chunk_size
init = requests.post(
f"{API}/post/publish/video/init/",
headers={"Authorization": f"Bearer {token}",
"Content-Type": "application/json; charset=UTF-8"},
json={
"post_info": {"title": title, "privacy_level": privacy_level},
"source_info": {
"source": "FILE_UPLOAD",
"video_size": size,
"chunk_size": chunk_size,
"total_chunk_count": count,
},
},
timeout=30,
)
init.raise_for_status()
data = init.json()["data"]
with open(path, "rb") as f:
for i in range(count):
start = i * chunk_size
# the last chunk takes every remaining byte
end = size - 1 if i == count - 1 else start + chunk_size - 1
f.seek(start)
body = f.read(end - start + 1)
put = requests.put(
data["upload_url"],
headers={"Content-Type": "video/mp4",
"Content-Range": f"bytes {start}-{end}/{size}"},
data=body,
timeout=300,
)
if put.status_code not in (201, 206):
raise RuntimeError(f"chunk {i}: {put.status_code} {put.text}")
return data["publish_id"]Retry a chunk on a 5xx, which TikTok's guide says to do. A 403 means upload_url expired, and a 416 means your Content-Range doesn't match what TikTok received. The direct post init is limited to 6 requests per minute per user token.
To send a draft to the creator's inbox instead, use /post/publish/inbox/video/init/ with the video.upload scope and no post_info. TikTok Direct Post API vs Upload to Inbox compares the two.
Step 4: Or let TikTok pull the video from a URL
Set source to PULL_FROM_URL and pass a video_url; TikTok downloads the file, so there's no upload loop.
"source_info": {
"source": "PULL_FROM_URL",
"video_url": "https://media.yourapp.example/clips/launch.mp4",
}FILE_UPLOAD |
PULL_FROM_URL |
|
|---|---|---|
| Your code | Chunked PUTs to upload_url |
One init call |
| Setup | None | Verify the domain or URL prefix in the developer portal |
| URL rules | n/a | HTTPS, no redirects, reachable for the download (it times out after one hour) |
| Typical error | 416 on a bad Content-Range |
url_ownership_unverified on an unverified domain |
Use PULL_FROM_URL when the file already sits in storage you control. Presigned object-storage URLs only work if their host is a domain or prefix you verified.
Step 5: Check the publish status
Poll /post/publish/status/fetch/ with the publish_id until it reads PUBLISH_COMPLETE or FAILED.
import time
def wait_for_publish(token: str, publish_id: str) -> dict:
for _ in range(20):
resp = requests.post(
f"{API}/post/publish/status/fetch/",
headers={"Authorization": f"Bearer {token}",
"Content-Type": "application/json; charset=UTF-8"},
json={"publish_id": publish_id},
timeout=30,
)
resp.raise_for_status()
data = resp.json()["data"]
if data["status"] in ("PUBLISH_COMPLETE", "FAILED", "SEND_TO_USER_INBOX"):
return data
time.sleep(5)
raise TimeoutError(publish_id)Until TikTok audits your app, every direct post is SELF_ONLY, and only private accounts can post. Our guide to posting to TikTok via API covers the audit, privacy rules, and the posting screen TikTok checks.
How do you read a TikTok profile and videos with Python?
Call GET /v2/user/info/ with a fields list, and POST /v2/video/list/ with fields in the query string and paging in the JSON body. Each field needs its scope: per TikTok's user info reference, follower_count, following_count, likes_count, and video_count need user.info.stats.
def user_info(token: str) -> dict:
resp = requests.get(
f"{API}/user/info/",
headers={"Authorization": f"Bearer {token}"},
params={"fields": "open_id,display_name,avatar_url,follower_count,video_count"},
timeout=30,
)
resp.raise_for_status()
return resp.json()["data"]["user"]
def all_videos(token: str):
cursor = None
while True:
body = {"max_count": 20}
if cursor:
body["cursor"] = cursor
resp = requests.post(
f"{API}/video/list/",
headers={"Authorization": f"Bearer {token}"},
params={"fields": "id,title,create_time,share_url,view_count,like_count,comment_count"},
json=body,
timeout=30,
)
resp.raise_for_status()
data = resp.json()["data"]
yield from data["videos"]
if not data["has_more"]:
break
cursor = data["cursor"]max_count tops out at 20 per page, per the video list reference, and cursor is a Unix timestamp in milliseconds. cover_image_url links expire after 6 hours, so don't store them. These are lifetime counters with no history; the TikTok analytics API post covers the Business API endpoints that return daily metrics.
How do you publish to TikTok with Adeli in Python?
One POST to https://app.tryadeli.com/api/v1/posts replaces Steps 1 to 5 and the refresh job. Adeli publishes to TikTok through TikTok API for Business, holds the TikTok app, and refreshes every connected account's tokens. Your code sends one API key:
import os
import requests
resp = requests.post(
"https://app.tryadeli.com/api/v1/posts",
headers={"Authorization": f"Bearer {os.environ['ADELI_API_KEY']}"},
json={
"platform": "tiktok",
"accountId": "00000000-0000-0000-0000-000000000000",
"post_mode": "DIRECT_POST",
"tiktok_title": "Launch day #buildinpublic",
"privacy_level": "PUBLIC_TO_EVERYONE",
"music_usage_confirmed": True,
"video": {"url": "https://media.example/video.mp4"},
},
timeout=60,
)
print(resp.status_code, resp.json())What changes compared with the direct calls:
- No domain verification on your side. Send base64, multipart, or a URL, and Adeli stages it on its own verified media host before TikTok pulls it.
- No private-until-audit phase. A
DIRECT_POSTvideo publishes publicly;post_mode: "MEDIA_UPLOAD"sends it to the creator's inbox as a draft. - Status and creator info are endpoints too: TikTok publish status and TikTok creator info.
TikTok's limit of 15 API posts per account per day still applies. Every field is in the Publish to TikTok docs, and the same endpoint in Adeli's posting API publishes to Instagram, YouTube, Facebook, X, and Bluesky. Your first 2 user profiles are free on pricing.