TikTok (tiktok)
tiktok is the TikTok for Developers (Login Kit + Content Posting API)
provider. It publishes videos from a local file (FILE_UPLOAD) and photo
carousels from public URLs (PULL_FROM_URL), either directly to the profile
(DIRECT_POST) or as a draft in the TikTok app inbox (UPLOAD). Publishing is
two-phase: post_pending registers the upload and returns pending; the engine
polls the read-only status endpoint. There is no finalize step, because the
publish call is irreversible and a retried mutation could post twice.
Do not confuse this with tiktok-business: a different
developer app, a different API host, different OAuth endpoints, a trailing-slash
redirect URI, and Access-Token headers instead of Bearer.
- Rust source:
crates/rustiz-providers/src/tiktok.rs - Upstream port:
tiktok.provider.ts - Editor: Normal
- Connection mode:
oauth
Read the gotchas first. Until the app passes TikTok’s Content Posting API audit, every post is forced to
SELF_ONLYand only private accounts can post, with a 5-users-per-24 h cap. Photo posts only work with a verified URL property (domain or URL prefix). WhenFRONTEND_URLis not HTTPS the login goes throughredirectmeto.com, and that relayed URL must be registered byte-for-byte.
Overview
| Property | Value |
|---|---|
| Identifier | tiktok |
| Display name | Tiktok (the display string in ProviderInfo) |
| Connection mode | oauth (authorization code; no PKCE challenge) |
| Editor | normal |
| Channel kind | social |
| Comments / threads | No. supports_comments = false; only the first post is published |
| Pending / async publishing | Yes. supports_pending = true; post_pending → check_post_status; no finalize_post |
| Analytics | Yes. supports_analytics = true; profile counts plus the last 20 videos |
| Post analytics | Yes. supports_post_analytics = true; views/likes/comments/shares for one video |
| Mentions | No. supports_mentions = false |
is_between_steps | false |
convert_to_jpeg | true — PNG uploads are converted to JPEG by the engine before publishing |
max_concurrent_job | 10000 (effectively unlimited; TikTok’s own per-user limits are the real constraint) |
Character limit (max_length) | 2000 characters (stricter than TikTok’s 2200 video / 4000 photo-description limits) |
| API | https://open.tiktokapis.com (v2) |
| OAuth scopes | video.list, user.info.basic, video.publish, video.upload, user.info.profile, user.info.stats |
| Token lifetime | 24 h; the port records 23 h (23 * 3600), but refresh is reactive (on the first expired call), not scheduled |
Server Environment Variables
required_env() returns exactly two variables:
#![allow(unused)]
fn main() {
&["TIKTOK_CLIENT_ID", "TIKTOK_CLIENT_SECRET"]
}
| Variable | Required | Default | Purpose |
|---|---|---|---|
TIKTOK_CLIENT_ID | Yes | none | Developer app Client key (16 characters in Postiz’s docs). |
TIKTOK_CLIENT_SECRET | Yes | none | Developer app Client secret (32 characters in Postiz’s docs). |
FRONTEND_URL | Yes, in practice | http://localhost:5000 | Builds the redirect URI; also decides whether redirectmeto.com is prefixed. Trailing / stripped. |
TIKTOK_BASE_URL | No | https://open.tiktokapis.com | API origin. Paths appended include /v2/oauth/token/, /v2/user/info/, /v2/post/publish/..., /v2/video/list/, /v2/video/query/, /v2/post/publish/status/fetch/, /v2/post/publish/creator_info/query/. |
TIKTOK_WEB_BASE_URL | No | https://www.tiktok.com | Authorize origin /v2/auth/authorize/ and the base of release URLs. |
required_env_alternatives() is not overridden.
rustiz providers --status | jq '.[] | select(.identifier=="tiktok")'
Step-by-Step Developer App Setup
1. Create the app
- Sign up / log in at https://developers.tiktok.com/.
- Create or join an organization (recommended; an individual-owned app is allowed but not recommended for production).
- Open https://developers.tiktok.com/apps → Connect an app, choose the owner organization.
- Fill in App details:
- app icon 1024×1024, JPEG/JPG/PNG, ≤ 5 MB;
- app name, category, description (shown on the consent screen);
- Platforms → Web and the official website URL.
- Add public Terms of Service and Privacy Policy URLs on HTTPS. Apps created after 9 Sep 2024 must verify these URL properties before review.
2. Add products
- Login Kit (Web): add the redirect URI (next section). This grants
user.info.basicby default. - Content Posting API: add it and enable the Direct Post configuration.
This is what makes
/video/init/and/content/init/usable. - Optionally Display API for
video.list(analytics/video queries).
3. Add the scopes
Under Scopes, add all six:
user.info.basic
user.info.profile
user.info.stats
video.list
video.publish
video.upload
video.publish is the Direct Post scope and requires approval. video.upload
is the inbox/draft scope. video.list powers analytics and the “missing posts”
lookup. user.info.stats is needed for follower/like counts.
The Postiz TikTok page lists
video.create. That is a legacy name; TikTok’s current scope reference hasvideo.list(read public videos), which is what the Rust code requests and checks. Request exactly the six scopes above.
4. Register the redirect URI
TikTok Login Kit requires absolute HTTPS, ≤ 512 characters, static (no query parameters), no fragment, at most 10 URIs. The Rust code builds:
{FRONTEND_URL}/integrations/social/tiktok
with one twist:
#![allow(unused)]
fn main() {
let prefix = if frontend.contains("https") { "" } else { "https://redirectmeto.com/" };
format!("{prefix}{frontend}/integrations/social/tiktok")
}
FRONTEND_URL=https://social.example.com→https://social.example.com/integrations/social/tiktok.FRONTEND_URL=http://localhost:5000→https://redirectmeto.com/http://localhost:5000/integrations/social/tiktok.redirectmeto.comis a public HTTPS redirector: TikTok sees an HTTPS URL, the human’s browser is then 302’d to localhost. The exact redirectmeto URL must be registered in Login Kit, and it is also what is sent to the token endpoint, so do not edit it.- The check is
frontend.contains("https"), a substring test on the whole URL, not a scheme check. A hostname containinghttpswould suppress the relay. - Localhost is not accepted by TikTok directly; use the relay or a public HTTPS tunnel/domain. Media, separately, must always be public HTTPS.
5. Verify URL properties (required for Content Posting)
In the app page → URL properties → Verify properties (Production mode):
- Verify the website, Terms and Privacy URLs.
- For photos and any
PULL_FROM_URLvideo, verify the domain or URL prefix that serves your media. Domain verification uses a DNS TXT signature; URL-prefix verification uses a signature file uploaded under the prefix. Subdomains are covered by a domain verification, but a different path is not covered by a URL prefix. - The media URL must be
https, must not redirect (3xx is invalid), and must stay reachable for up to one hour while TikTok downloads it.
If you skip this, the post fails with url_ownership_unverified or
photo_pull_failed.
6. Copy credentials
App page → Credentials:
TIKTOK_CLIENT_ID="your-client-key"
TIKTOK_CLIENT_SECRET="your-client-secret"
FRONTEND_URL="https://social.example.com"
Restart Rustiz and check rustiz providers --status.
Sandbox / testing mode
TikTok for Developers has a real sandbox:
- On the app page, toggle Production → Sandbox.
- Create Sandbox (up to 5 sandboxes), optionally cloning the production configuration. Edit app details/products in the sandbox and Apply changes.
- Under Target users, click Add account and log in with each TikTok account you want to test with (up to 10 accounts). The account owner agrees to the Developer Terms.
- Authorize each target user through Login Kit in the sandbox.
- URL verification in a sandbox is only required for the Content Posting API; the ToS/privacy/website checks are production-review requirements.
Sandbox limitations:
- TikTok’s sandbox is a restricted environment: expect Content Posting results
to remain private (
SELF_ONLY) rather than public, even after sandbox setup. It is for validating the integration/UX, not for public posting. - Sandbox configuration is separate; changes do not affect the live app.
- When satisfied, import the sandbox configuration into a production draft and submit it for review.
Production app review and the Content Posting audit
These are two separate gates:
- App review (Products/Scopes). Submit under App review with:
- a written explanation of how each product and scope is used;
- at least one demo video (up to 5, ≤ 50 MB each) showing the complete end-to-end flow, including the consent screen and the posting UX. Statuses: Draft → In review → Live (or Not approved with comments). Any later change must be submitted as a revision.
- Content Posting API audit. This is separate from app review and is what
lifts the unaudited restrictions:
- unaudited clients may only publish
SELF_ONLY; - every posting account must be private at posting time;
- at most 5 users may post through the client in any 24 h window;
- attempting a public level fails with
unaudited_client_can_only_post_to_private_accounts. Apply at https://developers.tiktok.com/application/content-posting-api. The audit checks the Content Sharing Guidelines: show which account receives the post, let the creator choose privacy/interaction settings, obtain consent before upload, and never add promotional watermarks.
- unaudited clients may only publish
Other limits that surface as errors: ~15–25 posts/account/day, at most 5
pending shares in any 24 h (spam_risk_too_many_pending_share), and a daily
active-user cap (reached_active_user_cap). The Rust provider maps all of these
to actionable BadBody messages.
Exact OAuth scopes and checks
#![allow(unused)]
fn main() {
info.scopes = vec![
"video.list",
"user.info.basic",
"video.publish",
"video.upload",
"user.info.profile",
"user.info.stats",
];
}
The authorization URL joins them with commas (scope=a,b,c), URL-encoded.
At token exchange check_scopes(&self.info.scopes, token["scope"]) requires all
six; commas or spaces are accepted in the response. If the user declines one
scope, the connection fails with not_enough_scopes even though TikTok may have
issued a usable token for the others.
The token request is form-encoded and includes:
client_key, client_secret, code, grant_type=authorization_code,
code_verifier=<state>, redirect_uri=<computed redirect>
There is no PKCE challenge in the authorization URL; code_verifier is sent
but is not tied to a challenge. TikTok ignores it today.
Connecting a Channel (CLI & MCP)
CLI
# Opens the TikTok consent page in the default browser.
rustiz channels connect tiktok --open
# Prints the URL to stderr and waits for the callback in-process.
rustiz channels connect tiktok
# Remote server: open the URL yourself, then poll.
rustiz channels connect-status <connectionId>
Do not pass --fields (it switches to the custom-credentials branch) or
--external-url (unsupported).
In local/in-process mode the CLI serves the callback on the FRONTEND_URL port
when that is http://localhost/http://127.0.0.1. With the default
http://localhost:5000 the login URL is the redirectmeto URL, but the browser
still ends up on localhost, so the CLI can finish the flow. A remote server must
serve the callback itself.
MCP
Start:
{ "provider": "tiktok" }
Result (or a URL elicitation):
{
"connected": false,
"pending": true,
"connectionId": "abc123",
"url": "https://www.tiktok.com/v2/auth/authorize/?client_key=...&redirect_uri=...",
"message": "A human must open this URL and log in. Then poll with connectionId or check integrationList."
}
Poll:
{ "connectionId": "abc123" }
is_between_steps is false: there is no channelSetupTool step. Once
connected, integrationList returns the channel id. The channel’s profile
field is the TikTok username, which is used to build post URLs.
Post Settings & Media Validation
settings_schema()
| Field | Type | Required | Behavior |
|---|---|---|---|
title | string (≤ 90) | No | Photo DIRECT_POST: sent as post_info.title, truncated to 90 chars. Photo UPLOAD: sent as the title, not truncated. Video: ignored — the post message becomes the title. Validation still enforces ≤ 90 even when it will be ignored. |
privacy_level | enum PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY | Yes | Only when DIRECT_POST. Defaults to PUBLIC_TO_EVERYONE at publish time if missing, but validation requires a value. |
duet | boolean | Yes | Videos only, DIRECT_POST only. Sent as disable_duet = !duet. Ignored for photos. |
stitch | boolean | Yes | Videos only, DIRECT_POST only. Sent as disable_stitch = !stitch. Ignored for photos. |
comment | boolean | Yes | Both media types, DIRECT_POST only. Sent as disable_comment = !comment. |
autoAddMusic | enum "yes", "no" | Yes | Photos only, DIRECT_POST only. "yes" sends auto_add_music: true (TikTok picks recommended music). Ignored for videos. |
brand_content_toggle | boolean | Yes | Paid-partnership disclosure; DIRECT_POST only. |
brand_organic_toggle | boolean | Yes | Own-business promotion disclosure; DIRECT_POST only. |
video_made_with_ai | boolean | No | Videos only, DIRECT_POST only. Sent as is_aigc. Not sent for photos (TikTok’s photo endpoint has a top-level is_aigc, which this port does not use). |
music | object {id, title?, artist?, image?, audio_volume?, video_volume?} | No | Declared for TikTok Business only. Legacy tiktok.rs never reads it. music.id must be non-empty if the object is present. |
location | object {id, name, address?} | No | Declared for TikTok Business only. Legacy tiktok.rs never reads it. id and name must be non-empty. |
content_posting_method | enum DIRECT_POST, UPLOAD | Yes | DIRECT_POST publishes; UPLOAD sends the media to the user’s TikTok inbox and TikTok ignores every other setting. The draft expires after 24 h if the user does not finish it. |
The schema also lists privacy_level, duet, stitch, comment,
autoAddMusic, brand_content_toggle, brand_organic_toggle,
content_posting_method in required, and has additionalProperties: true.
The schema is duplicated in
tiktok-business, somusicandlocationappear here even though only the Business provider implements them. Passing them totiktokis silently ignored.
validate_settings() rules
- The whole settings value must be a JSON object.
title, if non-null, must be a string of at most 90 characters.privacy_levelis required and must be one of the four enum values.duet,stitch,comment,brand_content_toggle,brand_organic_toggleare required booleans (missing or wrong type → error).video_made_with_ai, if present and non-null, must be a boolean.autoAddMusicis required and must be exactly"yes"or"no".content_posting_methodis required and must be"DIRECT_POST"or"UPLOAD".music, if present and non-null, must be an object with a non-empty stringid;title/artist/imagemust be strings when present;audio_volume/video_volumemust be numbers (or numeric strings) in0..=100.location, if present and non-null, must be an object with non-empty stringidandname;addressmust be a string when present.
The DTO requires the interaction booleans even for UPLOAD, where they are
discarded. Agents must send a complete settings object.
check_validity() rules
- No media →
"No video / images selected". - More than one media item and any video →
"Only pictures are supported when selecting multiple items". - More than one media item where the first is a video →
"You need one media". - All images: every image is measured; the first whose shorter side > 1080
produces
"Image N is WxH, TikTok allows a maximum of 1080px on the shorter side". This catches the all-or-nothingpicture_size_check_failedbefore publish.
There is no upper bound on the number of images for tiktok (the Business
provider checks 35). TikTok’s photo endpoint allows up to 35, so a larger
carousel fails at the API with invalid_params.
Media rules and publish behavior
Videos
- Detected by
has_extension(path, "mp4")— a case-insensitive substring check. A URL without.mp4is treated as a photo. - Local files and public URLs both work; the bytes are streamed from storage.
source_infoisFILE_UPLOADwithvideo_size,chunk_size,total_chunk_count:- ≤ 64 MiB → one chunk (
chunk_size = video_size, count1); -
64 MiB → 10 MiB chunks and
count = size / 10 MiB(floor). The final chunk absorbs the remainder and may exceedchunk_size(TikTok allows up to 128 MiB for the last part).
- ≤ 64 MiB → one chunk (
- Upload
PUTrequests sendContent-Type: video/mp4,Content-LengthandContent-Range: bytes start-end/size; 200/201/206 are accepted. - A TikTok rejection (
BadBody) fails the post. A network error during upload is treated as ambiguous: the provider returnspendingand lets the status endpoint decide, because TikTok may already have all the bytes. video_cover_timestamp_msis sent when the media carries athumbnailTimestamp.- TikTok’s documented limits are a maximum duration of 3, 5 or 10 minutes depending on the creator, 23–60 fps, 360–4096 px per side and ≤ 4 GB; the Rust port does not validate these.
Photos
- Photo posts only support
PULL_FROM_URL.source_info.photo_imagesis the list of media paths as stored (typically your public upload/CDN URLs),photo_cover_index: 0,source: "PULL_FROM_URL". post_modeisDIRECT_POSTorMEDIA_UPLOAD(note: not"UPLOAD").- The endpoint is always
/v2/post/publish/content/init/. - Requirements: WebP or JPEG, ≤ 20 MB per image, shorter side ≤ 1080 px, up to
35 images, public HTTPS and ownership-verified. The engine’s
convert_to_jpeg = truerewrites PNG uploads to JPEG first. is_aigcis not sent for photos.
Endpoint selection
content_posting_method | Media | Endpoint |
|---|---|---|
DIRECT_POST | video | POST /v2/post/publish/video/init/ |
DIRECT_POST | photo | POST /v2/post/publish/content/init/ |
UPLOAD | video | POST /v2/post/publish/inbox/video/init/ |
UPLOAD | photo | POST /v2/post/publish/content/init/ (post_mode: MEDIA_UPLOAD) |
What the body actually sends
DIRECT_POST video:
{
"post_info": {
"title": "<the whole post message>",
"privacy_level": "PUBLIC_TO_EVERYONE",
"disable_duet": false,
"disable_comment": false,
"disable_stitch": true,
"is_aigc": false,
"brand_content_toggle": false,
"brand_organic_toggle": true,
"video_cover_timestamp_ms": 1234
},
"source_info": { "source": "FILE_UPLOAD", "video_size": 123, "chunk_size": 123, "total_chunk_count": 1 }
}
DIRECT_POST photo:
{
"post_info": {
"title": "<settings.title, 90 chars>",
"description": "<the whole post message>",
"privacy_level": "PUBLIC_TO_EVERYONE",
"disable_comment": false,
"auto_add_music": true,
"brand_content_toggle": false,
"brand_organic_toggle": true
},
"source_info": {
"source": "PULL_FROM_URL",
"photo_cover_index": 0,
"photo_images": ["https://cdn.example/a.jpg", "https://cdn.example/b.jpg"]
},
"post_mode": "DIRECT_POST",
"media_type": "PHOTO"
}
UPLOAD sends only title (video) or title + description (photo), plus the
source info. Every privacy/interaction/brand/music setting is discarded by
TikTok.
Status, release resolution and analytics
check_post_status makes one read-only call to
POST /v2/post/publish/status/fetch/ with { "publish_id": ... }:
| Status | Result |
|---|---|
SEND_TO_USER_INBOX | Completed with post_id = "missing" and release_url = https://www.tiktok.com/messages?lang=en#<publish_id>. The draft has no public id until the user publishes it. |
PUBLISH_COMPLETE | If the raw body contains a numeric publicaly_available_post_id (TikTok’s typo), that id and the profile URL with /video/<id> or /photo/<id>; otherwise the publish id and https://www.tiktok.com/@<username>. The public id only appears after moderation. |
FAILED | BadBody, with handle_errors mapping known reasons. |
anything else (including PROCESSING_*) | Pending — the engine polls again. |
resolve_release_id handles a channel whose release id was "missing":
- the publish id is read from the URL fragment (
.../messages#<publish_id>); - only ids containing
_pub_or_inbox_are considered; - for non-drafts,
SELF_ONLYandMUTUAL_FOLLOW_FRIENDSimmediately returnUnavailable(TikTok never exposes a public id for non-public posts); - if the status has a public id →
Resolved; draft with none →None; non-draft with none →Pending.
release_url() queries /v2/video/query/?fields=id,share_url and strips the
query string from share_url — it carries TikTok tracking parameters, including
the app’s client key.
analytics():
/v2/user/info/?fields=follower_count,following_count,likes_count,video_count→ Followers, Following, Total Likes, Videos;- the last 20 videos from
/v2/video/list/are queried withid,like_count,comment_count,share_count,view_countand summed into Views, Recent Likes, Recent Comments, Recent Shares.
post_analytics() queries the same fields for one video and returns Views,
Likes, Comments, Shares. Analytics errors are swallowed (logged, empty
array), so a missing scope looks like “no data”, not an error.
missing() returns the last 20 videos as (id, cover_image_url) so a
"missing" release can be matched manually.
Plugs
Neither run_plug() nor run_post_plug() is overridden. Both use the trait
defaults and return Unsupported for every method. TikTok has no auto-repost or
auto-comment plug in this port (the upstream provider does not define them
either).
Implementation Notes & Gotchas
- The redirect URI is a third-party dependency when
FRONTEND_URLis not HTTPS.https://redirectmeto.com/is in the live login path. It must be reachable by the human’s browser, and the exact relayed URL must be registered in Login Kit. Ifredirectmeto.comis down, local/HTTP connects fail. Set an HTTPSFRONTEND_URLto remove the dependency. - Redirect URI substring check.
frontend.contains("https")decides the relay; it is not a URL parse. AFRONTEND_URLsuch ashttp://https.localhost:5000would skip the relay and TikTok would reject the URI. expires_inis hard-coded to 23 h. TikTok access tokens live 24 h. Sincerefresh_cronis false, there is no proactive refresh; the first API call after expiry fails withaccess_token_invalid, the provider maps it toRefreshToken, the engine refreshes and retries. Refresh tokens rotate; the new one is stored.open_idis normalized by removing dashes. TikTok open ids are app-scoped: the same TikTok account connected through a different client key gets a differentopen_id, so it becomes a separate Rustiz channel. Reusing the same app is required for continuity.- The open id is also the channel identity for analytics. Changing apps means reconnecting; the old channel’s analytics will not match.
video.listis requested even for posting. If TikTok grantsvideo.publish/video.uploadbut notvideo.list,check_scopesrefuses the connection. This is deliberate (analytics andmissingneed it), but it makes scope approval stricter than a publish-only integration.settings.titleis mostly a photo field. For videos the title is the post message, so a >90-chartitlesetting fails validation for nothing. For photos it is truncated to 90, while the description (the message) is capped bymax_length = 2000, below TikTok’s 4000.musicandlocationare dead settings on this provider. They exist in the shared schema buttiktok.rsnever reads them. Usetiktok-businessfor commercial music and location tags.autoAddMusiconly applies to photos. For videos the field is validated but never sent.video_made_with_aionly applies to videos. The photo endpoint accepts a top-levelis_aigc, which this port does not send.UPLOADis not publishing. It creates an inbox draft that the user must finish in the TikTok app within 24 h. The status becomesSEND_TO_USER_INBOX, the release id is"missing", and settings are ignored. Use it only when the user explicitly asks to review in the app.- Photo posts are URL pulls, not uploads. A local
UPLOAD_DIRECTORYfile path is not readable by TikTok. Your media must be served over public HTTPS from a verified domain/prefix. This is the most common cause ofphoto_pull_failed/url_ownership_unverified. - Validation is shallow. No file size, duration, fps, resolution (video) or
MIME check beyond the
.mp4substring and the photo shorter-side rule. TikTok rejects the publish later withfile_format_check_failed,duration_check_failed,frame_rate_check_failed, etc.;handle_errorsmaps those to readable messages. spam_risk_too_many_pending_sharemeans 5 pending drafts in 24 h. The provider’s message tells the user to open the TikTok inbox and try again after 24 h.reached_active_user_cap(unaudited daily user cap) is mapped toDisconnect, which cannot actually help; reconnecting does not reset the cap.post_urldepends onintegration.profile. It must contain the TikTok username (set fromuser/infoat connect time). If it is empty, release URLs becomehttps://www.tiktok.com/@/video/....- The public-id regex relies on a TikTok typo. It matches
"publicaly_available_post_id"(missing the secondl) in the raw response, exactly as TikTok emits it, because JSON numbers in that array can exceed2^53and lose precision if parsed normally. - Error identifier typo. The
FAILEDstatus branch usesProviderError::bad_body("titok-error-upload", ...)(missing at), while the upload path uses"tiktok-error-upload". Cosmetic, but it changes the error identifier in logs. - Status polling is single-shot by design. No loops or sleeps in
check_post_status; the engine’s durable timer re-runs it. A stuck or retried check can never re-run the publish, which prevents duplicates. The blockingpost()path loops 27 times / 8 minutes with 20 s sleeps. - Analytics swallows every non-token error. Missing scopes, rate limits and
transient failures all become
[]. Check the logs if the analytics panel is empty. - Creator info is never queried before publishing.
post_pendingsends the configuredprivacy_leveldirectly; it does not call/v2/post/publish/creator_info/query/and does not check the account’sprivacy_level_options,comment_disabled,duet_disabledormax_video_post_duration_sec. TikTok’s Direct Post UX guidelines require that query, and the API returnsprivacy_level_option_mismatchwhen the chosen level is not offered. The only caller ofcreator_info/queryis the unexposedmax_video_length(). This is also a common reason a Content Posting audit is rejected. max_video_length()is not exposed. It calls/v2/post/publish/creator_info/query/and returnsmax_video_post_duration_sec, but TikTok does not overridefunctions(), sotriggerToolcannot invoke it. It is only reachable from Rust code.- Length checks count Unicode scalar values, not UTF-16 code units. The
engine’s
max_lengthcheck andtake_charscountchars, while TikTok’s photo title limit is 90 UTF-16 runes. A title made of emoji (2 code units each) can passvalidate_settingsat 90 characters and still exceed TikTok’s 90-rune limit. The same applies to the 2000-character message check versus TikTok’s 2200/4000 UTF-16 limits. - Rate limits. The Content Posting API allows 6 requests per minute per user access token; status checks are a separate, higher per-endpoint limit (commonly 30/min). The blocking path polls every 20 s (3/min) and stays under both. TikTok also caps daily posts per account (~15–25) and pending drafts (5 per 24 h).
convert_to_jpegonly rewrites PNGs.prepare_post_mediaconverts PNG files to JPEG and re-stores them whenUPLOAD_DIRECTORYis set; other formats are passed through. TikTok’s accepted photo formats are WebP and JPEG, so a GIF photo post will fail at TikTok even though validation accepts it.
References
- Register an app: https://developers.tiktok.com/doc/getting-started-create-an-app
- Add a sandbox / target users: https://developers.tiktok.com/doc/add-a-sandbox
- Scopes reference (video.list / video.publish / video.upload / user.info.*): https://developers.tiktok.com/doc/tiktok-api-scopes
- Login Kit for Web (redirect URI rules, authorize URL, token exchange): https://developers.tiktok.com/doc/login-kit-web
- Content Posting API — Direct Post: https://developers.tiktok.com/doc/content-posting-api-get-started
- Content Posting API — Photo: https://developers.tiktok.com/doc/content-posting-api-reference-photo-post
- Media transfer guide (chunks, PULL_FROM_URL, URL ownership, restrictions): https://developers.tiktok.com/doc/content-posting-api-media-transfer-guide
- Content Posting API audit: https://developers.tiktok.com/application/content-posting-api
- App review FAQ: https://developers.tiktok.com/doc/getting-started-faq
- Postiz TikTok guide: https://docs.postiz.com/self-host/providers/tiktok
- Postiz posting rules: https://docs.postiz.com/general/platforms/overview