# TikTok API Python Tutorial: OAuth, Upload, Read Videos

Call the official TikTok API from Python with requests: OAuth token exchange, chunked video upload, and the user info and video list endpoints.

Canonical page: https://www.tryadeli.com/blog/tiktok-api-python
Last updated: 2026-10-26

By Nick Reyes, Co-founder and CTO, Adeli. Published 2026-10-26.

## The short answer

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](https://www.tryadeli.com/blog/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](https://requests.readthedocs.io/en/latest/)

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](https://github.com/tiktok/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](https://www.tryadeli.com/blog/how-to-get-tiktok-api-access) covers the review.
- **Scopes.** `user.info.basic` for the profile, `user.info.stats` for counts, `video.list` to read videos, and `video.publish` (direct post) or `video.upload` (drafts) to post.
- **A registered redirect URI.** HTTPS, static, with no query string or fragment, per TikTok's [Login Kit for Web](https://developers.tiktok.com/doc/login-kit-web) guide.

Keep secrets in environment variables:

```python
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`.**

```python
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), state
```

Store `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](https://developers.tiktok.com/doc/oauth-user-access-token-management) requires `Content-Type: application/x-www-form-urlencoded`, which `requests` sets when you pass `data=`:

```python
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:

```python
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 change
```

`code_verifier` (PKCE) is only for mobile and desktop apps, per TikTok's guide. Our [TikTok API access token](https://www.tryadeli.com/blog/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](https://developers.tiktok.com/doc/content-posting-api-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_size` equal to the file size.
- **`total_chunk_count` is `video_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:

```python
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:

```python
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](https://www.tryadeli.com/blog/tiktok-direct-post-api) 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.**

```python
"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`.**

```python
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](https://www.tryadeli.com/blog/post-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](https://developers.tiktok.com/doc/tiktok-api-v2-get-user-info), `follower_count`, `following_count`, `likes_count`, and `video_count` need `user.info.stats`.

```python
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](https://developers.tiktok.com/doc/tiktok-api-v2-video-list), 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](https://www.tryadeli.com/blog/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](https://www.tryadeli.com/product/tiktok) 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:

```python
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_POST` video 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](https://www.tryadeli.com/docs/api/posts/get-tiktok-publish-status) and [TikTok creator info](https://www.tryadeli.com/docs/api/posts/get-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](https://www.tryadeli.com/docs/api/posts/publish-tiktok), and the same endpoint in Adeli's [posting API](https://www.tryadeli.com/social-media-posting-api) publishes to Instagram, YouTube, Facebook, X, and Bluesky. Your first 2 user profiles are free on [pricing](https://www.tryadeli.com/pricing).

## Frequently asked questions

### Is there an official TikTok API library for Python?

Not for TikTok for Developers. Login Kit, the Content Posting API, and the Display API are documented as HTTP requests, so Python code calls them with requests. TikTok publishes an official Business API SDK on GitHub, installed as tiktok-business-api-sdk-official, which covers Marketing API areas such as authentication, campaigns, and reporting.

### Can I upload a video to TikTok with Python?

Yes. Initialize the post with the Content Posting API, then PUT the file to the returned upload_url in chunks of 5 MB to 64 MB, or pass a URL on a verified domain with PULL_FROM_URL. Until TikTok audits your app, every post is private to the creator.

### Are Python TikTok scraping libraries the official API?

No. Packages that read TikTok by imitating the web or mobile app aren't TikTok's API and don't use OAuth. They can break when TikTok changes its app, and they don't give you consent from the account owner, which a product acting on customers' accounts needs.

### How long does a TikTok access token last?

A TikTok for Developers user access token lasts 24 hours and the refresh token lasts 365 days. Refresh with grant_type set to refresh_token at the same token endpoint, and save the new refresh token if it changes.

## Related Adeli pages

- [TikTok API for Developers](https://www.tryadeli.com/product/tiktok.md): Publish, schedule, and read analytics for TikTok through one API. Adeli publishes through TikTok API for Business and handles token refresh. Free to start.
- [Social Media Posting & Scheduling API](https://www.tryadeli.com/social-media-posting-api.md): Adeli's social media posting and scheduling API publishes to Instagram, TikTok, YouTube, Facebook, X, and Bluesky in one request, now or at a set time. Free to start.
- [Social Media API for AI Content Generation Tools](https://www.tryadeli.com/use-cases/ai-content-generation.md): Publish the videos and carousels your AI tool creates to TikTok, YouTube, Instagram, and more, then show users how they performed.
- [All posts on the Adeli blog](https://www.tryadeli.com/blog.md)

## About the author

Nick Reyes is the CTO behind Adeli and builds its API, its platform integrations, and the docs. He is a product engineer with 10 years of experience at Phantom, Airbnb, and Google.
