PHP still runs most of the web, and it is still one of the fastest ways to get TikTok data into a MySQL table or a dashboard. In this tutorial we will use plain procedural PHP and the built-in cURL extension to call the TikLiveAPI REST API and pull TikTok user profiles, follower counts, video lists, and engagement metrics - no framework, no Composer packages, no headless browser.
You'll learn how to:
X-Api-Key headercursor and hasMorecurl_multiphp -m | grep curlRegister a TikLiveAPI account, choose a credit plan, and copy your API key from the profile dashboard. Every request carries that key in an X-Api-Key header - there is no OAuth dance, no token refresh, no cookies.
Keep the key out of source control by exporting it as an environment variable:
export TIKLIVEAPI_KEY="paste-your-key-here"
Then create a small config.php that every script in this tutorial will include:
<?php
// config.php
define('API_KEY', getenv('TIKLIVEAPI_KEY'));
define('BASE_URL', 'https://api.tikliveapi.com');
The /userinfo-by-username/ endpoint returns a user's nickname, bio, verification status, follower and following counts, total likes, and avatar URLs in a single call. Here is the raw cURL version with zero abstractions:
<?php
require 'config.php';
$query = http_build_query(['username' => 'tiktok']);
$curl = curl_init(BASE_URL . '/userinfo-by-username/?' . $query);
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['X-Api-Key: ' . API_KEY],
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($body === false || $status !== 200) {
exit("Request failed with HTTP {$status}\n");
}
$profile = json_decode($body, true);
echo $profile['user']['nickname'] . "\n";
echo 'Followers: ' . number_format($profile['stats']['followerCount']) . "\n";
echo 'Likes: ' . number_format($profile['stats']['heartCount']) . "\n";
echo 'Videos: ' . number_format($profile['stats']['videoCount']) . "\n";
CURLOPT_RETURNTRANSFER makes curl_exec() return the body instead of printing it, and json_decode($body, true) turns it into nested associative arrays: user holds profile fields, stats holds the counters.
Profile numbers are a snapshot; the real signal lives in the videos. The /user-posts/ endpoint takes a numeric userid rather than a username, so we resolve the ID first via /userid/ - and fold the cURL boilerplate into a reusable helper:
<?php
require 'config.php';
function tiklive_get(string $path, array $params): array {
$curl = curl_init(BASE_URL . $path . '?' . http_build_query($params));
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['X-Api-Key: ' . API_KEY],
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($body === false || $status !== 200) {
throw new RuntimeException("HTTP {$status} on {$path}");
}
return json_decode($body, true);
}
$userid = tiklive_get('/userid/', ['username' => 'tiktok'])['id'];
$data = tiklive_get('/user-posts/', ['userid' => $userid, 'count' => 35]);
foreach (array_slice($data['videos'], 0, 5) as $video) {
printf(
"%s views=%s likes=%s comments=%s\n",
$video['video_id'],
number_format($video['play_count']),
number_format($video['digg_count']),
number_format($video['comment_count'])
);
}
The /user-posts/ response has a top-level videos array. Each entry carries flat engagement counters - play_count, digg_count (likes), comment_count, share_count, and download_count - plus music_info, the no-watermark play URL, and the watermarked wmplay URL.
count defaults to 10 and tops out at 35 per request, so longer histories need pagination. Every response includes a cursor string you send back to fetch the next page, plus a boolean hasMore that tells you when to stop:
function all_posts(string $userid, int $pageSize = 35): array {
$videos = [];
$cursor = '0';
while (true) {
$data = tiklive_get('/user-posts/', [
'userid' => $userid,
'count' => $pageSize,
'cursor' => $cursor,
]);
foreach ($data['videos'] ?? [] as $video) {
$videos[] = $video;
}
if (empty($data['hasMore'])) {
break;
}
$cursor = $data['cursor'];
}
return $videos;
}
$videos = all_posts($userid);
echo 'Fetched ' . count($videos) . " videos\n";
Any script that hits a social-media API thousands of times a day will eventually meet timeouts, transient 5xx responses, and the occasional 429. The wrapper below retries exactly those cases with exponential backoff and gives up cleanly on everything else:
function safe_get(string $path, array $params, int $maxAttempts = 5): array {
$attempt = 0;
while (true) {
$attempt++;
$curl = curl_init(BASE_URL . $path . '?' . http_build_query($params));
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['X-Api-Key: ' . API_KEY],
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$errno = curl_errno($curl);
curl_close($curl);
if ($errno === 0 && $status === 200) {
return json_decode($body, true);
}
// Retry network errors, 429, and 5xx; fail fast on other 4xx.
$retryable = $errno !== 0 || $status === 429 || $status >= 500;
if (!$retryable || $attempt >= $maxAttempts) {
throw new RuntimeException("Giving up on {$path}: HTTP {$status}");
}
// Backoff: 1s, 2s, 4s, 8s - capped at 30s.
sleep(min(2 ** ($attempt - 1), 30));
}
}
The split matters: a 404 (user does not exist) or 401 (bad key) will never succeed on retry, so extra attempts only waste time. For a deeper treatment of backoff, queues, and credit budgeting, see our guide to building resilient pipelines around TikTok API rate limits.
Looping over 100 usernames one request at a time means paying 100 round-trip latencies. PHP has no async-await, but cURL ships its own concurrency primitive: curl_multi drives many handles in parallel and returns when all finish:
function bulk_profiles(array $usernames): array {
$multi = curl_multi_init();
$handles = [];
foreach ($usernames as $username) {
$query = http_build_query(['username' => $username]);
$curl = curl_init(BASE_URL . '/userinfo-by-username/?' . $query);
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['X-Api-Key: ' . API_KEY],
]);
curl_multi_add_handle($multi, $curl);
$handles[$username] = $curl;
}
do {
curl_multi_exec($multi, $running);
if ($running) {
curl_multi_select($multi);
}
} while ($running);
$profiles = [];
foreach ($handles as $username => $curl) {
$body = curl_multi_getcontent($curl);
$code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$profiles[$username] = ($code === 200) ? json_decode($body, true) : null;
curl_multi_remove_handle($multi, $curl);
curl_close($curl);
}
curl_multi_close($multi);
return $profiles;
}
$profiles = bulk_profiles(['tiktok', 'khaby.lame', 'charlidamelio']);
foreach ($profiles as $username => $profile) {
if ($profile === null) {
echo "error fetching {$username}\n";
continue;
}
echo $profile['user']['nickname'] . ': '
. number_format($profile['stats']['followerCount']) . "\n";
}
Two production tips:
null instead of blowing up the whole batch - check each result before using it.array_chunk($usernames, 20); 20 concurrent requests is safe on every plan.Let's wire the pieces into something useful: a cron-driven script that records follower counts for a watchlist into a CSV, one row per account per day:
<?php
// track_followers.php
require 'config.php'; // defines API_KEY, BASE_URL, safe_get()
$tracked = ['tiktok', 'khaby.lame', 'charlidamelio', 'willsmith'];
$outFile = __DIR__ . '/follower_history.csv';
$rows = [];
foreach ($tracked as $username) {
try {
$profile = safe_get('/userinfo-by-username/', ['username' => $username]);
$stats = $profile['stats'];
$rows[] = [
date('Y-m-d'),
$username,
$stats['followerCount'],
$stats['heartCount'],
$stats['videoCount'],
];
} catch (RuntimeException $exc) {
echo "skip {$username}: " . $exc->getMessage() . "\n";
}
}
if ($rows !== []) {
$isNew = !file_exists($outFile);
$handle = fopen($outFile, 'a');
if ($isNew) {
fputcsv($handle, ['date', 'username', 'followers', 'likes', 'video_count']);
}
foreach ($rows as $row) {
fputcsv($handle, $row);
}
fclose($handle);
}
Schedule it with a single crontab line:
30 6 * * * /usr/bin/php /var/www/scripts/track_followers.php
After a few weeks you have a growth curve for every account on the list - load the CSV into a spreadsheet or a MySQL table and you are doing real analytics. If your project lives inside a framework, the same patterns map cleanly onto Laravel's HTTP client, scheduler, and queues - covered end to end in our Laravel integration guide.
Everything above used three user endpoints, but TikLiveAPI exposes 37 endpoints across 10 categories - all speaking the same dialect. A few natural next stops:
Each one is a GET request with an X-Api-Key header returning JSON, so the tiklive_get() and safe_get() helpers above already work for all of them. The full reference lives in the documentation.
TikLiveAPI returns only data that is publicly visible on TikTok - what any logged-out visitor sees on a profile page. Private accounts are never bypassed. You remain responsible for downstream use, though: GDPR, CCPA, and TikTok's terms of service still apply to what you store and publish.
Limits are credit-based rather than per-second: each call costs one credit and your plan sets the monthly budget. There is no hard per-second cap, but bursts above ~50 requests per second from one key can briefly return 429 - which safe_get() above absorbs automatically.
No. Everything here is plain procedural PHP on the bundled cURL extension. If your project already uses Guzzle or Symfony HttpClient, go ahead - the API is plain HTTPS plus JSON and behaves identically with any client.
No - IP rotation, CAPTCHA solving, and session management all happen server-side at TikLiveAPI. Your PHP script calls api.tikliveapi.com directly with your key and never touches anti-bot infrastructure.
Anything with cURL and json_decode() can call the API. The samples use typed signatures and the null coalescing operator, so they need PHP 7.4 or newer as written; on older installs just drop the type hints.
You now have a procedural PHP toolkit for TikTok user data: auth, pagination, retries, parallel fetching, and a working cron pipeline. From here:
Prefer a different stack? The same auth, pagination, and retry flow is covered in our Python scraping tutorial, with identical endpoints and response shapes.
Building something interesting with TikTok data? Tell us about it via the contact page.
Ready to put what you read into code? Try our endpoints live or grab the full reference.