TikTok API Integration with Laravel: A Complete Guide

Published on May 29, 2026

Why Laravel and TikLiveAPI Fit Together

Laravel ships with the exact primitives a TikTok-aware application needs: an HTTP client built on Guzzle, Eloquent for modelling creator snapshots, queues for fan-out polling, and the scheduler for nightly ingestion. TikLiveAPI, in turn, hands you 37 ready-to-call TikTok endpoints behind a single X-Api-Key header, so you skip the entire scraping, rotation, and proxy layer.

This guide walks through wiring TikLiveAPI into a Laravel 11 application end to end: a typed client, persistence layer, queued snapshots, scheduled ingestion, API resources, retries, caching, an admin sketch, and tests. By the end you will have a creator dashboard that captures a daily snapshot per creator, exposes a paginated posts endpoint, and gracefully degrades when upstream is slow.

One honest note up front: TikLiveAPI does not expose a dedicated TikTok Live (livestream) endpoint. Live-specific stream telemetry is not available. What you can access about Live content that a creator publishes to their feed is reachable through the regular post and profile endpoints such as /post-detail/, /user-posts/, and /user-stories/. The patterns in this guide apply to all of those.

Prerequisites

  • Laravel 11 or newer
  • PHP 8.2 or newer
  • MySQL 8 or PostgreSQL 14 (Eloquent works with either)
  • Redis recommended for queues and cache (database driver is fine for development)
  • A TikLiveAPI account with an API key from your profile and credits from the pricing page

Step 1: Install and Configure

Add your key to .env:

TIKLIVEAPI_KEY=your_key_here
TIKLIVEAPI_BASE_URL=https://api.tikliveapi.com

Register it in config/services.php so the value flows through Laravel's config cache:

'tikliveapi' => [
    'key'      => env('TIKLIVEAPI_KEY'),
    'base_url' => env('TIKLIVEAPI_BASE_URL', 'https://api.tikliveapi.com'),
    'timeout'  => 15,
],

Never inject the key directly inside controllers. Always pull from config('services.tikliveapi.key') so it is overridable in tests and CI.

Step 2: Build a Typed Client

Create app/Services/TikLiveApi.php. This is the single class that talks to the upstream API. Everything else in your app calls methods on this object.

<?php

namespace App\Services;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;

class TikLiveApi
{
    public function __construct(
        private readonly string $key,
        private readonly string $baseUrl,
        private readonly int $timeout = 15,
    ) {}

    private function http(): PendingRequest
    {
        return Http::baseUrl($this->baseUrl)
            ->withHeaders(['X-Api-Key' => $this->key])
            ->acceptJson()
            ->timeout($this->timeout)
            ->retry(3, 250, throw: false);
    }

    public function userId(string $username): ?string
    {
        $res = $this->http()->get('/userid/', ['username' => $username]);
        return $res->successful() ? ($res->json('id') ?? null) : null;
    }

    public function userInfo(string $username): ?array
    {
        $res = $this->http()->get('/userinfo-by-username/', ['username' => $username]);
        return $res->successful() ? $res->json() : null;
    }

    public function userInfoById(string $userid): ?array
    {
        $res = $this->http()->get('/userinfo-by-id/', ['userid' => $userid]);
        return $res->successful() ? $res->json() : null;
    }

    public function userPosts(string $userid, int $count = 20, string $cursor = '0'): array
    {
        return $this->http()->get('/user-posts/', [
            'userid' => $userid,
            'count'  => $count,
            'cursor' => $cursor,
        ])->json() ?? ['videos' => [], 'cursor' => '0', 'hasMore' => false];
    }

    public function userFollowers(string $userid, int $count = 50, int $time = 0): array
    {
        return $this->http()->get('/user-followers/', [
            'userid' => $userid,
            'count'  => $count,
            'time'   => $time,
        ])->json() ?? ['followers' => [], 'total' => 0, 'time' => 0, 'hasMore' => false];
    }
}

Two things worth highlighting against the real response shapes. The followers endpoint paginates by a time integer rather than a cursor string, so the default fetcher uses time => 0 on the first page and then echoes back whatever the response carries. The userInfo response is nested: counters live under stats, profile fields under user. Do not treat it as a flat payload.

Bind the class as a singleton in app/Providers/AppServiceProvider.php:

