Platform Guides

Python Quickstart - TokPortal API

End-to-end Python requests workflow: create a TokPortal bundle, configure account and video fields, optionally upload media, publish, and read analytics.

Python Quickstart

This guide uses raw HTTP with requests. TokPortal also publishes a Node SDK and CLI; Python generated clients may exist locally, but the stable public Python path today is raw HTTP.

Reference: SDKs & CLI

1. Install

pip install requests

2. Configure auth

TokPortal uses the X-API-Key header.



BASE_URL = "https://app.tokportal.com/api/ext"
API_KEY = os.environ["TOKPORTAL_API_KEY"]

JSON_HEADERS = {
    "X-API-Key": API_KEY,
    "Content-Type": "application/json",
}

AUTH_HEADERS = {
    "X-API-Key": API_KEY,
}

Reference: Authentication

3. Create a bundle

def create_bundle() -> str:
    response = requests.post(
        f"{BASE_URL}/bundles",
        headers=JSON_HEADERS,
        json={
            "bundle_type": "account_and_videos",
            "platform": "tiktok",
            "country": "US",
            "videos_quantity": 1,
            "title": "Python quickstart",
            "external_ref": "python-quickstart-001",
        },
        timeout=30,
    )
    response.raise_for_status()
    payload = response.json()
    print("credits charged:", payload.get("credits_charged"))
    return payload["data"]["bundle_id"]

Reference: Create Bundle

4. Configure account

Publishing requires account configuration.

def configure_account(bundle_id: str) -> dict:
    response = requests.put(
        f"{BASE_URL}/bundles/{bundle_id}/account",
        headers=JSON_HEADERS,
        json={
            "username": "python_demo_us",
            "visible_name": "Python Demo",
            "biography": "Daily product videos.",
            "profile_picture_url": "https://cdn.example.com/profiles/python-demo.jpg",
        },
        timeout=30,
    )
    response.raise_for_status()
    return response.json()

Reference: Account Configuration

5. Provide a video URL

If your video is already available at a public/direct URL, use that URL directly as video_url.

video_url = "https://cdn.example.com/videos/promo.mp4"

For local files, use the presigned upload flow. This is usually better for 50-100 MB videos because the binary uploads directly to storage instead of going through your automation tool.

def upload_video_presigned(bundle_id: str, filepath: str) -> str:
    response = requests.post(
        f"{BASE_URL}/upload/video",
        headers=JSON_HEADERS,
        json={
            "filename": os.path.basename(filepath),
            "content_type": "video/mp4",
            "bundle_id": bundle_id,
        },
        timeout=30,
    )
    response.raise_for_status()
    upload = response.json()["data"]

    with open(filepath, "rb") as file_handle:
        put_response = requests.put(
            upload["upload_url"],
            data=file_handle,
            headers={"Content-Type": upload["content_type"]},
            timeout=300,
        )
    put_response.raise_for_status()
    return upload["public_url"]

Reference: Media Upload

6. Configure the video slot

def configure_video(bundle_id: str, video_url: str) -> dict:
    response = requests.put(
        f"{BASE_URL}/bundles/{bundle_id}/videos/1",
        headers=JSON_HEADERS,
        json={
            "video_type": "video",
            "description": "Built from Python #api",
            "target_publish_date": "2026-06-05",
            "video_url": video_url,
            "external_ref": "python-video-001",
        },
        timeout=30,
    )
    response.raise_for_status()
    return response.json()

Reference: Configure Videos

7. Publish

def publish_bundle(bundle_id: str) -> dict:
    response = requests.post(
        f"{BASE_URL}/bundles/{bundle_id}/publish",
        headers=AUTH_HEADERS,
        timeout=30,
    )
    response.raise_for_status()
    return response.json()

If publishing fails, inspect readiness:

def get_publish_readiness(bundle_id: str) -> dict:
    response = requests.get(
        f"{BASE_URL}/bundles/{bundle_id}/publish-readiness",
        headers=AUTH_HEADERS,
        timeout=30,
    )
    response.raise_for_status()
    return response.json()

Reference: Publish & Unpublish

8. Analytics

Analytics are available after the account is delivered and posts have data.

def get_account_analytics(account_id: str) -> dict:
    response = requests.get(
        f"{BASE_URL}/accounts/{account_id}/analytics",
        headers=AUTH_HEADERS,
        timeout=30,
    )
    response.raise_for_status()
    return response.json()

Reference: Analytics

Full script



BASE_URL = "https://app.tokportal.com/api/ext"
API_KEY = os.environ["TOKPORTAL_API_KEY"]

JSON_HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}
AUTH_HEADERS = {"X-API-Key": API_KEY}


def create_bundle() -> str:
    response = requests.post(
        f"{BASE_URL}/bundles",
        headers=JSON_HEADERS,
        json={
            "bundle_type": "account_and_videos",
            "platform": "tiktok",
            "country": "US",
            "videos_quantity": 1,
            "title": "Python quickstart",
            "external_ref": "python-quickstart-001",
        },
        timeout=30,
    )
    response.raise_for_status()
    payload = response.json()
    print("credits charged:", payload.get("credits_charged"))
    return payload["data"]["bundle_id"]


def configure_account(bundle_id: str) -> None:
    response = requests.put(
        f"{BASE_URL}/bundles/{bundle_id}/account",
        headers=JSON_HEADERS,
        json={
            "username": "python_demo_us",
            "visible_name": "Python Demo",
            "biography": "Daily product videos.",
            "profile_picture_url": "https://cdn.example.com/profiles/python-demo.jpg",
        },
        timeout=30,
    )
    response.raise_for_status()


def upload_video_presigned(bundle_id: str, filepath: str) -> str:
    response = requests.post(
        f"{BASE_URL}/upload/video",
        headers=JSON_HEADERS,
        json={
            "filename": os.path.basename(filepath),
            "content_type": "video/mp4",
            "bundle_id": bundle_id,
        },
        timeout=30,
    )
    response.raise_for_status()
    upload = response.json()["data"]

    with open(filepath, "rb") as file_handle:
        put_response = requests.put(
            upload["upload_url"],
            data=file_handle,
            headers={"Content-Type": upload["content_type"]},
            timeout=300,
        )
    put_response.raise_for_status()
    return upload["public_url"]


def configure_video(bundle_id: str, video_url: str) -> None:
    response = requests.put(
        f"{BASE_URL}/bundles/{bundle_id}/videos/1",
        headers=JSON_HEADERS,
        json={
            "video_type": "video",
            "description": "Built from Python #api",
            "target_publish_date": "2026-06-05",
            "video_url": video_url,
            "external_ref": "python-video-001",
        },
        timeout=30,
    )
    response.raise_for_status()


def publish_bundle(bundle_id: str) -> dict:
    response = requests.post(
        f"{BASE_URL}/bundles/{bundle_id}/publish",
        headers=AUTH_HEADERS,
        timeout=30,
    )
    response.raise_for_status()
    return response.json()


if __name__ == "__main__":
    bundle_id = create_bundle()
    configure_account(bundle_id)

    local_video = os.environ.get("PROMO_VIDEO_PATH")
    if local_video:
        video_url = upload_video_presigned(bundle_id, local_video)
    else:
        video_url = os.environ["PROMO_VIDEO_URL"]

    configure_video(bundle_id, video_url)
    result = publish_bundle(bundle_id)
    print(result)

Related docs: Create Bundle, Media Upload, Webhooks, MCP Server.