1322 TikTok Tracker API

Live-only uploads, reposts, and LIVE start/end. REST and WebSocket. Adding, restarting, recovering, or reconnecting never replays history.

Authentication

REST uses your API key. WebSocket uses a separate WebSocket key. Do not mix them.

REST — either header:

Authorization: Bearer <api_key>
X-API-Key: <api_key>

WebSocket — either header (query-string keys are not enabled):

Authorization: Bearer <ws_key>
X-WS-Key: <ws_key>

Mutations also require a stable Idempotency-Key (8–128 characters). Replaying the same key returns the original committed result.

Don't have keys? Join discord.gg/1322 and open a ticket.

REST endpoints

Base URL: https://tiktok.1322.io

GET /health

Public liveness. No auth.

GET /v1/status

Subscription details and current tracked count.

FieldTypeDescription
client.client_idstringYour tenant id
client.namestringSubscription name
client.expires_atRFC3339 or omittedExpiry when set
client.max_accountsintPlan tracking limit
client.tracked_countintAccounts currently tracked
client.websocket_enabledboolWebSocket delivery capability
client.discord_enabledboolDiscord delivery capability
readyboolTenant control-plane ready

GET /v1/list

Accounts you currently track.

FieldTypeDescription
tracksobject[]Each row has track_id, username, sec_uid, events, state, ws_delivery, discord_delivery
countintNumber of tracks

events is some subset of upload, repost, live. First add is warming until the current page is primed; no historical event is emitted.

POST /v1/track

Start tracking a TikTok account. 202 while the account is warming, 200 once already primed. Always live-only.

Send exactly one account alias: username, account, url, or tiktok.

FieldDescription
usernameoptionalHandle, with or without @
eventsoptional["upload","repost","live"] when omitted
discord_channel_idoptionalDiscord snowflake for this account
emit_initialoptionalMust be omitted or false. true is rejected
StatusMeaning
200 / 202Tracked. Body is {"track":{...},"quota":{...}}
400Invalid body, alias, or emit_initial:true
401Missing or invalid API key
403Expired or API disabled
409Already tracked or list full
422Username could not be resolved
curl -X POST https://tiktok.1322.io/v1/track \
  -H "Authorization: Bearer api_xxxxxxxxxxxx" \
  -H "Idempotency-Key: track-example-001" \
  -H "Content-Type: application/json" \
  -d '{"username":"example","events":["upload","repost","live"]}'

POST /v1/untrack

Stop tracking. Send exactly one selector: track_id, username, account, or sec_uid.

StatusMeaning
200{"track_id":"...","removed":true,"remaining":N}
400Missing, extra, or invalid selector

PATCH /v1/tracks/:track_id

Change events, ws_delivery, discord_delivery, or discord_channel_id for an existing track. Requires Idempotency-Key.

GET /v1/limits

Account quota plus WebSocket connection caps for your tenant.

GET /v1/ws/status

Whether WebSocket is enabled, your opaque path, and current connection counts. The key is never returned. public_url is stripped of query strings when present.

WebSocket

Connecting

Connect to your opaque tenant path. The path is not your client_id. Read it from Discord or GET /v1/ws/status.

wss://tiktok.1322.io/ws/<opaque_path>

# Auth (either header — not the REST API key):
#   Authorization: Bearer <ws_key>
#   X-WS-Key: <ws_key>

# Node.js (ws). Browsers cannot set these headers on WebSocket().
import WebSocket from "ws"
const ws = new WebSocket("wss://tiktok.1322.io/ws/<opaque_path>", {
  headers: { Authorization: "Bearer <ws_key>" }
})
ws.on("message", (buf) => {
  const event = JSON.parse(buf.toString())
  // tiktok.upload.created | tiktok.repost.created
  // tiktok.live.started | tiktok.live.ended | tiktok.media.ready
  console.log(event)
})

