aporat / scrapecreators-php
PHP client for the Scrape Creators API: typed methods for every documented endpoint across TikTok, Instagram, YouTube, X, Reddit, LinkedIn, Facebook and 20 more platforms, provider-side cache control, typed exceptions for missing, restricted and age-gated profiles, and an optional Laravel service pr
Requires
- php: ^8.4
- ext-json: *
- guzzlehttp/guzzle: ^7.9 || ^8.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- illuminate/support: ^12.0 || ^13.0
- laravel/pint: ^1.21
- orchestra/testbench: ^10.0 || ^11.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^12.0 || ^13.0
Suggests
- illuminate/support: Registers the client in a Laravel container from config/scrapecreators.php (^12 || ^13)
Provides
None
Conflicts
None
Replaces
None
README
A PHP client for the Scrape Creators API: a typed method for every documented endpoint across TikTok, Instagram, YouTube, X, Reddit, LinkedIn, Facebook, GitHub, Threads, Bluesky, Spotify and twenty more platforms, control of the provider-side cache that turns repeat lookups into zero-credit calls, and exceptions that tell a missing profile from a restricted one from an outage, including the HTTP 200 placeholders the API uses for some of them. Works in any PHP project; a service provider is included for Laravel.
Not affiliated with Scrape Creators.
✨ Features
- Every endpoint, typed. 251 methods on 27 platform resources, generated from the API's OpenAPI document: required parameters are positional, optional ones are named, each carries the API's own description and allowed values.
$client->instagram()->profile('nasa', trim: true). Anything newer than this package is reachable throughget()/post()or the$extraparameter every method takes - Provider-side cache, handled. Set a default
cache_max_ageonce and the client sends it to every endpoint that supports it; Scrape Creators then answers from a recent scrape for 0 credits. PassClient::LIVEon a call to force a fresh scrape - Errors you can branch on.
NotFoundExceptionfor a missing account,RestrictedExceptionfor a private, age-gated or region-blocked one (withisAgeRestricted()andisPrivate()),UnavailableExceptionfor their outage or a timeout,InsufficientCreditsException,AuthenticationException,InvalidRequestException,RateLimitException. The API reports several of these states as HTTP 200 placeholders; the client reads the envelope so you do not have to - Billing in view. Every
ResponseexposescreditsCharged(),creditsRemaining(),isCached()andcachedAt()alongsidedata()and dot-pathget() - Laravel ready. Auto-discovered service provider,
config/scrapecreators.php,Clientbound as a singleton
🛠 Installation
composer require aporat/scrapecreators-php
Requires PHP 8.4+ and Guzzle 7.9 or 8. Get an API key at app.scrapecreators.com.
use Aporat\ScrapeCreators\Client; $client = new Client($apiKey, [ 'cache_max_age' => '1d', // optional: accept scrapes up to a day old for 0 credits 'timeout' => 20, // seconds, the default ]); $profile = $client->instagram()->profile('nasa'); $profile->get('data.user.follower_count'); $profile->creditsCharged(); // 1 for a live scrape, 0 from the cache $profile->isCached();
📚 Usage
Platforms
One accessor per platform, one method per endpoint. Method names follow the path: /v1/tiktok/video/transcript is $client->tiktok()->videoTranscript($url), /v3/tiktok/profile/videos is profileVideos(), /v1/account/get-api-usage is $client->account()->apiUsage().
$client->tiktok()->profile(handle: 'nasa'); $client->tiktok()->profileVideos(handle: 'nasa', sortBy: 'popular'); $client->youtube()->channel(handle: '@nasa'); $client->youtube()->videoTranscript('https://www.youtube.com/watch?v=...'); $client->twitter()->userTweets(handle: 'nasa'); $client->reddit()->subreddit(subreddit: 'php', sort: 'top', timeframe: 'week'); $client->linkedin()->company(url: 'https://www.linkedin.com/company/nasa'); $client->threads()->profile(handle: 'nasa'); $client->bluesky()->profile(handle: 'nasa.bsky.social'); $client->spotify()->artist(url: 'https://open.spotify.com/artist/...'); $client->github()->user(handle: 'aporat'); $client->creatorTools()->findSocialProfiles('instagram', 'nasa'); $client->linkInBio()->linktree(url: 'https://linktr.ee/nasa'); $client->account()->creditBalance();
| Accessor | Platform | Accessor | Platform |
|---|---|---|---|
tiktok() |
TikTok, TikTok Shop, TikTok Ad Library | twitch() |
Twitch |
instagram() |
appleMusic() |
Apple Music | |
youtube() |
YouTube | spotify() |
Spotify |
facebook() |
Facebook, Marketplace, Events, Ad Library | soundCloud() |
SoundCloud |
twitter() |
X / Twitter | kwai() |
Kwai |
reddit() |
kick() |
Kick | |
linkedin() |
LinkedIn, LinkedIn Ad Library | snapchat() |
Snapchat |
github() |
GitHub | telegram() |
Telegram |
google() |
Google search, Google Ad Library | cosmos() |
Cosmos |
threads() |
Threads | rumble() |
Rumble |
bluesky() |
Bluesky | creatorTools() |
find social profiles, find creator emails, detect age and gender |
truthSocial() |
Truth Social | linkInBio() |
Linktree, Komi, Pillar, Linkbio, Linkme |
pinterest() |
amazon() |
Amazon shop | |
account() |
credit balance, usage |
The three endpoints that also accept a JSON body have a ...Post() twin taking the body as an array: facebook()->adLibrarySearchAdsPost([...]), facebook()->adLibraryCompanyAdsPost([...]), reddit()->postCommentsPost([...]).
Any endpoint
$client->get('v1/instagram/profile', ['handle' => 'nasa', 'trim' => true], cacheable: true); $client->post('v1/reddit/post/comments', ['url' => $url]);
Booleans are sent as true / false, nulls are dropped, enums are sent by value. cacheable says whether the endpoint takes cache_max_age, so the client's default applies.
Responses
$response = $client->instagram()->profile('nasa'); $response->data(); // the `data` member, or the whole envelope when there is none $response->get('data.user.biography'); // dot path, with an optional default $response['data']['user']; // array access to the envelope $response->toArray(); $response->creditsCharged(); // int|null $response->creditsRemaining(); // int|null $response->isCached(); // served from Scrape Creators' cache (0 credits) $response->cachedAt(); // DateTimeImmutable|null $response->raw(); // the PSR-7 response
The provider-side cache
Thirty-six endpoints take cache_max_age (1d, 3d, 7d, 14d, 30d): when Scrape Creators holds a scrape at least that fresh it returns it for 0 credits, marked cached: true with cached_at. Set a default once and override per call:
$client = new Client($apiKey, ['cache_max_age' => '1d']); $client->instagram()->profile('nasa'); // cache_max_age=1d $client->instagram()->profile('nasa', cacheMaxAge: '7d'); // cache_max_age=7d $client->instagram()->profile('nasa', cacheMaxAge: Client::LIVE); // no cache_max_age: a live scrape
A cached profile carries whatever signed CDN image URLs the scrape saw, and networks expire those within days. Keep the default short if you download pictures.
Errors
use Aporat\ScrapeCreators\Exceptions\InsufficientCreditsException; use Aporat\ScrapeCreators\Exceptions\NotFoundException; use Aporat\ScrapeCreators\Exceptions\RestrictedException; use Aporat\ScrapeCreators\Exceptions\ScrapeCreatorsException; use Aporat\ScrapeCreators\Exceptions\UnavailableException; try { $profile = $client->instagram()->profile($handle); } catch (NotFoundException) { // No such account. Safe to cache as a miss. } catch (RestrictedException $e) { // The account exists but the network will not show it: $e->isAgeRestricted(), $e->isPrivate(). // Do not reject the handle and do not cache it as missing; restrictions lift. } catch (UnavailableException $e) { // Their outage, or a timeout. Says nothing about the account; retry later. } catch (InsufficientCreditsException) { // Top up. } catch (ScrapeCreatorsException $e) { $e->getStatusCode(); // int|null $e->getApiMessage(); // the envelope's `message`, when there was one $e->getBody(); // the decoded envelope $e->getResponse(); // the PSR-7 response }
How responses map:
| What Scrape Creators sends | Exception |
|---|---|
| 400 | InvalidRequestException |
| 401 | AuthenticationException (also thrown before any request when the key is empty) |
| 402 | InsufficientCreditsException |
403, or any envelope with age_restricted: true or a message saying "restricted" or "private" |
RestrictedException |
404, any other 4xx, or a 200 with error: "not_found", account_deactivated: true, userId: null or a 4xx errorStatus |
NotFoundException |
| 429 | RateLimitException |
5xx, a 200 with a 5xx errorStatus, or no response at all |
UnavailableException |
| a body that is not a JSON object | InvalidResponseException |
success: false with anything else |
ScrapeCreatorsException |
🧱 Laravel
The service provider is auto-discovered and binds Aporat\ScrapeCreators\Client as a singleton. Publish the config and set the key:
php artisan vendor:publish --tag=scrapecreators-config
SCRAPECREATORS_API_KEY=sc_... SCRAPECREATORS_CACHE_MAX_AGE=1d SCRAPECREATORS_TIMEOUT=20
public function __construct(private readonly \Aporat\ScrapeCreators\Client $scrapeCreators) {}
In tests, bind a client built on a Guzzle MockHandler, or $this->app->instance(Client::class, ...).
🔁 Keeping up with the API
spec/openapi.json is a snapshot of docs.scrapecreators.com/openapi.json. src/Resources is generated from it:
curl -sSL -o spec/openapi.json https://docs.scrapecreators.com/openapi.json composer generate
CI fails when the committed resources differ from what the snapshot produces. Until a new endpoint lands here, call it with get() / post().
📄 License
MIT. See LICENSE.