Skip to content

fix(tiktok): stop posts silently landing in the TikTok inbox instead of publishing#1710

Merged
nevo-david merged 3 commits into
mainfrom
fix/tiktok-posting-method
Jul 14, 2026
Merged

fix(tiktok): stop posts silently landing in the TikTok inbox instead of publishing#1710
nevo-david merged 3 commits into
mainfrom
fix/tiktok-posting-method

Conversation

@giladresisi

Copy link
Copy Markdown
Collaborator

Problem

Scheduled TikTok posts were being marked successful in Postiz and Temporal but never published. The postSocial activity returned postId: "missing" with a tiktok.com/messages release URL.

Root cause: the post had content_posting_method: "UPLOAD" instead of DIRECT_POST. UPLOAD does not publish — TikTok's API delivers the video to the account's app inbox, where the user must manually complete it within ~24h or it's discarded. This is TikTok's SEND_TO_USER_INBOX terminal status, which we (correctly) treat as a successful upload.

Why the agent chose UPLOAD: the chat/MCP agent had to supply content_posting_method with no guidance — it's a bare required enum ('DIRECT_POST' | 'UPLOAD') with no description, and TikTok's agent @Rules said nothing about it. For a video post, "UPLOAD" reads like the obvious choice. The user never asked for it; the model guessed.

Fix

1. @Rules guidance (tiktok.provider.ts) — the agent is now told to use DIRECT_POST unless the user explicitly asks to review/edit inside the TikTok app first, and that UPLOAD does not publish.

2. Schema description (tiktok.dto.ts) — @JSONSchema({ description }) explains, on the field itself, that UPLOAD never publishes. It's metadata only: it registers no validator, so @IsIn([...]) still rejects a missing or invalid value and the field remains required.

Both are prose the model reads before it picks a value, which is the only thing that helps here — the field is required, so the model always sends something, and no server-side default can rescue an explicit UPLOAD. They reach different populations:

Consumer Reached by
Internal web agent + external MCP agents @Rules (via integrationSchema) and the field description
Public API caller that reads rules from GET /public/v1/integration-settings/:id @Rules
Public API caller that reads only the settings schema @JSONSchema description

The dashboard is unaffected — its React form hardcodes DIRECT_POST.

3. Consistent handling of a missing value (tiktok.provider.ts) — while fixing the above it turned out the three places that read content_posting_method disagreed about what an absent value means:

Site Absent value meant
postingMethod() DIRECT_POST — its switch already has case 'DIRECT_POST': default:
buildTikokPostInfoBody() inbox-shaped body, no privacy_level
buildTikokSourceInfoBody() photo post_mode: 'MEDIA_UPLOAD' — the app inbox

Net effect: a photo with no method silently went to the inbox, and a video with no method hit the publish endpoint with a body missing privacy_level, which TikTok rejects. Normalized in one contentPostingMethod() helper — only an explicit UPLOAD selects the inbox flow, everything else publishes — used at all three sites, so the video endpoint's existing default is now the behavior of the whole publish path.

Settings validation already keeps a missing value out of the publish path for normal posts (the field is required in TikTokDto, validated with skipMissingProperties: false), so this is the belt-and-braces layer for anything that bypasses it: drafts, older rows, direct repository writes.

Testing

  • Reproduced and verified live over MCP on a test channel.
  • Drove the three body/endpoint builders with photo and video posts, method present and absent: absent now matches DIRECT_POST exactly (endpoint, post_info.privacy_level, post_mode), and explicit UPLOAD is unchanged. Before the change, absent gave MEDIA_UPLOAD for photos and a privacy_level-less direct body for videos.
  • Generated-schema check on TikTokDto: description present, enum unchanged, content_posting_method still listed in required.
  • Typechecks (backend) — the 6 remaining errors in nestjs-libraries are pre-existing and unrelated (mastodon, media repository, track service).

Not covered here

Two other copies of this enum are documented outside this repo and are unreachable from a backend change. Both are being fixed separately:

  • gitroomhq/postiz-docspublic-api/providers/tiktok.mdx describes UPLOAD as "Upload for manual posting"; public-api/openapi.json has a bare enum with no description. (Hand-maintained, not generated.)
  • gitroomhq/postiz-agent — the skill bundles its own PROVIDER_SETTINGS.md, which lists the enum with no explanation. Skill runs read that file rather than calling integrations:settings, so no backend fix reaches them.

Notes

Supersedes #1687, which could not take the rewritten history (force-push is blocked on that branch). The earlier version of this fix also added a PROVIDER_SETTINGS_DEFAULTS map to integration.schedule.post.ts that filled an omitted content_posting_method; it has been dropped. It put a TikTok-specific value in a generic tools file, it only applied on the MCP path (so the same payload behaved differently through MCP than through the public API), and it was redundant — the field is required, so an omitted key is rejected by validation rather than falling through to UPLOAD. The provider-level normalization above covers the same ground where it actually matters, at publish time.

First of two related changes. A follow-up feature branch (agent list/update of scheduled posts) stacks on this one and will be opened as a separate PR.

🤖 Generated with Claude Code