Delivery is live-only. A reconnect does not replay missed frames. Use object.kind for the post type (video, photo, or carousel). The permalink path is /video/ or /photo/. Video uploads and video reposts may include object.media_url. When the origin has a playable prefix, a sibling tiktok.media.ready frame (1322.tiktok.media.v1) repeats that event_id, permalink url, and playable media_url. It is not a second post. Photos and carousels do not get media_url or tiktok.media.ready. Caption text is description. Structured mentions and hashtags come from TikTok textExtra when present, including non-ASCII tags that a simple #tag regex would miss. Uploads and reposts may include object.duration (seconds) and object.music. LIVE start may include object.title and object.cover_url from the last live_user room lookup. Posts do not have a separate title.

Browser pages on https://tiktok.1322.io or https://1322.io may set an Origin header; other browser origins are rejected. Clients that send no Origin (Node, Go, most CLI tools) are accepted.

Bad handshake is HTTP 401 / 403 / 404 / 429 before upgrade — wrong key, wrong path, expired tenant, WebSocket disabled, origin denied, or connection limit.

TypeScript types

export type ContentEventType =
  | "tiktok.upload.created"
  | "tiktok.repost.created"
  | "tiktok.live.started"
  | "tiktok.live.ended"

export type MediaEventType = "tiktok.media.ready"

export interface Account {
  sec_uid: string
  username: string
  display_name: string
  verified?: true
}

export interface OriginalAuthor {
  sec_uid: string
  username: string
  display_name: string
  avatar_url?: string
  verified?: true
}

export type ContentKind = "video" | "photo" | "carousel"

export interface MusicInfo {
  title?: string
  author_name?: string
  original?: true
  duration?: number
}

export interface EventObject {
  video_id?: string
  url?: string
  description?: string
  cover_url?: string
  media_url?: string
  image_urls?: string[]
  avatar_url?: string
  kind?: ContentKind
  mentions?: string[]
  hashtags?: string[]
  duration?: number
  music?: MusicInfo
  title?: string
  original_author?: OriginalAuthor
  room_id?: string
  previous_state?: string
  current_state?: string
  session_generation?: number
}

export interface EventEnvelope {
  schema: "1322.tiktok.event.v1"
  event_id: string
  sequence: number
  type: ContentEventType
  platform: "tiktok"
  account: Account
  object: EventObject
  occurred_at: string | null
  observed_at: string
  source: { lane: string; poll_sequence: number }
  delivery: { mode: "live_only" }
}

export interface MediaReadyEnvelope {
  schema: "1322.tiktok.media.v1"
  event_id: string
  type: "tiktok.media.ready"
  platform: "tiktok"
  observed_at: string
  object: { video_id: string; url: string; media_url: string }
}

Uploads include occurred_at. Reposts and LIVE frames send occurred_at: null. Ignore unknown fields. Tenant frames never include raw TikTok play_url.

What each event includes

These are the fields we actually parse from TikTok polls. We do not invent Binance-style extras (play/digg/comment/share stats, follower counts, bitrate, duet/stitch types, or music CDN URLs). verified is omitted unless TikTok sent true. Discord titles for posts are still generated from @username plus kind.

KindHow you tellPresent
Video uploadkind: "video", URL path /video/, no image_urlsaccount.display_name (nickname), optional account.verified, cover_url, object.avatar_url, caption, duration in seconds when TikTok sends it, music (title, author_name, original, optional duration), HMAC media_url when we have a playable origin. Sibling tiktok.media.ready repeats the same event_id.
Photokind: "photo", URL path /photo/, one image_urls entrySame account/cover/avatar/caption/music. No media_url. duration is present when TikTok sends a slideshow/video length.
Carousel / slideshowkind: "carousel", URL path /photo/, 2–10 image_urlsSame as photo. TikTok returns at most 10 images; we keep that cap. Discord sends extra gallery messages after the first four images.
Text-style / photo-mode cardStill an upload. If TikTok attached a card image it is a photo; caption-only with a video cover stays videoCaption from desc, or contents[0].desc when the top-level caption is empty. Mentions/hashtags from textExtra.
Reposttiktok.repost.createdSame media shape as the original item, plus original_author (sec_uid, username, display_name, avatar_url, optional verified). account is the tracked reposter — their verified comes from identity stash, not from item.author. object.avatar_url is the original creator’s face — that is what the repost poll returns as item.author.
LIVE starttiktok.live.startedroom_id, previous_state, current_state, session_generation. account.display_name, optional account.verified, and object.avatar_url come from the last seen tracked profile. object.title and object.cover_url come from the last live_user resolver poll while that room was live. The batch alive-check has no title/cover, so a start that fires before the resolver may omit them. No kind / mentions / hashtags / duration / music.
LIVE endtiktok.live.endedSame room/session fields and stashed profile. No title or cover_url.

