# TikTok user posts

`POST https://api.endless.app/social/tiktok_user_posts`

Source: https://endless.app/docs/api/social/tiktok-user-posts

Get a TikTok account's videos with engagement counts (plays, likes, comments, shares, saves), hashtags, and the sound each video uses (`music.id` feeds `tiktok_music_posts`). For a date range, pass `since` and the walk pages back until it reaches that date — do NOT try to cover a range with a big `max_items`.

For a date range pass `since` and `until` rather than a large item cap — plus `tz`, the user's timezone, so the days are cut on THEIR calendar. The walk pages back until it reaches that date, and the tool returns a `totals` block (posts, views, likes, comments, engagement_rate) computed over exactly that window. Quote those numbers; never add up the rows yourself. Before reporting a total, read these four fields — each one exists because ignoring it produced a wrong report:
- `partial: true` — the walk stopped early, so this is a FLOOR, not a measurement. Say so, or re-run; never present it as the period's total.
- `overlaps` — names a sibling feed returning the SAME pieces. Report one feed or the other; adding both counts every piece twice.
- `views_metric` — which upstream number `views` came from. Two feeds on one platform can report different values for the same piece, so only compare or combine totals whose `views_metric` matches.
- `measured_at` — when it was read. Live counts move fast; state it whenever you report a total, and never explain a difference between two runs as a mistake before checking whether the pieces simply gained views between them.

COVERING MORE THAN ONE ACCOUNT OR FEED: never add these totals in your head. Each result carries a `receipt` — a TSV block with the same five columns on every platform. Paste the receipts into `run_code` and total them there: dedupe on (platform, id) so a piece returned by two feeds is counted once, keep an empty field as missing rather than 0, and only add views across receipts whose `views_metric` matches. Report the code's number, not a retyped one, and hand back the per-piece list so the total can be checked by hand. For comparing several accounts at once, prefer `tiktok_trend_scan`.

## Authentication

- `bearerAuth`: HTTP bearer (JWT)

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Endless-Api-Version` | header | string | no | Pin the caller to an API major (`1`). Omit to accept the current version. A major this deployment cannot serve is refused with 400 `unsupported_api_version`. |

## Request body

Required.

Content type: `application/json` — object

- `organization_id` (string, required)
- `max_credits` (integer): Refuse the call when its quoted ceiling exceeds this many credits.
- `tool_call_id` (string | null): The tool call behind this read, for the ledger. Every read that runs is billed, a repeated id too.
- `conversation_id` (string (uuid) | null): The chat this call belongs to, for cost attribution.
- `message_id` (string (uuid) | null): The chat turn this call belongs to, for cost attribution.
- `input` (object, required)
  - `username` (string, required): TikTok handle, without the leading @
  - `max_items` (integer): How many posts to return (max 70). Ignored when `since` is set.
  - `sort` ("recent" | "popular"): 'recent' = newest first (monitoring); 'popular' = the account's top videos.
  - `since` (string): Page back until posts from this date (YYYY-MM-DD) are reached. Use this when the requested source content is bounded by publication date, instead of raising the item cap. For creative references or account voice, omit date filters unless the user limits the source period; a planning deadline does not limit when references were published. Never pass a far-past date to mean 'everything', and omit it when the user asks for no date filter: every page walked back costs credits.
  - `until` (string): End of the window (YYYY-MM-DD, inclusive). Pair with `since` to get exact `totals` for a date range instead of summing rows yourself.
  - `tz` (string): IANA timezone the date window is cut in (e.g. 'America/Argentina/Buenos_Aires'). Pass the user's timezone from the system prompt whenever you pass `since`/`until` — days cut in the wrong zone shift every piece published late in the evening into the next day. Defaults to UTC.

## Responses

### 200 OK

Content type: `application/json` — object

- `operation` ("instagram_profile" | "instagram_user_posts" | "instagram_user_reels" | "instagram_user_stories" | "instagram_user_highlights" | "instagram_highlight_stories" | "instagram_post" | "instagram_post_comments" | "tiktok_profile" | "tiktok_user_posts" | "tiktok_post" | "tiktok_post_comments" | "tiktok_search" | "tiktok_hashtag_posts" | "tiktok_music_posts" | "tiktok_trending" | "tiktok_trend_scan" | "linkedin_profile" | "linkedin_company" | "linkedin_user_posts" | "linkedin_company_posts" | "linkedin_post" | "linkedin_search_people" | "linkedin_search_posts" | "youtube_channel" | "youtube_channel_videos" | "youtube_channel_playlists" | "youtube_channel_shorts" | "youtube_video" | "youtube_search" | "youtube_trending" | "facebook_page" | "facebook_page_posts" | "facebook_page_reels" | "facebook_post" | "facebook_post_comments" | "facebook_group" | "facebook_group_posts" | "facebook_search" | "reddit_search" | "reddit_subreddit_posts" | "reddit_post" | "reddit_post_comments" | "x_search" | "x_profile" | "x_user_posts" | "x_post" | "x_post_comments" | "x_user_connections" | "x_tweet_engagers" | "x_trends" | "resolve_mention", required): Operation name — see the operations catalog.
- `result` (object, required)
  - `summary` (string, required): One-line result, for the reader.
  - `username` (string, required)
  - `sort` ("recent" | "popular", required)
  - `count` (integer, required)
  - `totals` (object)
    - `window` (object, required)
    - `posts` (integer, required)
    - `views` (number, required)
    - `likes` (number, required)
    - `comments` (number, required)
    - `engagement_rate` (number | null, required)
    - `rows_without_views` (integer)
    - `rows_without_likes` (integer)
    - `rows_without_comments` (integer)
    - `views_metric` (string)
    - `measured_at` (string, required)
    - `partial` (true)
    - `overlaps` (string)
    - `oldest` (string)
    - `newest` (string)
  - `receipt` (string)
  - `coverage` (string, required)
  - `posts` (array<object>, required)
    - `id` (string)
    - `url` (string)
    - `text` (string)
    - `category` (string)
    - `region` (string)
    - `created_at` (string)
    - `duration_seconds` (number)
    - `play_count` (number)
    - `like_count` (number)
    - `comment_count` (number)
    - `share_count` (number)
    - `save_count` (number)
    - `hashtags` (array<string>)
    - `music` (object)
    - `author` (object)
    - `image_url` (string)
    - `slide_image_urls` (array<string>)
- `requests` (integer, required)
- `credits_charged` (integer, required)

### Errors

- `400` (Error): The request is malformed, or failed input validation. `message` names the field and the rule it broke; `data.issues` carries them individually.
- `401` (Error): No bearer token, or a token that failed verification.
- `402` (Error): The workspace has no credits left, or the caller is over a spending limit.
- `403` (Error): Authenticated, but the caller's role in this workspace does not carry the permission this operation needs.
- `409` (Error): The request is well-formed, but the resource's current state forbids it — a taken name, a last admin, a limit already reached.
- `429` (Error): The caller has spent its rate-limit window. `Retry-After` says when to come back.
- `500` (Error): The request failed on our side. Safe to retry an idempotent operation with backoff.