$this->app->singleton(TikLiveApi::class, function () {
    return new TikLiveApi(
        key:     config('services.tikliveapi.key'),
        baseUrl: config('services.tikliveapi.base_url'),
        timeout: config('services.tikliveapi.timeout'),
    );
});

Step 3: Model Layer

Three tables cover most analytics use cases. A creator row per tracked TikTok account, a snapshot row per day per creator, and a post row per video we have observed.

// database/migrations/2026_05_29_000001_create_tiktok_creators_table.php
Schema::create('tiktok_creators', function (Blueprint $t) {
    $t->id();
    $t->string('username')->unique();
    $t->string('tiktok_user_id')->nullable()->index();
    $t->string('nickname')->nullable();
    $t->text('avatar')->nullable();
    $t->boolean('verified')->default(false);
    $t->timestamp('last_synced_at')->nullable();
    $t->timestamps();
});

// database/migrations/2026_05_29_000002_create_tiktok_snapshots_table.php
Schema::create('tiktok_snapshots', function (Blueprint $t) {
    $t->id();
    $t->foreignId('creator_id')->constrained('tiktok_creators')->cascadeOnDelete();
    $t->date('captured_on');
    $t->unsignedBigInteger('follower_count')->default(0);
    $t->unsignedBigInteger('following_count')->default(0);
    $t->unsignedBigInteger('heart_count')->default(0);
    $t->unsignedBigInteger('video_count')->default(0);
    $t->timestamps();
    $t->unique(['creator_id', 'captured_on']);
});

// database/migrations/2026_05_29_000003_create_tiktok_posts_table.php
Schema::create('tiktok_posts', function (Blueprint $t) {
    $t->id();
    $t->foreignId('creator_id')->constrained('tiktok_creators')->cascadeOnDelete();
    $t->string('aweme_id')->unique();
    $t->unsignedBigInteger('play_count')->default(0);
    $t->unsignedBigInteger('digg_count')->default(0);
    $t->text('play_url')->nullable();
    $t->text('hd_play_url')->nullable();
    $t->timestamp('observed_at');
    $t->timestamps();
});

The Eloquent models are thin and conventional:

class TikTokCreator extends Model
{
    protected $fillable = ['username', 'tiktok_user_id', 'nickname', 'avatar', 'verified', 'last_synced_at'];
    protected $casts = ['verified' => 'bool', 'last_synced_at' => 'datetime'];

    public function snapshots() { return $this->hasMany(TikTokSnapshot::class, 'creator_id'); }
    public function posts()     { return $this->hasMany(TikTokPost::class, 'creator_id'); }
}

Step 4: Snapshot Service

The service layer is where you translate the upstream nested shape into your flat database columns. Keeping this conversion in one place means downstream code never has to know the API exists.

namespace App\Services;

use App\Models\TikTokCreator;
use App\Models\TikTokSnapshot;
use Carbon\Carbon;

class SnapshotService
{
    public function __construct(private readonly TikLiveApi $api) {}

    public function capture(TikTokCreator $creator): ?TikTokSnapshot
    {
        $payload = $this->api->userInfo($creator->username);
        if (!$payload || !isset($payload['stats'])) {
            return null;
        }

        $user  = $payload['user']  ?? [];
        $stats = $payload['stats'] ?? [];

        $creator->forceFill([
            'tiktok_user_id'  => $user['id']          ?? $creator->tiktok_user_id,
            'nickname'        => $user['nickname']    ?? $creator->nickname,
            'avatar'          => $user['avatarMedium']?? $creator->avatar,
            'verified'        => (bool) ($user['verified'] ?? false),
            'last_synced_at'  => now(),
        ])->save();

        return TikTokSnapshot::updateOrCreate(
            ['creator_id' => $creator->id, 'captured_on' => Carbon::today()],
            [
                'follower_count'  => $stats['followerCount']  ?? 0,
                'following_count' => $stats['followingCount'] ?? 0,
                'heart_count'     => $stats['heartCount']     ?? 0,
                'video_count'     => $stats['videoCount']     ?? 0,
            ],
        );
    }
}

The nested camelCase keys come straight from the verified user-info response. If you later want to populate the same snapshot from info-by-id (handy when you already cached the numeric id), the shape is identical, so the service does not need a second code path.

Step 5: Queue Jobs

Hitting upstream synchronously inside a scheduled command blocks the whole scheduler if any one creator is slow. Push each creator into its own queued job:

