Source: https://developers.tokportal.com/use-cases/platform-guides/python-quickstart/
Markdown: https://developers.tokportal.com/use-cases/platform-guides/python-quickstart.md

# 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](https://developers.tokportal.com/sdks-cli)

## 1. Install

```bash
pip install requests
```

## 2. Configure auth

TokPortal uses the `X-API-Key` header.

```python


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](https://developers.tokportal.com/authentication)

## 3. Create a bundle

```python
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](https://developers.tokportal.com/create-bundle)

## 4. Configure account

Publishing requires account configuration.

```python
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](https://developers.tokportal.com/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`.

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

```python
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](https://developers.tokportal.com/media-upload)

## 6. Configure the video slot

```python
from datetime import date, timedelta


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": (date.today() + timedelta(days=3)).isoformat(),
            "video_url": video_url,
            "external_ref": "python-video-001",
        },
        timeout=30,
    )
    response.raise_for_status()
    return response.json()
```

Compute the date instead of hardcoding it: the earliest `target_publish_date` is today + 3 days while the account is still being created, and today + 1 once that account is delivered or when the bundle runs on an existing account. An earlier date is rejected with `INVALID_DATE` carrying `earliest_allowed`, and a bundle accepts at most 3 videos per day (`VIDEOS_PER_DAY_EXCEEDED`).

Once a video is delivered it sits in review and auto-finalizes about 72 hours after entering review, regardless of `auto_finalize_videos` — that flag does not switch the timer off. Approve or push back inside that window.

Reference: [Configure Videos](https://developers.tokportal.com/configure-videos)

## 7. Publish

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

```python
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](https://developers.tokportal.com/publish-unpublish)

## 8. Analytics

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

```python
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](https://developers.tokportal.com/analytics)

## Full script

```python

from datetime import date, timedelta


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": (date.today() + timedelta(days=3)).isoformat(),
            "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](https://developers.tokportal.com/create-bundle), [Media Upload](https://developers.tokportal.com/media-upload), [Webhooks](https://developers.tokportal.com/webhooks), [MCP Server](https://developers.tokportal.com/mcp).