account.display_name is empty until we have seen a nickname for that sec_uid. After a process restart the first upload or LIVE-room lookup fills it again. Discord uses the same object: generated title from kind, caption with @ and # turned into TikTok links, duration/sound fields, mention/hashtag fields, and a gallery of every image_urls entry (four images per message). LIVE start Discord uses the room title as the description and the cover as the embed image when those fields are present.

Payload examples

Upload:

{
  "schema": "1322.tiktok.event.v1",
  "event_id": "01J5QUPLOAD000000000000000",
  "sequence": 12345,
  "type": "tiktok.upload.created",
  "platform": "tiktok",
  "account": {
    "sec_uid": "MS4wLjABAAAAfixture",
    "username": "example",
    "display_name": "Example",
    "verified": true
  },
  "object": {
    "video_id": "7540000000000000001",
    "url": "https://www.tiktok.com/@example/video/7540000000000000001",
    "description": "fixture upload",
    "cover_url": "https://p16-sign.tiktokcdn-us.com/fixture.jpeg",
    "avatar_url": "https://p16-sign.tiktokcdn-us.com/avatar.jpeg",
    "kind": "video",
    "duration": 18,
    "music": {
      "title": "original sound",
      "author_name": "Example",
      "original": true,
      "duration": 18
    }
  },
  "occurred_at": "2026-08-19T10:00:00Z",
  "observed_at": "2026-08-19T10:00:00.742Z",
  "source": { "lane": "creator_item_list", "poll_sequence": 9876 },
  "delivery": { "mode": "live_only" }
}

Photo / carousel (same event type; path is /photo/, no media_url):

{
  "schema": "1322.tiktok.event.v1",
  "event_id": "01J5QPHOTO0000000000000000",
  "sequence": 12345,
  "type": "tiktok.upload.created",
  "platform": "tiktok",
  "account": {
    "sec_uid": "MS4wLjABAAAAfixture",
    "username": "example",
    "display_name": "Example"
  },
  "object": {
    "video_id": "7540000000000000002",
    "url": "https://www.tiktok.com/@example/photo/7540000000000000002",
    "description": "fixture photo @friend #tag",
    "cover_url": "https://p16-sign.tiktokcdn-us.com/photo.jpeg",
    "image_urls": [
      "https://p16-sign.tiktokcdn-us.com/1.jpeg",
      "https://p16-sign.tiktokcdn-us.com/2.jpeg"
    ],
    "avatar_url": "https://p16-sign.tiktokcdn-us.com/avatar.jpeg",
    "kind": "carousel",
    "mentions": ["friend"],
    "hashtags": ["tag"]
  },
  "occurred_at": "2026-08-19T10:00:00Z",
  "observed_at": "2026-08-19T10:00:00.742Z",
  "source": { "lane": "creator_item_list", "poll_sequence": 9877 },
  "delivery": { "mode": "live_only" }
}

Repost:

{
  "schema": "1322.tiktok.event.v1",
  "event_id": "01J5QREPOST00000000000000",
  "sequence": 12346,
  "type": "tiktok.repost.created",
  "platform": "tiktok",
  "account": {
    "sec_uid": "MS4wLjABAAAAfixture",
    "username": "example",
    "display_name": "Example"
  },
  "object": {
    "video_id": "7539999999999999999",
    "url": "https://www.tiktok.com/@origin/video/7539999999999999999",
    "description": "fixture repost",
    "cover_url": "https://p16-sign.tiktokcdn-us.com/repost.jpeg",
    "avatar_url": "https://p16-sign.tiktokcdn-us.com/origin-avatar.jpeg",
    "kind": "video",
    "original_author": {
      "sec_uid": "MS4wLjABAAAAorigin",
      "username": "origin",
      "display_name": "Origin",
      "avatar_url": "https://p16-sign.tiktokcdn-us.com/origin-avatar.jpeg",
      "verified": true
    }
  },
  "occurred_at": null,
  "observed_at": "2026-08-19T10:00:01.101Z",
  "source": { "lane": "repost_item_list", "poll_sequence": 8123 },
  "delivery": { "mode": "live_only" }
}

LIVE started:

{
  "schema": "1322.tiktok.event.v1",
  "event_id": "01J5QLIVESTART00000000000",
  "sequence": 12347,
  "type": "tiktok.live.started",
  "platform": "tiktok",
  "account": {
    "sec_uid": "MS4wLjABAAAAfixture",
    "username": "example",
    "display_name": "Example",
    "verified": true
  },
  "object": {
    "room_id": "7460000000000000001",
    "previous_state": "offline",
    "current_state": "live",
    "session_generation": 9,
    "avatar_url": "https://p16-sign.tiktokcdn-us.com/avatar.jpeg",
    "title": "late night",
    "cover_url": "https://p16-sign.tiktokcdn-us.com/live-cover.jpeg"
  },
  "occurred_at": null,
  "observed_at": "2026-08-19T10:00:02.250Z",
  "source": { "lane": "live_batch", "poll_sequence": 500 },
  "delivery": { "mode": "live_only" }
}

LIVE ended:

{
  "schema": "1322.tiktok.event.v1",
  "event_id": "01J5QLIVEEND0000000000000",
  "sequence": 12348,
  "type": "tiktok.live.ended",
  "platform": "tiktok",
  "account": {
    "sec_uid": "MS4wLjABAAAAfixture",
    "username": "example",
    "display_name": "Example"
  },
  "object": {
    "room_id": "7460000000000000001",
    "previous_state": "live",
    "current_state": "offline",
    "session_generation": 9
  },
  "occurred_at": null,
  "observed_at": "2026-08-19T10:30:02.250Z",
  "source": { "lane": "live_batch", "poll_sequence": 1940 },
  "delivery": { "mode": "live_only" }
}

Playable media sibling (same event_id as the upload or repost, not a second post):

{
  "schema": "1322.tiktok.media.v1",
  "event_id": "01J5QUPLOAD000000000000000",
  "type": "tiktok.media.ready",
  "platform": "tiktok",
  "observed_at": "2026-08-24T00:00:00Z",
  "object": {
    "video_id": "7540000000000000001",
    "url": "https://www.tiktok.com/@example/video/7540000000000000001",
    "media_url": "https://tiktok.1322.io/v1/media/7540000000000000001.mp4?exp=1780000000&sig=abc"
  }
}

Common error codes

HTTPerror.codeMeaning
400invalid_requestBad JSON, query, alias, or emit_initial:true
401invalid_authenticationMissing or invalid key
403subscription_expired / api_disabled / forbiddenExpired, disabled, or not permitted. Open a ticket at discord.gg/1322
404not_foundUnknown track or route
409already_tracked / list_full / conflictDuplicate track, quota, or idempotency clash
422resolve_failedTikTok account could not be resolved
429rate_limitedBack off and retry
500internal_errorInternal error

Error body shape: {"error":{"code":"...","message":"...","request_id":"..."}}

Machine-readable contract: /openapi.json. Route table: /v1/docs.