# TikTok Manager — REST API Reference

Complete reference for the `/api/v1` REST API. The web UI and this API run the same use cases
with the same validation; anything the UI can do, the API can do.

---

## Table of Contents

1. [Overview](#1-overview)
2. [Prerequisites](#2-prerequisites)
3. [Authentication](#3-authentication)
4. [Response Format](#4-response-format)
5. [Account Selection](#5-account-selection)
6. [Health](#6-health)
7. [Accounts](#7-accounts)
8. [Videos (Published)](#8-videos-published)
9. [Publish Now](#9-publish-now)
10. [Publish Status](#10-publish-status)
11. [Scheduled Posts](#11-scheduled-posts)
12. [Insights](#12-insights)
13. [TikTok Caveats](#13-tiktok-caveats)
14. [Error Handling](#14-error-handling)
15. [Code Examples](#15-code-examples)
16. [Quick Reference](#16-quick-reference)

---

## 1. Overview

TikTok Manager publishes and follows up on TikTok videos through the official TikTok APIs
(Login Kit, Display API, Content Posting API): direct posts and drafts, file uploads and
URL pulls, app-side scheduling, video stats and account insights.

## 2. Prerequisites

- A running TikTok Manager instance (`docker compose up -d` — web on `https://localhost:9506`
  by default, plus the `tiktok-scheduler` container for scheduled posts).
- At least one account connected through the web UI (TikTok consent screen — the API cannot
  drive it).
- The `API_KEY` from the server's `.env`.

## 3. Authentication

Every request must carry the API key in a header (never in the query string):

```
X-API-Key: <your-api-key>
```

or

```
Authorization: Bearer <your-api-key>
```

A missing or wrong key returns `401 UNAUTHORIZED`. The comparison is constant-time.

## 4. Response Format

Every endpoint returns the same envelope:

```json
{ "success": true,  "data": { ... }, "error": null }
```

On error:

```json
{ "success": false, "data": null, "error": { "code": "ERROR_CODE", "message": "Human-readable" } }
```

Error messages are safe to show to a user; internal details (TikTok error payloads) go to the
server log only.

## 5. Account Selection

Endpoints that act on an account accept either:

| Source | Example |
|---|---|
| Query parameter | `?account_id=1` (database id **or** TikTok open_id) |
| Header | `X-Account-Id: 1` |

- **Read endpoints** fall back to the first connected account when neither is given.
- **Mutating endpoints** (`POST`, `PATCH`, `PUT`, `DELETE`) have **no fallback**: without an
  explicit account they return `400 EXPLICIT_ACCOUNT_REQUIRED`.

## 6. Health

### 6.1 Health Check

```
GET /api/v1/health
```

**Response `data`:** `{ "status": "ok", "version": "1.0" }`

---

## 7. Accounts

### 7.1 List Connected Accounts

```
GET /api/v1/accounts
```

**Response `data`:** array of account objects sorted by display name. Tokens are encrypted at
rest and are never part of any response.

```json
[
  {
    "id": 1,
    "open_id": "open_id_0001",
    "display_name": "My Account",
    "avatar_url": "https://...",
    "scopes": ["user.info.basic", "user.info.stats", "video.list", "video.publish"],
    "follower_count": 1000,
    "following_count": 100,
    "likes_count": 5000,
    "video_count": 12,
    "created_at": "2026-10-13T09:00:00+00:00"
  }
]
```

### 7.2 Get One Account

```
GET /api/v1/accounts/{database_id}
```

### 7.3 Refresh Account Stats

```
POST /api/v1/accounts/{database_id}/refresh
```

Refreshes the token when needed and fetches fresh profile/stats from TikTok.

**Response `data`:** the updated account object.

### 7.4 Disconnect an Account

```
DELETE /api/v1/accounts/{database_id}
```

Removes the account, its encrypted tokens and its scheduled posts from this app. The TikTok
account itself is unaffected.

**Response `data`:** `{ "deleted": true, "name": "My Account" }`

---

## 8. Videos (Published)

### 8.1 List Videos

```
GET /api/v1/videos?account_id={id}&max_count=20&cursor=0
```

| Parameter | In | Type | Default | Description |
|-----------|-----|------|---------|-------------|
| `account_id` | query | string/int | first account | Account to list (see §5) |
| `max_count` | query | int | 20 | 1–50 |
| `cursor` | query | int | 0 | Pagination cursor from a previous response |

**Response `data`:**

```json
{
  "videos": [
    {
      "id": "7234567890123456789",
      "title": "New lesson #math",
      "video_duration": 45,
      "cover_image_url": "https://...",
      "create_time": 1700000000,
      "view_count": 1234,
      "like_count": 56,
      "comment_count": 7,
      "share_count": 8
    }
  ],
  "cursor": 20,
  "has_more": false
}
```

---

## 9. Publish Now

Publishing is asynchronous on TikTok's side: the API returns as soon as the video is handed
over and reports the processing status (`PUBLISH_COMPLETE`, `FAILED`, or still `PROCESSING`).
This app waits up to ~90 seconds for a verdict before responding.

### 9.1 Create a Post (JSON — video from a public URL)

```
POST /api/v1/posts?account_id={id}
```

Content-Type: `application/json`. **Requires an explicit account** (§5).

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `video_url` | string | one of video_url/video_file | public https URL TikTok can pull the video from (PULL_FROM_URL) |
| `title` | string | no | caption, ≤ 2,200 characters |
| `privacy_level` | string | no | `SELF_ONLY` (default), `MUTUAL_FOLLOW_FRIENDS`, `PUBLIC_TO_EVERYONE` — unaudited apps: SELF_ONLY only |
| `mode` | string | no | `draft` (default: sent to the creator's TikTok inbox through `/v2/post/publish/inbox/video/init/`; needs `video.upload`; `title` and `privacy_level` are ignored because the creator writes the caption and picks the audience in the TikTok app) or `direct` (needs `video.publish`) |

**Response `data` (201):**

```json
{ "publish_id": "...", "publish_status": "PUBLISH_COMPLETE", "video_id": "72345..." }
```

### 9.2 Create a Post (multipart — video file upload)

```
POST /api/v1/posts/media?account_id={id}
```

Content-Type: `multipart/form-data`. **Requires an explicit account.**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `video_file` | file | yes | the video (`.mp4`, `.mov`, `.webm`, ≤ 4 GB) |
| `title` | string | no | caption, ≤ 2,200 characters |
| `privacy_level` | string | no | as §9.1 |
| `mode` | string | no | as §9.1 |

The file is stored under a server-generated name inside `UPLOAD_FOLDER`, uploaded to TikTok in
32 MB chunks, and deleted immediately after (success or failure). Client file names are never
used as paths.

**Response `data` (201):** as §9.1.

---

## 10. Publish Status

### 10.1 Fetch Publish Status

```
GET /api/v1/posts/{publish_id}/status
```

**Response `data`:**

```json
{
  "publish_id": "...",
  "publish_status": "PUBLISH_COMPLETE",
  "video_id": "72345...",
  "fail_reason": null
}
```

`publish_status`: `PROCESSING_UPLOAD`, `PROCESSING_DOWNLOAD`, `PUBLISH_COMPLETE` or `FAILED`.

---

## 11. Scheduled Posts

TikTok's API has no scheduled publish — this app schedules for you (ADR-005): posts wait in the
database and the `tiktok-scheduler` container publishes them at the chosen time. Times are
UTC.

### 11.1 List Scheduled Posts

```
GET /api/v1/scheduled?account_id={id}
```

**Response `data`:** array of scheduled post objects (all statuses, oldest first):

```json
[
  {
    "id": 1,
    "account_id": 1,
    "title": "Tomorrow's lesson",
    "privacy_level": "SELF_ONLY",
    "mode": "direct",
    "video_url": "https://example.com/lesson.mp4",
    "scheduled_at": "2026-10-14T09:00:00",
    "status": "pending",
    "publish_id": null,
    "video_id": null,
    "error": null,
    "created_at": "2026-10-13T10:00:00"
  }
]
```

### 11.2 Schedule a Post (JSON — video from a public URL)

```
POST /api/v1/scheduled?account_id={id}
```

**Requires an explicit account.** Fields as §9.1 plus:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `scheduled_at` | ISO-8601 | yes | any future time (UTC); the URL must still be reachable at that moment |

**Response `data` (201):** the scheduled post object (status `pending`).

### 11.3 Schedule a Post (multipart — video file)

```
POST /api/v1/scheduled/media?account_id={id}
```

**Requires an explicit account.** Multipart fields as §9.2 plus `scheduled_at` (required). The
file is stored under `MEDIA_FOLDER` until the scheduler publishes it (then deleted), or until
you delete the post.

**Response `data` (201):** the scheduled post object.

### 11.4 Reschedule

```
PATCH /api/v1/scheduled/{id}?account_id={id}
```

Body: `{ "scheduled_at": "2026-10-15T09:00:00+00:00" }`. Puts the post back in the `pending`
queue and clears any previous error. **Requires an explicit account.**

### 11.5 Retry a Failed Post

```
POST /api/v1/scheduled/{id}/retry?account_id={id}
```

Puts a `failed` post back in the `pending` queue with its original time (already past, so the
scheduler publishes it on the next tick). **Requires an explicit account.**

**Response `data`:** the scheduled post object (status `pending`).

### 11.6 Delete a Scheduled Post

```
DELETE /api/v1/scheduled/{id}?account_id={id}
```

Deletes the row and its stored media file. **Requires an explicit account.**

**Response `data`:** `{ "deleted": true, "id": 1 }`

---

## 12. Insights

### 12.1 Account Insights

```
GET /api/v1/insights/account?account_id={id}
```

**Response `data`:** `{ "open_id": "...", "user": { follower_count, following_count, likes_count, video_count, display_name, avatar_url, ... } }`

### 12.2 Top Videos by Views

```
GET /api/v1/insights/videos?account_id={id}&limit=10
```

Ranks the account's recent videos by view count.

**Response `data`:** `{ "open_id": "...", "videos": [ ...video objects as §8.1... ] }`

---

## 13. TikTok Caveats

- **Unaudited apps:** direct posts only accept `privacy_level=SELF_ONLY`; anything else is
  refused by TikTok and surfaces as `403 FORBIDDEN` with a clear message. Drafts are unaffected.
- **Daily caps:** TikTok enforces API rate limits and daily posting limits (`429 RATE_LIMITED`).
- **Drafts (default mode):** a draft is sent to the account's TikTok inbox; TikTok notifies the
  creator, who taps the notification in the TikTok app to edit, caption and post it. Drafts need
  the `video.upload` scope; a mode whose scope the account did not grant is refused with
  `400 INVALID_INPUT`. Drafts need no Direct Post audit.
- **Async processing:** `publish_status` may still be `PROCESSING_*` when the response returns;
  poll §10.1.

---

## 14. Error Handling

| Code | HTTP | Meaning |
|------|------|---------|
| `UNAUTHORIZED` | 401 | Missing or wrong API key |
| `API_NOT_CONFIGURED` | 503 | The server has no `API_KEY` configured |
| `NO_ACCOUNT` | 404 | No account connected / no account matches `account_id` |
| `NOT_FOUND` | 404 | Account, scheduled post or TikTok object does not exist |
| `EXPLICIT_ACCOUNT_REQUIRED` | 400 | Mutation without an explicit account (§5) |
| `INVALID_INPUT` | 400 | Validation failed (message says what) |
| `NO_FILE` | 400 | Media upload without a file |
| `FORBIDDEN` | 403 | TikTok rejected the token, a scope is missing, or the unaudited-app privacy rule |
| `RATE_LIMITED` | 429 | TikTok rate limit or daily posting cap |
| `UPSTREAM_ERROR` | 502 | TikTok API failed |

Error messages are fixed, client-safe strings; the TikTok error payload is logged server-side only.

---

## 15. Code Examples

```bash
API="https://localhost:9506/api/v1"
KEY="X-API-Key: $API_KEY"

# list accounts
curl -H "$KEY" "$API/accounts"

# publish now from a public video URL
curl -X POST -H "$KEY" -H "Content-Type: application/json" \
     -d '{"video_url": "https://example.com/lesson.mp4", "title": "New lesson #math"}' \
     "$API/posts?account_id=1"

# publish now from a local file
curl -X POST -H "$KEY" -F "video_file=@lesson.mp4" -F "title=New lesson #math" \
     "$API/posts/media?account_id=1"

# publish as a draft
curl -X POST -H "$KEY" -H "Content-Type: application/json" \
     -d '{"video_url": "https://example.com/lesson.mp4", "mode": "draft"}' \
     "$API/posts?account_id=1"

# schedule for tomorrow 09:00 UTC
curl -X POST -H "$KEY" -H "Content-Type: application/json" \
     -d '{"video_url": "https://example.com/lesson.mp4", "scheduled_at": "2026-10-14T09:00:00+00:00"}' \
     "$API/scheduled?account_id=1"

# check a publish status
curl -H "$KEY" "$API/posts/<publish_id>/status"

# list scheduled posts / reschedule / delete
curl -H "$KEY" "$API/scheduled?account_id=1"
curl -X PATCH -H "$KEY" -H "Content-Type: application/json" \
     -d '{"scheduled_at": "2026-10-15T09:00:00+00:00"}' "$API/scheduled/1?account_id=1"
curl -X POST -H "$KEY" "$API/scheduled/1/retry?account_id=1"
curl -X DELETE -H "$KEY" "$API/scheduled/1?account_id=1"

# published videos with stats
curl -H "$KEY" "$API/videos?account_id=1&max_count=20"

# top videos by views
curl -H "$KEY" "$API/insights/videos?account_id=1&limit=5"
```

---

## 16. Quick Reference

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/v1/health` | Health check |
| GET | `/api/v1/accounts` | List connected accounts |
| GET | `/api/v1/accounts/{id}` | Get one account |
| POST | `/api/v1/accounts/{id}/refresh` | Refresh account stats |
| DELETE | `/api/v1/accounts/{id}` | Disconnect an account |
| GET | `/api/v1/videos` | Published videos with stats (`?max_count=`, `?cursor=`) |
| POST | `/api/v1/posts` | Publish now (JSON, video URL) — explicit account |
| POST | `/api/v1/posts/media` | Publish now (multipart file) — explicit account |
| GET | `/api/v1/posts/{publish_id}/status` | Publish processing status |
| GET | `/api/v1/scheduled` | List scheduled posts |
| POST | `/api/v1/scheduled/{id}` | Schedule (JSON, video URL) — explicit account |
| POST | `/api/v1/scheduled/media` | Schedule (multipart file) — explicit account |
| PATCH | `/api/v1/scheduled/{id}` | Reschedule — explicit account |
| POST | `/api/v1/scheduled/{id}/retry` | Retry a failed post — explicit account |
| DELETE | `/api/v1/scheduled/{id}` | Delete a scheduled post — explicit account |
| GET | `/api/v1/insights/account` | Account totals |
| GET | `/api/v1/insights/videos` | Top videos by views |