giladresisi and others added 3 commits July 13, 2026 14:24
The agent (chat / MCP) had to pick content_posting_method with no
guidance: the setting is a bare required enum with no description or
default, and TikTok's agent rules said nothing about it. For a video
post "UPLOAD" reads like the obvious choice - but UPLOAD does not
publish, it only drops the video into the user's TikTok app inbox,
where it must be completed manually within ~24h or it is discarded.
Users hit this as "post marked successful but never published".

TikTok's @rules text now tells the agent to use DIRECT_POST unless the
user explicitly asks to review/edit in the app first, and explains that
UPLOAD does not publish.

No settings default is needed to back this up: content_posting_method is
required in TikTokDto (@isin with no @IsOptional, validated with
skipMissingProperties: false), so an omitted key cannot fall through to
UPLOAD - validatePosts rejects it and the tool hands the error back to
the agent to retry. The failure mode is the agent *choosing* UPLOAD, and
only prose the agent reads can prevent that.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`content_posting_method` was exposed to schema consumers as a bare
`enum: ["DIRECT_POST", "UPLOAD"]` with no description, so an LLM asked to
"upload this video to TikTok" would reasonably pick the value literally
named UPLOAD - which never publishes, it only drops the media into the
user's TikTok app inbox where it expires after 24 hours.

Annotate the field with `@JSONSchema({ description })`. This is metadata
only: it is read by class-validator-jsonschema when generating the schema
and registers no validator, so `@IsIn([...])` still rejects a missing or
invalid value and the field stays required. Verified against the
generated schema: `description` is present, `enum` is unchanged, and
`content_posting_method` remains in `required`.

The annotation reaches every `getValidationSchemas()` consumer:
- `integrationSchema` MCP tool (internal web agent + external MCP agents)
- `GET /public/v1/integration-settings/:id` (public API, and the CLI's
  `postiz integrations:settings`)

The dashboard is unaffected - its React form hardcodes DIRECT_POST.

For agents this layers on top of the @rules text in the previous commit:
both are prose the model reads before it picks a value, which is the only
thing that helps here - the field is required, so the model always sends
*something*, and no server-side default can rescue an explicit UPLOAD.
For public API callers that read the schema but not `rules`, this
description is the only in-band signal.

Precedent for the decorator: whop.dto.ts, discord.dto.ts, listmonk.dto.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three places in the provider read `content_posting_method`, and they did
not agree on what a missing value means:

- `postingMethod()` already defaulted it: its `switch` has
  `case 'DIRECT_POST': default:`, so an absent value picks `/video/init/`
  (publish) rather than `/inbox/video/init/`.
- `buildTikokPostInfoBody()` compared `=== 'DIRECT_POST'`, so an absent
  value fell into the inbox-shaped body with no `privacy_level`.
- `buildTikokSourceInfoBody()` compared the same way, so an absent value
  gave a photo `post_mode: 'MEDIA_UPLOAD'` - the app inbox, which never
  publishes.

Net effect of the disagreement: a photo with no method silently went to
the inbox, and a video with no method hit the *publish* endpoint with a
body missing `privacy_level`, which TikTok rejects.

Normalize once in `contentPostingMethod()` - only an explicit 'UPLOAD'
selects the inbox flow, everything else (including a missing value)
publishes - and use it at all three sites, so the video endpoint's
existing default is now the behaviour of the whole publish path.

Settings validation keeps a missing value out of the publish path for
normal posts (the field is required in TikTokDto), so this is the
belt-and-braces layer for anything that bypasses it - drafts, older rows
written before the field existed, direct repository writes.

Verified by driving the three builders with photo/video posts and the
method present/absent: absent now matches DIRECT_POST exactly (endpoint,
post_info privacy_level, post_mode), and explicit UPLOAD is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@postiz-contribution postiz-contribution Bot added the contribution:approved Approved contributor label Jul 13, 2026
@postiz-contribution

Copy link
Copy Markdown

Contribution-checker quality warning
Heuristic score: 0/100 (low). This is a non-blocking warning surfaced by the project's quality settings.

Heuristics that flagged:

  • Wall-of-text PR body: 5500 chars (>2500)
  • Excessive inline code references: 65 inline refs (>3)
  • PR doesn't use the repo's PR template: 5 required checkbox items missing (max 1, match ≥80%)
  • Body adds too many extra headers beyond the template: 5 extra headers (>1)
  • AI watermark phrase: Matched: "Generated with Claude Code"
  • Commit message too long: Longest: 1680 chars

If this is a genuine contribution, please add detail to your PR description and tighten the diff scope before reviewers look at it.

@postiz-agent

postiz-agent Bot commented Jul 13, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@nevo-david nevo-david merged commit 70ab710 into main Jul 14, 2026
11 checks passed
@nevo-david nevo-david deleted the fix/tiktok-posting-method branch July 14, 2026 07:27
giladresisi added a commit that referenced this pull request Jul 15, 2026
Combine the UPLOAD guidance from #1710 with this branch's settings-visibility
text: keep the 24-hour inbox expiry detail in both the @rules string and the
content_posting_method @jsonschema description, plus the settings-ignored /
video-only constraints from this branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution:approved Approved contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants