# TikTok Jobs API

> Query TikTok's postings through one API with ?source=tiktok — plus the website-path header its API 400s without.

Source: https://hyperjobs.io/ats/tiktok

TikTok is not a platform. It's one employer with a bespoke careers site at
`lifeattiktok.com`, and this adapter reaches TikTok's postings and nothing else.
One [Workday](/ats/workday) adapter reaches every Workday tenant; this one reaches
one company.

It's here because TikTok is large enough for its jobs to matter on their own, and
because a bespoke site has no vendor fingerprint for platform discovery to find.
The price is a dedicated adapter, owned forever, breaking on TikTok's schedule.

## How TikTok's careers API actually works

A POST, not bot-protected, descriptions and structured salary inline:

```bash
curl -X POST "https://api.lifeattiktok.com/api/v1/public/supplier/search/job/posts" \
  -H "website-path: tiktok" \
  -H "origin: https://lifeattiktok.com" \
  -H "referer: https://lifeattiktok.com/" \
  -d '{"keyword":"","limit":100,"offset":0}'
```

<Tip title="Header-gated, not bot-protected">
  There's no Akamai here, no browser needed, no TLS impersonation — plain HTTP
  works. But the request is **header-gated**: `website-path: tiktok` plus a
  matching `origin` and `referer` are required, and without them the API returns
  **400**.

  That's the good kind of quirk, at least: it fails loudly. Contrast
  [Apple](/ats/apple), where a missing block returns zero results and a clean
  200. A 400 tells you something is wrong; a silent zero doesn't.

  `website-path` exists because the same backend serves several ByteDance career
  sites. You're telling it which brand you're asking about.
</Tip>

### What the adapter reads

Everything comes back in one page, no detail call:

| Field | What it becomes |
| --- | --- |
| `title`, `description`, `requirement` | Body assembled as description + a `Requirements` section |
| `code` | The requisition id (`"A220892"`) |
| `job_category.en_name` | Department |
| `job_subject.en_name` | Team |
| `recruit_type.en_name` | `"Regular"`, `"Intern"` — employment type and commitment |
| `job_post_info` | `{ min_salary, max_salary, currency }` — structured pay |
| `city_info` | A nested location chain (see below) |
| `publish_time` | Epoch timestamp |

The description is two fields, not one: `description` plus `requirement`,
reassembled with a heading. Read `description` alone and you get the pitch without
the requirements — the same split [Lever](/ats/lever) and [Amazon](/ats/amazon)
have, in a third arrangement. `en_name` throughout is the tell that this is a
bilingual system with English alongside Chinese.

### Locations are a linked list

`city_info` isn't a string or a flat object. It's a **parent chain**: a city node
whose `parent` is a region whose `parent` is a country. Getting a location means
walking the chain to the root and joining what you find.

Read `city_info.en_name` and stop, and every location loses its country — leaving
you a bare city to geocode, which is how `Cambridge` becomes the wrong one.

## What bites you

**`publish_time` is an epoch with no declared unit.** Sometimes seconds,
sometimes milliseconds. Treat seconds as milliseconds and the posting lands in
1970; treat milliseconds as seconds and it lands tens of thousands of years from
now. The adapter switches on magnitude — under a trillion means seconds. There's
nothing in the payload that tells you which you're holding.

<Warning title="The salary has no period">
  `job_post_info` gives a min, a max and a currency. It does **not** say whether
  that range is annual, monthly or hourly.

  The numbers are real and their unit isn't in the data. A range that's monthly in
  one market and annual in another looks identical in the payload, and annualising
  the wrong one is off by a factor of twelve. The adapter stores the unit as null
  rather than guessing, because a wrong unit is worse than a missing one.
</Warning>

**One adapter, one company, forever.** Same deal as [Apple](/ats/apple),
[Amazon](/ats/amazon), [Uber](/ats/uber), [Google](/ats/google),
[Microsoft](/ats/microsoft) and [IBM](/ats/ibm): a dedicated adapter maintained
indefinitely, breaking independently, for one employer's jobs.

## Or use ours

Three headers, offset pagination, a two-part body reassembled, a parent chain
walked to a country, an epoch disambiguated by size, and a salary whose period
isn't stated left honestly unitless — with TikTok's rows in the same
[`Job`](/docs/objects/job) shape as every other [source](/docs/reference/sources),
dedup against the same role on [LinkedIn](/ats/linkedin), expiry by board diffing
since the payload declares none, and a single-company adapter's upkeep being ours
rather than yours.

Query it with `?source=tiktok` — the same string that keys
[`/v1/meta`](/docs/api-reference/meta) → `sources`. That endpoint is the live
vocabulary and carries the counts as they move; it's worth reading rather than
trusting a list on a page.

[Sources](/docs/reference/sources) · [Quickstart](/docs/quickstart)