namespace App\Jobs;

use App\Models\TikTokCreator;
use App\Services\SnapshotService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ScheduleCreatorSnapshot implements ShouldQueue
{
    use Dispatchable, Queueable, SerializesModels, InteractsWithQueue;

    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(public TikTokCreator $creator) {}

    public function handle(SnapshotService $service): void
    {
        $service->capture($this->creator);
    }
}

Step 6: Scheduler

In Laravel 11 the scheduler lives in routes/console.php. Fan out one job per creator at 03:00 every day:

use App\Jobs\ScheduleCreatorSnapshot;
use App\Models\TikTokCreator;
use Illuminate\Support\Facades\Schedule;

Schedule::call(function () {
    TikTokCreator::query()->each(
        fn ($creator) => ScheduleCreatorSnapshot::dispatch($creator)
    );
})->dailyAt('03:00')->name('tiktok:snapshot-fanout')->onOneServer();

An Artisan command for ad-hoc backfills is also useful. Generate one with php artisan make:command CaptureCreatorSnapshot and have it call ScheduleCreatorSnapshot::dispatchSync($creator) for a single username argument.

Step 7: API Resource

Expose the data to your frontend through an API resource so the JSON shape stays stable even when the database schema shifts.

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class TikTokCreatorResource extends JsonResource
{
    public function toArray($request): array
    {
        $latest = $this->snapshots()->latest('captured_on')->first();
        return [
            'username'   => $this->username,
            'nickname'   => $this->nickname,
            'verified'   => $this->verified,
            'avatar'     => $this->avatar,
            'followers'  => $latest?->follower_count,
            'videos'     => $latest?->video_count,
            'hearts'     => $latest?->heart_count,
            'synced_at'  => optional($this->last_synced_at)->toIso8601String(),
        ];
    }
}

Step 8: HTTP Retries and Circuit Breaker

The client already retries three times with a 250 ms gap. For real production you want a circuit breaker so a misbehaving upstream does not torch your queue workers. A lightweight version uses the cache as a shared failure counter:

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use RuntimeException;

class CircuitBreaker
{
    public function __construct(
        private readonly string $key,
        private readonly int $threshold = 5,
        private readonly int $cooldown = 60,
    ) {}

    public function guard(callable $call): mixed
    {
        if (Cache::get("cb:open:{$this->key}")) {
            throw new RuntimeException("Circuit open for {$this->key}");
        }
        try {
            $result = $call();
            Cache::forget("cb:fail:{$this->key}");
            return $result;
        } catch (\Throwable $e) {
            $fails = Cache::increment("cb:fail:{$this->key}");
            if ($fails >= $this->threshold) {
                Cache::put("cb:open:{$this->key}", true, $this->cooldown);
            }
            throw $e;
        }
    }
}

Wrap any client method call from inside the job: app(CircuitBreaker::class, ['key' => 'tikliveapi'])->guard(fn () => $service->capture($creator)).

Step 9: Request-Scoped Caching

During a single web request you might call userInfo() from a controller and again from a Blade component. Wrap repeated lookups with Cache::remember() using a short TTL so duplicate calls inside one request do not burn credits.

public function userInfoCached(string $username, int $ttl = 60): ?array
{
    return Cache::remember(
        "tla:userinfo:{$username}",
        $ttl,
        fn () => $this->userInfo($username),
    );
}

One minute is a sensible default for browser-driven lookups. For background analytics jobs prefer no caching at all so each snapshot is honest about the moment it was captured.

Step 10: Lazy Posts Endpoint

Now wire a route that returns posts for any creator and lazily fetches from upstream if your database does not have them yet. The user-posts response paginates with a cursor string and a camelCase hasMore boolean, with the items themselves arriving as flat snake_case rows.

// routes/api.php
Route::get('/creators/{username}/posts', function (string $username, TikLiveApi $api) {
    $creator = TikTokCreator::firstOrCreate(['username' => $username]);
    if (!$creator->tiktok_user_id) {
        $creator->update(['tiktok_user_id' => $api->userId($username)]);
    }

    $cursor = request('cursor', '0');
    $payload = Cache::remember(
        "tla:posts:{$creator->tiktok_user_id}:{$cursor}",
        60,
        fn () => $api->userPosts($creator->tiktok_user_id, 20, $cursor),
    );

    return response()->json([
        'videos'  => $payload['videos']  ?? [],
        'cursor'  => $payload['cursor']  ?? '0',
        'hasMore' => $payload['hasMore'] ?? false,
    ]);
});

