kelvinzer0/curl-impersonate-php
PHP wrapper for curl-impersonate that makes HTTP requests mimic real browser TLS and HTTP/2 fingerprints, helping bypass bot detection and anti-scraping checks while keeping the familiar cURL-style API.
The package is a thin PHP wrapper around a patched curl build (curl-impersonate). The PHP code is the easy part — the real dependency is the native binary. Budget most of your first hour for that.
kelvinzer0/curl-impersonate-php does not ship the impersonating curl. You need the curl-impersonate-chrome / curl-impersonate-firefox binaries present on the host (or a Docker base image that includes them). Confirm with curl-impersonate-chrome --version and curl-impersonate-chrome -V | grep -i nghttp2.composer require kelvinzer0/curl-impersonate-php
get()/post(), a Guzzle-style client, or a fluent builder, so confirm before writing code around it.403 with a Cloudflare/Akamai page to vanilla curl or Guzzle. Fetch it with the wrapper's Chrome profile and confirm you now get the real HTML, plus cf-ray/server headers instead of a challenge page.A typical shape (verify names against the source — this family of packages is not consistent):
$client = new CurlImpersonate('chrome'); // profile / binary selector
$client->setHeaders(['Accept-Language' => 'en-US,en;q=0.9']);
$response = $client->get('https://target.example/protected');
Validate immediately against a fingerprint echo service (https://tls.peet.ws/api/all or tls.browserleaks.com/json). If the JA3/JA4 there matches stock curl instead of Chrome, your binary path is wrong and everything downstream is a false sense of security.
Bind it into the container. Wrap the client in a small ImpersonatedHttpClient service and register it in a service provider. Don't instantiate the wrapper in controllers — you want one place to control profile selection, proxy, and timeouts, and one place to swap in a fake during tests.
Match the whole profile, not just TLS. The point is consistency: TLS ClientHello + HTTP/2 SETTINGS/priority frames + header order + User-Agent. Pick a profile (chrome, chrome110, firefox, safari) and derive the UA and Accept* headers from that profile's real browser version. Hard-coding a random modern UA on top of an older Chrome fingerprint is worse than doing nothing.
Cookie jar per session. Multi-step flows (landing page → CSRF token → login POST → authenticated fetch) need a persistent cookie jar on the same client instance. Reuse the instance within a job; create a fresh one per account to avoid cross-contamination.
Pair with proxies, and treat IP reputation as the primary variable. For most anti-bot stacks today, fingerprint gets you past the handshake and IP/ASN reputation determines whether you see a challenge. Rotate proxies per request or per session depending on whether the flow is stateful, and keep the proxy sticky for login sequences.
Queue it and rate-limit per domain. These calls are slow (hundreds of ms to seconds) and get you banned fast if bursty. A Laravel job per URL with RateLimiter::for('target-domain') and a release() backoff on 429/403 is the standard pattern.
Classify failures explicitly. Distinguish:
403 with a JS challenge body → fingerprint/IP insufficient, escalate to a headless browser.429 / 503 with Retry-After → back off, don't rotate aggressively.200 but empty/blocked content → you're being soft-blocked; inspect the body, don't trust the status code.Build this into a small result object (ok, challenged, blocked, error) so downstream jobs can act on it.
Keep curl-impersonate as one tier in a ladder. Pattern that works: plain Guzzle → curl-impersonate → headless browser (Playwright/Puppeteer) → manual/paid unblocker. Route up the ladder only when the cheaper tier fails, and cache which tier worked per domain.
Testing. Wrap the client behind an interface and bind a fake in tests that returns recorded fixtures. You do not want CI depending on a native binary and live anti-bot endpoints.
curl-impersonate-chrome -V for nghttp2.chrome110 profile in 2026 is a red flag on its own. Pin the binary version in your Dockerfile and schedule upgrades.Host, Connection, sec-ch-ua, User-Agent, Accept, Accept-Language, ...) or use the profile's default and override sparingly.Http facade. Http::get() uses Guzzle and will silently bypass your impersonating client. Route all protected fetches through the wrapper's service class.server, cf-ray, response size, and a hash of the first 1KB of body for failures. Anti-bot behavior changes quietly; without this you can't tell when a profile stopped working.How can I help you explore Laravel packages today?