---
id: CHG-20261014-RG-02
title: Drafts: upload through the inbox endpoint with video.upload; drafts become the default publish path
date: 2026-10-14
author: RG
type: bugfix
scope: [backend, web-ui, api, docs]
status: done
breaking: true
patterns: []
files:
  - app/tiktok_api.py
  - app/config.py
  - app/services/posts.py
  - app/services/scheduled.py
  - app/routes/post_routes.py
  - app/routes/api_routes.py
  - app/templates/post_new.html
  - DOC/API.md
  - README.md
  - .env.example
  - RULEFOLDER/FEATURE-REGISTRY.md
  - tests/conftest.py
  - tests/services/test_posts.py
  - tests/services/test_tiktok_api_init.py
  - tests/routes/test_gui_posts.py
  - tests/test_token_encryption.py
gui_routes:
  - GET /posts/new
  - POST /posts/new
api_endpoints:
  - POST /api/v1/posts
  - POST /api/v1/posts/media
  - POST /api/v1/scheduled
  - POST /api/v1/scheduled/media
parity_exception: ""
api_doc_updated: yes
security_impact: "none — same guards; a post mode is now refused unless the account granted its scope"
tests:
  - tests/services/test_tiktok_api_init.py
  - tests/services/test_posts.py
  - tests/routes/test_gui_posts.py
---

## What
Publishing now sends the video to the creator's TikTok inbox as a draft by default. TikTok notifies
the creator, who taps the notification in the TikTok app to caption and post it. The New Post page
explains this step, shows a preview of the chosen file, and only offers Direct post when the account
granted `video.publish`. The default scopes are now `user.info.basic,user.info.stats,video.list,video.upload`.

## Why
**Problem:** "draft" mode called the Direct Post endpoint (`/v2/post/publish/video/init/`) without
`post_info`, so it never produced a draft; drafts belong to `/v2/post/publish/inbox/video/init/`
with the `video.upload` scope, which the app did not request.
**Product reason for the new default:** a draft needs only `video.upload` and no Direct Post
audit, so it works for every connected creator from day one; unaudited Direct Post is limited to
private (SELF_ONLY) videos. Direct post stays available for accounts that grant `video.publish`.

## How
`tiktok_api._init_body()` chooses the endpoint and body per mode: a draft sends `source_info` only;
a direct post keeps `post_info` (privacy, caption). `services.posts.validate_post_fields()` takes the
account's granted scopes and refuses a mode whose scope (`MODE_SCOPES`) is missing, so the GUI,
the API and scheduling share one rule (ADR-002). Mode `draft` is the default everywhere.
Out of scope: Direct Post's audit UX (creator_info query, no-default privacy, interaction toggles,
commercial-content disclosure); required before the app applies for the Direct Post audit.

## API impact
`POST /api/v1/posts`, `/posts/media`, `/scheduled`, `/scheduled/media`: `mode` now defaults to
`draft` (breaking for a client that relied on the old `direct` default); a mode whose scope the
account did not grant returns `400 INVALID_INPUT`. `DOC/API.md` §9.1 and §13 updated.

## Security review
- Input validated? yes — mode allow-list unchanged, plus the granted-scope check
- Auth guard present on every new route? no new route; existing guards unchanged
- State change on GET? no
- Secrets, tokens or personal data touched, logged or returned? no
- New dependency? no

## Verification
- `.venv\Scripts\python -m pytest` → 71 passed (new: inbox endpoint for file and URL drafts, direct
  keeps post_info, scope gate, draft-first form, draft flash).
- `python RULEFOLDER\tools\build_changelog.py` and `python RULEFOLDER\tools\check_rules.py` → "CHANGELOG.md rebuilt from 4 fragments", "All rule checks passed".
