PM3K gives you two main ways to work with TikTok:
PULL_FROM_URL, not file upload) and then check publish status by publish_id. For public low-code automation, the canonical endpoints are direct_post and status.Base URLs (production):
POST https://pm3k.org/api/tiktok/direct_postPOST https://pm3k.org/api/tiktok/status
All responses are JSON with
Content-Type: application/json; charset=UTF-8.
Dashboard scheduling is a separate PM3K session flow: the dashboard creates, reads, and deletes scheduled jobs, while actual scheduled execution happens internally in PM3K.
pm3k.org.
PM3K stores your TikTok open_id and plan in internal storage (KV).
Free plan basics:
Paid plans increase key lifetime, storage TTL and upload limits, but TikTok anti-spam limits stay the same.
Important:
Use this header for all API calls:
Authorization: Bearer YOUR_API_KEY_HERE
source_url)You always pass a URL to the video file, not the file itself. Two common options:
https://files.pm3k.org/uploads/<open_id>/<date>/<id>.mp4
source_url in direct_post.Available on active plans (Free / Monthly) after verification.
Requirements for your own domain:
source_url must return the media file directly. PM3K does not follow HTTP redirects in direct_post.
If the source responds with a redirect (3xx), the request fails with redirect_not_allowed.
If PM3K cannot fetch the file for another reason (for example, timeout, network error, 4xx/5xx response, or upstream read failure), the request may fail with fetch_error.
File size limit: Free plan is typically around 100 MB, paid plans around 200 MB.
Larger files typically fail with too_large or too_large_need_proxy.
Domain verification flow:
media.example.com).https://your-domain.com/pm3k-verify.txt).
source_url in the API.
Even for a verified domain, PM3K will still pull your video through its own storage first (temporary copy). PM3K may use trusted media metadata from that copy (for example size + ETag when available) for duplicate / anti-spam checks. TikTok does not fetch directly from your domain via this API; PM3K sits in the middle for better safety and de-duplication.
POST /api/tiktok/direct_postCreate a TikTok post using PULL_FROM_URL.
URL
POST https://pm3k.org/api/tiktok/direct_post
Headers
Authorization: Bearer YOUR_API_KEY_HERE
Content-Type: application/json
Body (basic)
{
"source_url": "https://files.pm3k.org/uploads/.../video.mp4",
"title": "My caption without links",
"privacy": "PUBLIC_TO_EVERYONE",
"disable_comment": false,
"disable_duet": false,
"disable_stitch": false
}
You can send a PM3K direct post request in n8n without pasting a full raw JSON payload. Filling the body field by field is supported.
The important part is not how you enter the values, but what the final HTTP request looks like. PM3K expects a JSON request body, so the node must still send a real JSON object even if you entered the values one row at a time in the UI.
Recommended HTTP Request setup
POSThttps://pm3k.org/api/tiktok/direct_postAuthorization: Bearer YOUR_API_KEYContent-Type: application/jsonDo not send the request as:
form-datax-www-form-urlencodedImportant: field-by-field is still JSON
When you add body values one by one in n8n, the final request must still be a JSON object.
n8n note: in raw JSON mode, string values with double quotes may produce invalid JSON. Use field-by-field body input or JSON.stringify(...) for string expressions.
Good result
{
"source_url": "https://files.pm3k.org/uploads/.../video.mp4",
"privacy": "PUBLIC_TO_EVERYONE",
"title": "Caption"
}
Bad result
source_url=https://files.pm3k.org/uploads/.../video.mp4&privacy=PUBLIC_TO_EVERYONE
The second example is not a JSON body and may lead to 400 invalid_request.
Video post: field-by-field example
For a normal video post, the safest minimum setup is:
source_url → direct public video URLprivacy → for example PUBLIC_TO_EVERYONEtitle (optional)disable_comment (optional)disable_duet (optional)disable_stitch (optional)Example final JSON body
{
"source_url": "https://files.pm3k.org/uploads/.../video.mp4",
"title": "My caption",
"privacy": "PUBLIC_TO_EVERYONE",
"disable_comment": false,
"disable_duet": false,
"disable_stitch": false
}
privacy is required. title is optional.
disable_comment, disable_duet, and
disable_stitch are optional interaction fields.
Photo post: field-by-field example
For photo mode, use:
media_type → PHOTOphoto_images → array of direct public image URLsphoto_cover_index → 0 or another valid indexprivacytitle (optional)Example final JSON body
{
"media_type": "PHOTO",
"photo_images": [
"https://files.pm3k.org/uploads/.../a.jpg",
"https://files.pm3k.org/uploads/.../b.png"
],
"photo_cover_index": 0,
"title": "Caption for a photo post",
"privacy": "PUBLIC_TO_EVERYONE"
}
Important: photo_images must be an array, not a single string.
Correct
{
"photo_images": ["https://files.pm3k.org/uploads/.../a.jpg"]
}
Incorrect
{
"photo_images": "https://files.pm3k.org/uploads/.../a.jpg"
}
Branded content fields
If you enable ad / disclosure mode, the current recommended fields are
brand_organic_toggle and brand_content_toggle.
When used, send them as real booleans.
{
"source_url": "https://files.pm3k.org/uploads/.../video.mp4",
"privacy": "PUBLIC_TO_EVERYONE",
"is_ad": true,
"brand_organic_toggle": true,
"brand_content_toggle": false
}
Do not send branded content booleans as strings like:
{
"brand_organic_toggle": "true"
}
Use:
{
"brand_organic_toggle": true
}
Common mistakes when filling the node row by row
privacy.
{
"source_url": "https://files.pm3k.org/uploads/.../video.mp4"
}
{
"source_url": "https://files.pm3k.org/uploads/.../video.mp4",
"privacy": "PUBLIC_TO_EVERYONE"
}
photo_images as text instead of an array.
{
"media_type": "PHOTO",
"photo_images": "https://files.pm3k.org/uploads/.../a.jpg",
"photo_cover_index": 0,
"privacy": "PUBLIC_TO_EVERYONE"
}
{
"media_type": "PHOTO",
"photo_images": ["https://files.pm3k.org/uploads/.../a.jpg"],
"photo_cover_index": 0,
"privacy": "PUBLIC_TO_EVERYONE"
}
{
"is_ad": true,
"brand_organic_toggle": "false",
"brand_content_toggle": "true"
}
{
"is_ad": true,
"brand_organic_toggle": false,
"brand_content_toggle": true
}
Quick checklist before you run the node
POST.https://pm3k.org/api/tiktok/direct_post.Authorization: Bearer ... header is present.Content-Type: application/json header is present.privacy is present.photo_images is an array for photo posts.Use this endpoint from n8n, Make, or your own scripts to send a prepared vertical video directly to TikTok.
{
"source_url": "https://files.pm3k.org/abc123.mp4",
"title": "Your TikTok caption here",
"privacy": "PUBLIC_TO_EVERYONE",
"disable_comment": false,
"disable_duet": false,
"disable_stitch": false,
"is_ad": false,
"brand_organic_toggle": false,
"brand_content_toggle": false,
"brand": ""
}
source_url (string, required for video mode) — HTTPS URL of your vertical MP4/MOV.
The URL must be either from PM3K storage or from a verified custom domain.
title (string, optional) — TikTok caption, up to ~2200 characters.
privacy (string, required) — TikTok privacy level. Recommended values:
PUBLIC_TO_EVERYONEMUTUAL_FOLLOW_FRIENDSFOLLOWER_OF_CREATORSELF_ONLY"public", "friends", "followers", "private" —
PM3K will normalize them to the correct TikTok enum.
If privacy is missing, PM3K rejects the request with 400 invalid_request.
If privacy is present but the value is unknown, PM3K falls back to SELF_ONLY.
For photo mode, send photo_images and photo_cover_index.
title stays optional there too, while privacy is still required.
If you omit title, PM3K does not invent a caption for the request.
This also applies to new scheduled jobs created through the dashboard.
Empty captions are allowed. Empty title values are not checked against
caption duplicate or caption-spread rules. Non-empty captions still are.
Important: The video must be vertical (9:16).
PM3K does not re-encode, rotate or compress your video — the exact file you provide
is uploaded to TikTok as-is. Files that are not vertical may be rejected with a
bad_request error.
Duration limits: TikTok may reject videos that exceed the maximum duration allowed for your account. PM3K does not override this limit — TikTok enforces it on upload.
PM3K does not compress, modify or re-encode your video. The file hosted at
source_url is the file TikTok receives.
disable_comment (boolean, optional) — if true,
comments are turned off for this video.
disable_duet (boolean, optional) — if true,
duets are disabled.
disable_stitch (boolean, optional) — if true,
stitches are disabled.
These fields are optional but recommended if you use PM3K for commercial posts. They are designed to reflect TikTok’s ad / branded content policies.
is_ad (boolean, optional) — turn on ad / disclosure mode for this post.
brand_organic_toggle (boolean, optional) — you promote your own brand, product, or service.
brand_content_toggle (boolean, optional) — you promote a third-party brand
(branded content collaboration).
brand (string, optional) — brand or advertiser name.
When you use branded content, provide a clear brand name here.
If is_ad is true, you must also choose a disclosure type:
send brand_organic_toggle for your own brand or
brand_content_toggle for third-party branded content.
If is_ad is true but neither disclosure type is selected,
PM3K rejects the request with 400 invalid_request.
PM3K also accepts the legacy aliases ad_your_brand and
ad_branded_content, but the current dashboard export uses
brand_organic_toggle and brand_content_toggle.
You don’t need to send any technical metadata (file size, ETag, etc.). PM3K reads everything it needs from its own storage and uses internal checks to block duplicate uploads and spammy captions.
Photo Mode lets you publish a TikTok photo carousel (slides) using the same
/api/tiktok/direct_post endpoint. PM3K always sends
PULL_FROM_URL to TikTok and uses PM3K storage URLs for final delivery.
{
"media_type": "PHOTO",
"photo_images": [
"https://files.pm3k.org/uploads/<open_id>/albums/<album_id>/a.jpg",
"https://files.pm3k.org/uploads/<open_id>/albums/<album_id>/b.webp"
],
"photo_cover_index": 0,
"auto_add_music": true,
"title": "Caption for a photo post",
"privacy": "PUBLIC_TO_EVERYONE",
"disable_comment": false,
"disable_duet": false,
"disable_stitch": false,
"is_ad": false,
"brand_organic_toggle": false,
"brand_content_toggle": false,
"brand": ""
}
In the dashboard, Photo mode shows a live JSON payload as uploads complete. If you click Upload only (get link), PM3K upgrades retention for the album.
POST https://pm3k.org/api/album/retain
{ "album_id": "..." }
Use the resulting JSON in n8n/Make with /api/tiktok/direct_post.
Use a JSON body there as well. Do not switch to form-data or
x-www-form-urlencoded, and keep privacy in the final payload.
photo_cover_index is required and 0-based.Content-Length (otherwise content_length_required).Posting limits are unchanged: max 1 post/hour and 10 posts/day total across photo/video posts for API usage. Dashboard upload limits remain plan-based (photo albums count as 1 upload).
| Error | Meaning |
|---|---|
too_few_images | Missing images or empty array. |
too_many_images | More than 35 images. |
cover_out_of_range | Invalid cover index. |
unsupported_image_type | Image type is not accepted for this flow. External images may be JPEG/WebP/PNG; PM3K-hosted photo URLs must be JPEG/WebP. |
image_too_large | Image exceeds 20 MB. |
image_too_large_px | Image exceeds 1080px on one side. |
content_length_required | Missing Content-Length for external URLs. |
domain_not_verified | External domain not verified. |
domain_mismatch | External URL host does not match the verified domain on this PM3K account. |
image_convert_failed | PM3K could not prepare the external image into a supported upload format. |
{
"ok": true,
"publish_id": "7153xxxxxxxxxxxxxxxxxx"
}
You will use publish_id with /api/tiktok/status to check the publish state.
POST /api/tiktok/status
Check current TikTok publish status for a post created by
direct_post.
URL
POST https://pm3k.org/api/tiktok/status
Headers
Authorization: Bearer YOUR_API_KEY_HERE
Content-Type: application/json
Body
{
"publish_id": "7153xxxxxxxxxxxxxxxxxx"
}
{
"ok": true,
"status": "PUBLISHING",
"state": "pending",
"fail_reason": null,
"raw": {
"...": "TikTok original response"
}
}
Fields:
status – raw TikTok status (e.g. PUBLISHING, PUBLISH_COMPLETE, PUBLISH_FAILED).state – simplified state:
pending – still processing.done – published successfully.failed – TikTok failed to publish.fail_reason – TikTok fail reason, if any.raw – full TikTok JSON for debugging.
For /status, PM3K applies a per-minute limit of
20 calls per minute per TikTok account.
PM3K also enforces a hard limit of 40 calls per hour per open_id. The first time you exceed this limit in a 1-hour window, status polling enters a 1-hour cooldown. A new successful post clears this cooldown earlier.
If exceeded, you receive:
{
"ok": false,
"error": "rate_limit_status",
"message": "Too many /status calls for this account, please slow down.",
"retry_after": "2025-01-01T12:00:00.000Z"
}
HTTP status: 429.
Hourly cooldown response:
{
"ok": false,
"error": "status_cooldown",
"message": "Status polling is temporarily paused for this account after too many /status checks.",
"cooldown": "temporary_backoff",
"retry_after_seconds": 3600,
"retry_after": "2025-01-01T12:30:00.000Z",
"cooldownUntil": "2025-01-01T12:30:00.000Z"
}
For status_cooldown, cooldownUntil is the canonical
absolute deadline, retry_after_seconds is the canonical duration field,
and plain retry_after is a deprecated legacy alias kept for backward compatibility.
If the cooldown was cleared early by a successful post, but the same hour window is still capped:
{
"ok": false,
"error": "hourly_limit_status",
"retry_after": "2025-01-01T13:00:00.000Z",
"resetAt": "2025-01-01T13:00:00.000Z"
}
Recommended polling pattern:
You can manage TikTok posts directly from the PM3K dashboard. No extra public API endpoints are required for this dashboard flow.
source_url in your own low-code workflows
(for example, call /api/tiktok/direct_post from n8n or Make, and then check /api/tiktok/status).
n8n/Make delays are controlled by your workflow. PM3K internal scheduling
is only available through the dashboard.
Captions with links are blocked. If title contains
http://, https:// or www., PM3K rejects the request:
{
"ok": false,
"error": "links_forbidden",
"message": "Links are not allowed in TikTok captions for this service."
}
HTTP status: 400.
PM3K maintains a forbidden-words list (STOP_WORDS) covering, for example:
If your caption contains one of these words or phrases, PM3K returns:
{
"ok": false,
"error": "forbidden_word",
"message": "Caption contains forbidden content: \"...\"."
}
HTTP status: 400. Such captions are not sent to TikTok at all.
PM3K avoids posting the same video repeatedly for the same account with two layers of protection.
source_url.If one of these video duplicate checks blocks the request, PM3K returns:
{
"ok": false,
"error": "video_duplicate",
"message": "This video URL was already used for this TikTok account recently."
}
HTTP status: 400. For backward compatibility, the public error code and message text stay the same. Client-supplied media metadata alone does not trigger the longer fingerprint-based block. The same duplicate checks also apply when a scheduled video job is executed.
Two layers apply to non-empty captions. Empty captions are not checked against these two caption-reuse rules.
{
"ok": false,
"error": "caption_duplicate_account",
"message": "This caption was already used on this TikTok account recently."
}
{
"ok": false,
"error": "caption_global_spam",
"message": "This caption is being reused across too many accounts in a short time."
}
Both responses use HTTP status 400.
Per TikTok account (per creator_id):
If you exceed these limits:
{
"ok": false,
"error": "daily_limit",
"message": "Daily TikTok posting limit (10/day) reached."
}
or
{
"ok": false,
"error": "interval_limit",
"message": "You must wait about N more minutes before next TikTok post."
}
HTTP status: 429.
If TikTok itself returns HTTP 429 (rate limit) for your account, PM3K
sets a cooldown for this creator_id (around 3 hours).
During cooldown you will see:
{
"ok": false,
"error": "tiktok_rate_limit",
"message": "TikTok returned HTTP 429 (rate limit) for this account; posting is temporarily paused.",
"retry_after_seconds": 10800
}
or subsequent calls will return:
{
"ok": false,
"error": "tiktok_cooldown",
"message": "TikTok recently returned HTTP 429 for this account; posting is temporarily paused."
}
Both use HTTP status 429. Do not auto-retry during cooldown.
Canonical public error values for
/api/tiktok/direct_post
and /api/tiktok/status:
unauthorized (401) – missing or malformed authentication. For /status, a Bearer API key is required. For direct_post, PM3K accepts either a Bearer API key or a dashboard session cookie.invalid_api_key (401) – key not found or revoked.api_key_expired (401) – Plan window expired for the public /api/tiktok/* automation surface such as /api/tiktok/direct_post and /api/tiktok/status. Message: "Your plan has expired. Please upgrade to a paid plan."plan_required (401) – no active PM3K plan is attached to this account.tiktok_not_connected (401) – TikTok account is not connected for this PM3K user; reconnect in dashboard.tiktok_reauth_required (401) – TikTok refresh token expired; reconnect TikTok in dashboard.tiktok_refresh_failed (401) – PM3K could not refresh the TikTok access token; reconnect TikTok in dashboard.bad_request (400) – invalid JSON or missing required request fields (for example source_url, publish_id, invalid photo item, or invalid URL format).invalid_request (400) – validation error in the JSON body (for example missing privacy, invalid branded-content toggle types, or is_ad: true without a disclosure type).domain_not_verified (403) – external media host is not verified for this PM3K account.domain_mismatch (403) – external media URL host does not match the verified domain on this account.fetch_error (400) – PM3K could not fetch or prepare the external media URL for reasons other than an HTTP redirect (for example timeout, network failure, 4xx/5xx response, or upstream read/stream failure).redirect_not_allowed (400) – external media URL responded with an HTTP redirect (3xx); PM3K does not follow redirects in direct_post.content_length_required (400) – external media URL must provide a valid Content-Length header.too_large (413) – media file is too large for the current plan or fetch path.too_large_need_proxy (413) – media is larger than the direct fetch limit for this plan; use a smaller file or PM3K-hosted media.too_few_images (400) – photo post did not include any valid images.too_many_images (400) – photo post included too many images.cover_out_of_range (400) – photo_cover_index is outside the uploaded image range.unsupported_image_type (400) – image type is not accepted for this photo flow.image_too_large (400) – image exceeds the supported size limit.image_too_large_px (400) – image exceeds the supported pixel dimensions.image_convert_failed (400) – PM3K could not convert the external image into a supported upload format.links_forbidden (400) – caption contains a link.forbidden_word (400) – caption hits forbidden content; not sent to TikTok.video_duplicate (400) – duplicate video protection triggered for this account (same source URL in a very short recent window, or an additional trusted media fingerprint match within 30 days when available).caption_duplicate_account (400) – same non-empty caption reused on this account within 7 days.caption_global_spam (400) – request rejected because the same non-empty caption was reused on too many accounts in 24 hours.daily_limit (429) – 10 posts/day limit reached.interval_limit (429) – 1-hour interval not respected.tiktok_cooldown (429) – PM3K is temporarily pausing new posts because TikTok recently returned HTTP 429 for this account.tiktok_rate_limit (429) – TikTok returned HTTP 429 during post init; wait for cooldown before retrying.rate_limit_status (429) – too many /status calls; see retry_after.status_cooldown (429) – temporary 1-hour pause after too many /status calls; use cooldownUntil as the canonical deadline and retry_after_seconds as the canonical duration. Plain retry_after remains as a deprecated legacy alias for backward compatibility. A successful new post clears the cooldown earlier.hourly_limit_status (429) – hourly cap reached; wait until resetAt.tiktok_error (400) – fallback bucket when TikTok returned an upstream error and PM3K does not expose a more specific top-level code. Inspect raw.error.code passthrough (400) – in some upstream failures, PM3K returns TikTok's own error.code directly as the top-level error value.proxy_error (502) – PM3K Pages could not reach the TikTok backend at all. This is only the Pages -> backend unreachable case, not a general media-fetch bucket.Rare internal 5xx responses or upstream failures without a stable JSON error body may still happen, but they are not part of the supported public error contract for these endpoints.
POSThttps://pm3k.org/api/tiktok/direct_postAuthorization: Bearer <your key>, Content-Type: application/jsonBody (JSON):
{
"source_url": "https://files.pm3k.org/uploads/.../video.mp4",
"title": "My caption without links",
"privacy": "PUBLIC_TO_EVERYONE",
"disable_comment": false,
"disable_duet": false,
"disable_stitch": false
}
Send this node as a real JSON body. Do not build the request as
form-data or x-www-form-urlencoded.
privacy is required, photo posts must use photo_images,
and branded-content toggles should be sent as real booleans.
Store ok and publish_id from the response. If
ok is false, log error and message and stop the flow.
In n8n, always add an IF node right after this step:
if ok == true → continue to the status loop;
if ok == false → stop the flow and log error/message.
Do not try to access publish_id when ok is false.
POST https://pm3k.org/api/tiktok/status with:
{
"publish_id": "{{$json["publish_id"]}}"
}
state == "done" → success.state == "failed" → log fail_reason and stop.pending) → wait and repeat, up to a max attempts limit.