Individual videos returned here carry aweme_id, play, wmplay, play_count, and digg_count. If you need the HD no-watermark URL, call /post-detail/ with the video URL, which returns hdplay alongside play and wmplay in a flat snake_case payload.

A Filament or Nova Admin Sketch

A simple admin panel for browsing snapshots is one Filament resource away. Run php artisan make:filament-resource TikTokCreator --generate and expose a table with columns for username, nickname, and last_synced_at. Add a relation manager for snapshots with a chart widget that pulls follower_count over the last 30 days. Same idea with Nova: a Resource for the creator and a HasMany field for snapshots is enough to navigate your archive without writing Blade.

Drop in a quick action button labelled Capture now that dispatches ScheduleCreatorSnapshot::dispatchSync($record). Operators get an instant refresh when they need to debug a creator drift.

Testing with Http::fake()

Mocking TikLiveAPI keeps tests fast and free. Use Laravel's built-in faker against the verified response shapes:

use Illuminate\Support\Facades\Http;

it('captures a snapshot from the upstream user-info response', function () {
    Http::fake([
        'api.tikliveapi.com/userinfo-by-username/*' => Http::response([
            'user'  => ['id' => '9876', 'nickname' => 'Ada', 'verified' => true],
            'stats' => [
                'followerCount'  => 12000,
                'followingCount' => 42,
                'heartCount'     => 380000,
                'videoCount'     => 88,
            ],
        ]),
    ]);

    $creator = TikTokCreator::create(['username' => 'ada']);
    app(SnapshotService::class)->capture($creator);

    expect($creator->snapshots()->count())->toBe(1)
        ->and($creator->snapshots()->first()->follower_count)->toBe(12000);
});

For the followers endpoint, remember to fake a payload that uses time rather than cursor, and for following, that the top key is followings with a trailing s. For /post-comments/ tests, comment items use id, not cid.

Deployment Notes

Run Laravel Horizon for queue monitoring. Pin the snapshot queue to its own connection so it cannot starve your web request queue. A sensible Horizon supervisor allocates two processes for tiktok-snapshots with a balance of auto and a max of 10 concurrent jobs, which lines up with the average 750 ms upstream latency without overwhelming smaller plans.

Cost projection is simple because TikLiveAPI is pay-as-you-go: one request equals one credit. Snapshotting 200 creators every day consumes 6,000 credits per month for the userInfo call alone. Adding a daily user-posts fetch with one page each doubles it. Plan against your credit balance accordingly and instrument the SnapshotService to count successful captures so you can reconcile against the dashboard's daily counters.

For the cron itself, point your server's system cron at * * * * * cd /var/www/app && php artisan schedule:run, then let Laravel decide when 03:00 actually fires. On multi-server setups the onOneServer() modifier prevents duplicate fan-outs.

FAQ

Does TikLiveAPI expose a TikTok Live (livestream) endpoint?

No. There is no dedicated livestream endpoint. For Live content that appears in a creator's profile you can use /user-posts/, /user-stories/, or /post-detail/ against the regular post and profile endpoints.

Which header should the Laravel HTTP client send?

Always X-Api-Key with your TikLiveAPI key. No bearer prefix, no query string fallback. The example client above sets it once on the PendingRequest so every method inherits it.

How do I paginate followers versus posts?

The /user-followers/ and /user-following/ endpoints paginate with a time integer that you echo back from the previous response. Almost everything else (posts, comments, search) uses a cursor string plus a hasMore boolean. Mixing them up is the most common integration bug.

Can I rely on Laravel's HTTP retry to handle transient errors?

For most cases yes. The example client retries three times with a short backoff and throw: false, which lets you decide per call whether to escalate. Combine it with the small CircuitBreaker service shown above so a sustained outage does not pile up failing jobs.

Where do I test calls before writing code?

Open the interactive Playground. It proxies through the dashboard with your key injected server side, so you can inspect the exact JSON shape before binding Eloquent models. The same data drives the full documentation, and if you want to debug request flow visually, head to contact or browse the blog for related write-ups.

Build with the TikTok API

Ready to put what you read into code? Try our endpoints live or grab the full reference.

Open Playground Read Documentation