Most TikTok scraper integrations get written in Python or Node, but when you live in a terminal, shipping a 12-line script that runs in cron is faster than scaffolding a project. Bash plus curl plus jq is the smallest possible client: zero runtime to install on a fresh server, no virtualenv to forget, no package.json to keep in sync. It pipes naturally into awk, sort, tee, and your existing log pipeline.
This cookbook targets sysadmins and DevOps engineers using the TikLiveAPI REST API. Every recipe authenticates with the X-Api-Key header against https://api.tikliveapi.com, returns JSON, and works inside a GitHub Actions job, a Jenkins step, or a plain crontab -e entry.
curl 7.50 or newer (for --fail-with-body if you upgrade past 7.76; --fail works on older versions).jq 1.6+ for JSON parsing. On Debian: apt install jq. On macOS: brew install jq.EPOCHSECONDS.Never hardcode the key. Two patterns work well:
# Option 1: environment variable in ~/.profile or systemd unit
export TIKLIVE_KEY="sk_live_xxxxxxxxxxxxxxxx"
# Option 2: ~/.netrc style file with 600 perms
install -m 600 /dev/null ~/.tiklive
echo "TIKLIVE_KEY=sk_live_xxxxxxxxxxxxxxxx" > ~/.tiklive
# then in scripts:
source ~/.tiklive
For CI, drop the key into the runner's secret store and expose it as TIKLIVE_KEY.
The /userinfo-by-username/ endpoint returns a nested object with user{} and stats{} blocks. Counters live under stats in camelCase: followerCount, videoCount, heartCount.
curl -s "https://api.tikliveapi.com/userinfo-by-username/?username=tiktok" \
-H "X-Api-Key: $TIKLIVE_KEY" \
| jq '{name: .user.nickname, followers: .stats.followerCount, hearts: .stats.heartCount}'
Most paginated endpoints want the numeric userid, not the @handle. /userid/ returns a flat { "id": "..." } object.
resolve_uid() {
curl -s "https://api.tikliveapi.com/userid/?username=$1" \
-H "X-Api-Key: $TIKLIVE_KEY" | jq -r .id
}
UID=$(resolve_uid mrbeast)
echo "Numeric UID: $UID"
/user-posts/ returns videos[], a string cursor, and a camelCase hasMore boolean. To count items in one page:
curl -s "https://api.tikliveapi.com/user-posts/?userid=$UID&count=30" \
-H "X-Api-Key: $TIKLIVE_KEY" \
| jq '.videos | length'
Walk every page until hasMore is false. Note the cursor is a millisecond timestamp string for posts; followers and following use a different field name covered below.
cursor="0"
while :; do
resp=$(curl -s "https://api.tikliveapi.com/user-posts/?userid=$UID&count=30&cursor=$cursor" \
-H "X-Api-Key: $TIKLIVE_KEY")
echo "$resp" | jq -c '.videos[] | {id: .aweme_id, plays: .play_count}'
has_more=$(echo "$resp" | jq -r '.hasMore')
[ "$has_more" != "true" ] && break
cursor=$(echo "$resp" | jq -r '.cursor')
sleep 0.4
done
Followers and following use a time cursor (unix seconds), not cursor. The following list also returns the plural key followings, not following:
# /user-followers/ pagination
time_cur="0"
curl -s "https://api.tikliveapi.com/user-followers/?userid=$UID&count=50&time=$time_cur" \
-H "X-Api-Key: $TIKLIVE_KEY" | jq '.followers[] | .unique_id'
# /user-following/ - note the response key is 'followings'
curl -s "https://api.tikliveapi.com/user-following/?userid=$UID&count=50&time=0" \
-H "X-Api-Key: $TIKLIVE_KEY" | jq '.followings[].unique_id'
Every video item under /user-posts/ is flat snake_case. Filter viral ones inline:
curl -s "https://api.tikliveapi.com/user-posts/?userid=$UID&count=30" \
-H "X-Api-Key: $TIKLIVE_KEY" \
| jq -r '.videos[] | select(.play_count > 1000000) | "\(.play_count)\t\(.title)"'
Parallelism via xargs -P is the closest thing bash has to a thread pool. Respect the 200 requests per minute default cap by capping concurrency.
cat usernames.txt | xargs -n1 -P4 -I{} bash -c '
u=$(curl -s "https://api.tikliveapi.com/userinfo-by-username/?username={}" \
-H "X-Api-Key: $TIKLIVE_KEY")
echo "{}|$(echo $u | jq -r .stats.followerCount)"
' > followers.csv
/post-detail/ returns a flat snake_case object (no wrapping data{}) with three download fields: play (no watermark), wmplay (with watermark), and hdplay (HD no watermark).
POST_URL="https://www.tiktok.com/@tiktok/video/7300000000000000000"
detail=$(curl -s "https://api.tikliveapi.com/post-detail/?url=$POST_URL" \
-H "X-Api-Key: $TIKLIVE_KEY")
hd=$(echo "$detail" | jq -r .hdplay)
id=$(echo "$detail" | jq -r .aweme_id)
curl -L -o "${id}.mp4" "$hd"
Pipe through awk to build a tracking row per run. Append-only files survive log rotation and graph well in Grafana via the CSV datasource.
#!/usr/bin/env bash
set -euo pipefail
: "${TIKLIVE_KEY:?missing}"
USER="$1"
out=$(curl -s "https://api.tikliveapi.com/userinfo-by-username/?username=$USER" \
-H "X-Api-Key: $TIKLIVE_KEY")
echo "$out" | jq -r --arg d "$(date -u +%F)" --arg u "$USER" \
'[$d, $u, .stats.followerCount, .stats.videoCount, .stats.heartCount] | @csv' \
>> snapshots.csv
Run the snapshot every day at 03:15 UTC for a list of creators:
15 3 * * * source /etc/tiklive.env && \
while read u; do /opt/tiklive/snapshot.sh "$u"; done < /etc/tiklive/users.txt
Compare today's count against yesterday's last CSV row.
today=$(curl -s "https://api.tikliveapi.com/userinfo-by-username/?username=$USER" \
-H "X-Api-Key: $TIKLIVE_KEY" | jq -r .stats.followerCount)
yesterday=$(tail -n2 snapshots.csv | head -n1 | awk -F, '{print $3}' | tr -d '"')
if [ "$today" -lt "$yesterday" ]; then
delta=$((yesterday - today))
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"$USER lost $delta followers ($yesterday -> $today)\"}" \
"$SLACK_WEBHOOK_URL"
fi
The default curl behavior on 5xx is to print the error body and exit 0, which silently corrupts your CSV. The fix is a three-line preamble plus curl --fail:
set -euo pipefail
shopt -s inherit_errexit
api() {
local path="$1"; shift
curl --fail --silent --show-error --retry 3 --retry-delay 2 \
--max-time 30 \
-H "X-Api-Key: $TIKLIVE_KEY" \
"https://api.tikliveapi.com${path}" "$@"
}
# usage
api "/userinfo-by-username/?username=tiktok" | jq .stats.followerCount
For 429 rate-limit responses, sleep and retry. The standard cap is 200 requests per minute:
fetch_with_backoff() {
local url="$1" tries=0
until body=$(curl -sS -w '\n%{http_code}' -H "X-Api-Key: $TIKLIVE_KEY" "$url"); do
code=$(echo "$body" | tail -n1)
[ "$code" = "429" ] || return 1
tries=$((tries+1))
[ "$tries" -gt 5 ] && return 1
sleep $((2 ** tries))
done
echo "$body" | sed '$d'
}
/post-comments/ returns comments[] where each item carries an id field (not cid), plus text, digg_count, reply_total, and a nested snake_case user{}. Top-level keys are comments, total, cursor, and the camelCase hasMore.
curl -s "https://api.tikliveapi.com/post-comments/?url=$POST_URL&count=50" \
-H "X-Api-Key: $TIKLIVE_KEY" \
| jq -r '.comments[] | "\(.id)\t\(.digg_count)\t\(.text)"'
# follow up: pull replies to a specific comment
curl -s "https://api.tikliveapi.com/post-comment-replies/?video_id=$VID&comment_id=$CID&count=20" \
-H "X-Api-Key: $TIKLIVE_KEY" | jq '.'
Drop this into .github/workflows/snapshot.yml to commit a daily snapshot back to the repo:
name: tiktok-snapshot
on:
schedule: [{ cron: '15 3 * * *' }]
workflow_dispatch:
jobs:
snap:
runs-on: ubuntu-latest
env:
TIKLIVE_KEY: ${{ secrets.TIKLIVE_KEY }}
steps:
- uses: actions/checkout@v4
- run: sudo apt-get install -y jq
- run: ./snapshot.sh tiktok >> data/snapshots.csv
- run: |
git config user.name bot
git config user.email bot@example.com
git add data/snapshots.csv
git commit -m "daily snapshot $(date -u +%F)" || exit 0
git push
Bash is correct when:
.sh file to a server is the spec.Pivot to Python (or Go) when:
xargs -P.For ASCII handles, no. For unicode or URLs containing & and ?, yes. Use curl --data-urlencode with -G to be safe: curl -G --data-urlencode "url=$POST_URL" ....
Pass headers via -H @file or environment variables, never on the command line. ps auxe exposes args but not headers built from env vars inside the child process.
The API returns an HTTP error and your curl --fail script exits nonzero. Top up at the pricing page; refunds are available on unused credit packages.
Yes. The playground sends real requests through a server-side proxy that injects your key, so you can iterate on parameters before wiring them into a script.
Reach the team via the contact page or read related guides on the blog. Field shapes can drift; the documentation is the canonical reference.
Ready to put what you read into code? Try our endpoints live or grab the full reference.