# PHP (/sdks/php)

<!-- agent-signals: reading_time_min: 11 · est_tokens: 5004 · updated: 2026-07-30 -->
Related: [Overview](/sdks/overview.md), [Python](/sdks/python.md), [Node](/sdks/node.md), [Go](/sdks/go.md), [Java](/sdks/java.md), [Ruby](/sdks/ruby.md)

## Installation [#installation]

The official PHP SDK is maintained in the Firecrawl monorepo at [apps/php-sdk](https://github.com/firecrawl/firecrawl/tree/main/apps/php-sdk).

To install the Firecrawl PHP SDK, add the dependency via Composer:

```bash
composer require firecrawl/firecrawl-sdk
```

<Note>
  Requires PHP 8.1 or later.
</Note>

### Laravel Integration [#laravel-integration]

The SDK includes first-class Laravel support with auto-discovery. After installing the package, publish the configuration file:

```bash
php artisan vendor:publish --provider="Firecrawl\Laravel\FirecrawlServiceProvider"
```

Then add your API key to your `.env` file:

```env
FIRECRAWL_API_KEY=fc-your-api-key
```

The following environment variables are supported:

| Variable                   | Default                     | Description                              |
| -------------------------- | --------------------------- | ---------------------------------------- |
| `FIRECRAWL_API_KEY`        | —                           | Your Firecrawl API key (required)        |
| `FIRECRAWL_API_URL`        | `https://api.firecrawl.dev` | API base URL                             |
| `FIRECRAWL_TIMEOUT`        | `300`                       | HTTP request timeout in seconds          |
| `FIRECRAWL_MAX_RETRIES`    | `3`                         | Automatic retries for transient failures |
| `FIRECRAWL_BACKOFF_FACTOR` | `0.5`                       | Exponential backoff factor in seconds    |

## Usage [#usage]

1. Get an API key from [firecrawl.dev](https://firecrawl.dev)
2. Set the API key as an environment variable named `FIRECRAWL_API_KEY`, or pass it with `FirecrawlClient::create(apiKey: ...)`

Here is a quick example using the current SDK API surface:

```php
use Firecrawl\Client\FirecrawlClient;
use Firecrawl\Models\CrawlOptions;
use Firecrawl\Models\ScrapeOptions;

$client = FirecrawlClient::fromEnv();

$doc = $client->scrape(
    'https://firecrawl.dev',
    ScrapeOptions::with(formats: ['markdown'])
);

$crawl = $client->crawl(
    'https://firecrawl.dev',
    CrawlOptions::with(limit: 5)
);

echo $doc->getMarkdown();
echo 'Crawled pages: ' . count($crawl->getData());
```

### Using the Laravel Facade [#using-the-laravel-facade]

In a Laravel application you can use the `Firecrawl` facade or dependency injection:

```php
use Firecrawl\Client\FirecrawlClient;
use Firecrawl\Laravel\Facades\Firecrawl;

// Via Facade
$doc = Firecrawl::scrape('https://example.com');

// Via Dependency Injection
class ScrapeController
{
    public function __construct(
        private readonly FirecrawlClient $firecrawl,
    ) {}

    public function index()
    {
        $doc = $this->firecrawl->scrape('https://example.com');
        return response()->json(['markdown' => $doc->getMarkdown()]);
    }
}
```

### Scraping a URL [#scraping-a-url]

To scrape a single URL, use the `scrape` method.

```php
use Firecrawl\Models\Document;
use Firecrawl\Models\ScrapeOptions;

$doc = $client->scrape(
    'https://firecrawl.dev',
    ScrapeOptions::with(
        formats: ['markdown', 'html'],
        onlyMainContent: true,
        waitFor: 5000,
    )
);

echo $doc->getMarkdown();
echo $doc->getMetadata()['title'] ?? '';
```

#### JSON Extraction [#json-extraction]

Extract structured JSON with `JsonFormat` via the `scrape` endpoint:

```php
use Firecrawl\Models\JsonFormat;
use Firecrawl\Models\ScrapeOptions;

$jsonFmt = JsonFormat::with(
    prompt: 'Extract the product name and price',
    schema: [
        'type' => 'object',
        'properties' => [
            'name' => ['type' => 'string'],
            'price' => ['type' => 'number'],
        ],
    ],
);

$doc = $client->scrape(
    'https://example.com/product',
    ScrapeOptions::with(formats: [$jsonFmt])
);

print_r($doc->getJson());
```

### Crawling a Website [#crawling-a-website]

To crawl a website and wait for completion, use `crawl`.

```php
use Firecrawl\Models\CrawlOptions;
use Firecrawl\Models\ScrapeOptions;

$job = $client->crawl(
    'https://firecrawl.dev',
    CrawlOptions::with(
        limit: 50,
        maxDiscoveryDepth: 3,
        scrapeOptions: ScrapeOptions::with(formats: ['markdown']),
    )
);

echo 'Status: ' . $job->getStatus();
echo 'Progress: ' . $job->getCompleted() . '/' . $job->getTotal();

foreach ($job->getData() as $page) {
    echo $page->getMetadata()['sourceURL'] ?? '';
}
```

### Start a Crawl [#start-a-crawl]

Start a job without waiting using `startCrawl`.

```php
use Firecrawl\Models\CrawlOptions;

$start = $client->startCrawl(
    'https://firecrawl.dev',
    CrawlOptions::with(limit: 100)
);

echo 'Job ID: ' . $start->getId();
```

### Checking Crawl Status [#checking-crawl-status]

Check crawl progress with `getCrawlStatus`.

```php
$status = $client->getCrawlStatus($start->getId());
echo 'Status: ' . $status->getStatus();
echo 'Progress: ' . $status->getCompleted() . '/' . $status->getTotal();
```

### Cancelling a Crawl [#cancelling-a-crawl]

Cancel a running crawl with `cancelCrawl`.

```php
$result = $client->cancelCrawl($start->getId());
print_r($result);
```

### Crawl Errors [#crawl-errors]

Fetch crawl-level errors (if any) with `getCrawlErrors`.

```php
$errors = $client->getCrawlErrors($start->getId());
print_r($errors);
```

### Mapping a Website [#mapping-a-website]

Discover links on a site using `map`.

```php
use Firecrawl\Models\MapOptions;

$data = $client->map(
    'https://firecrawl.dev',
    MapOptions::with(
        limit: 100,
        search: 'blog',
    )
);

foreach ($data->getLinks() as $link) {
    echo ($link['url'] ?? '') . ' - ' . ($link['title'] ?? '');
}
```

### Searching the Web [#searching-the-web]

Search with optional search settings using `search`.

```php
use Firecrawl\Models\SearchOptions;

$results = $client->search(
    'firecrawl web scraping',
    SearchOptions::with(limit: 10)
);

foreach ($results->getWeb() as $result) {
    echo ($result['title'] ?? '') . ' - ' . ($result['url'] ?? '');
}
```

### Batch Scraping [#batch-scraping]

Scrape multiple URLs in parallel using `batchScrape`.

```php
use Firecrawl\Models\BatchScrapeOptions;
use Firecrawl\Models\ScrapeOptions;

$job = $client->batchScrape(
    ['https://firecrawl.dev', 'https://firecrawl.dev/blog'],
    BatchScrapeOptions::with(
        options: ScrapeOptions::with(formats: ['markdown']),
    )
);

foreach ($job->getData() as $doc) {
    echo $doc->getMarkdown();
}
```

For manual async control, use `startBatchScrape`, `getBatchScrapeStatus`, and `cancelBatchScrape`:

```php
use Firecrawl\Models\BatchScrapeOptions;
use Firecrawl\Models\ScrapeOptions;

$start = $client->startBatchScrape(
    ['https://firecrawl.dev', 'https://firecrawl.dev/blog'],
    BatchScrapeOptions::with(
        options: ScrapeOptions::with(formats: ['markdown']),
    )
);

$status = $client->getBatchScrapeStatus($start->getId());
echo 'Batch status: ' . $status->getStatus();

$cancel = $client->cancelBatchScrape($start->getId());
print_r($cancel);
```

### Agent [#agent]

Run an AI-powered agent with `agent`.

```php
use Firecrawl\Models\AgentOptions;

$result = $client->agent(
    AgentOptions::with(
        prompt: 'Find the pricing plans for Firecrawl and compare them',
    )
);

print_r($result->getData());
```

With a JSON schema for structured output:

```php
use Firecrawl\Models\AgentOptions;

$result = $client->agent(
    AgentOptions::with(
        prompt: 'Extract pricing plan details',
        urls: ['https://firecrawl.dev'],
        schema: [
            'type' => 'object',
            'properties' => [
                'plans' => [
                    'type' => 'array',
                    'items' => [
                        'type' => 'object',
                        'properties' => [
                            'name' => ['type' => 'string'],
                            'price' => ['type' => 'string'],
                        ],
                    ],
                ],
            ],
        ],
    )
);

print_r($result->getData());
```

For manual async control, use `startAgent`, `getAgentStatus`, and `cancelAgent`:

```php
use Firecrawl\Models\AgentOptions;

$start = $client->startAgent(
    AgentOptions::with(
        prompt: 'Summarize what Firecrawl does in one sentence',
        urls: ['https://firecrawl.dev'],
    )
);

$status = $client->getAgentStatus($start->getId());
echo 'Agent status: ' . $status->getStatus();

$cancel = $client->cancelAgent($start->getId());
print_r($cancel);
```

### Usage & Metrics [#usage--metrics]

Check concurrency and remaining credits:

```php
use Firecrawl\Models\ConcurrencyCheck;
use Firecrawl\Models\CreditUsage;

$concurrency = $client->getConcurrency();
echo 'Concurrency: ' . $concurrency->getConcurrency() . '/' . $concurrency->getMaxConcurrency();

$credits = $client->getCreditUsage();
echo 'Remaining credits: ' . $credits->getRemainingCredits();
```

## Laravel AI SDK Tools [#laravel-ai-sdk-tools]

The SDK ships native tool classes for the [Laravel AI SDK](https://laravel.com/docs/ai-sdk) (`laravel/ai`), so agents can scrape, search, map, and crawl the web without an MCP server or manual HTTP calls.

```bash
composer require laravel/ai
```

<Note>
  Requires 

  `firecrawl/firecrawl-sdk`

   1.9.0 or later, plus 

  `laravel/ai`

   0.9 or later (PHP 8.3+, Laravel 12+). The tool classes only load when 

  `laravel/ai`

   is installed.
</Note>

The tools resolve the `FirecrawlClient` from the container, so your existing `config/firecrawl.php` and `FIRECRAWL_API_KEY` setup is reused as is:

```php
use Firecrawl\Laravel\Tools\FirecrawlScrape;
use Firecrawl\Laravel\Tools\FirecrawlSearch;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
use Stringable;

class ResearchAssistant implements Agent, HasTools
{
    use Promptable;

    public function instructions(): Stringable|string
    {
        return 'You are a research assistant. Use the Firecrawl tools to find and read web content.';
    }

    public function tools(): iterable
    {
        return [
            new FirecrawlScrape,
            new FirecrawlSearch,
        ];
    }
}

$response = ResearchAssistant::make()->prompt('What does firecrawl.dev do?');
```

### Available Tools [#available-tools]

| Class             | Tool name          | What it does                             |
| ----------------- | ------------------ | ---------------------------------------- |
| `FirecrawlScrape` | `firecrawl_scrape` | Scrape one URL and return clean markdown |
| `FirecrawlSearch` | `firecrawl_search` | Search the web, returns JSON results     |
| `FirecrawlMap`    | `firecrawl_map`    | Discover the URLs on a website           |
| `FirecrawlCrawl`  | `firecrawl_crawl`  | Crawl multiple pages into markdown       |

The tool names match the Firecrawl MCP server, so agents see the same vocabulary across surfaces. Register all four at once with the spread helper:

```php
use Firecrawl\Laravel\Tools\FirecrawlTools;

public function tools(): iterable
{
    return [...FirecrawlTools::all()];
}
```

Every tool also accepts an explicit client, for one off credentials or use outside the container. `FirecrawlTools::all()` passes one to all four tools:

```php
use Firecrawl\Client\FirecrawlClient;

$client = FirecrawlClient::create(apiKey: 'fc-other-key');

new FirecrawlScrape($client);
// or
FirecrawlTools::all($client);
```

### Tool Parameters [#tool-parameters]

Each tool exposes a small, model-facing schema. These are the parameters the agent can pass:

| Tool               | Parameter          | Description                                              |
| ------------------ | ------------------ | -------------------------------------------------------- |
| `firecrawl_scrape` | `url` (required)   | Absolute URL of the page to scrape, including the scheme |
| `firecrawl_search` | `query` (required) | The search query                                         |
|                    | `limit`            | Maximum results to return, 1–20. Defaults to 5           |
| `firecrawl_map`    | `url` (required)   | Base URL of the website to map                           |
|                    | `search`           | Optional term to filter discovered URLs by relevance     |
|                    | `limit`            | Maximum URLs to return, 1–500. Defaults to 100           |
| `firecrawl_crawl`  | `url` (required)   | URL to start crawling from                               |
|                    | `limit`            | Maximum pages to crawl, 1–25. Defaults to 5              |

Out-of-range `limit` values are clamped to the nearest bound rather than rejected, so a model that asks for 99 search results gets 20 instead of an error.

### Tool Behavior [#tool-behavior]

Tool failures such as rate limits, timeouts, and invalid URLs are returned to the model as readable error strings rather than thrown, so agent runs degrade gracefully. Outputs are capped to stay within model context: scrape results truncate at 80,000 characters, crawl pages at 15,000 characters each under a 100,000 character whole result budget, and search and map results drop tail items with an explicit omitted marker.

`firecrawl_search` and `firecrawl_map` return JSON arrays of results. `firecrawl_scrape` returns the page as markdown.

### Crawl Results [#crawl-results]

`firecrawl_crawl` waits up to 55 seconds for the crawl to finish, then returns a JSON object that makes the outcome explicit. Failed, cancelled, or partial crawls stay visible to the model through the `status` field rather than being silently truncated:

```json
{
  "status": "completed",
  "completed": 5,
  "total": 5,
  "pages": [
    { "url": "https://example.com/docs", "markdown": "..." }
  ]
}
```

Two optional fields appear when results don't fit: `omittedPages` counts pages dropped to stay inside the output budget, and `note` tells the model that more pages exist on the server and that it should use a smaller limit or scrape specific pages with `firecrawl_scrape`. The tool reports pagination instead of following it, so agents that need every page of a large crawl should use `FirecrawlClient` directly.

If the crawl is still running when the wait expires, the tool says so and reminds the model the crawl may still complete server-side. Crawl starts carry a UUID idempotency key, so an HTTP-level retry never creates a duplicate crawl.

If your agent runs inside a queued job, keep the crawl limit small or raise the worker's job timeout. The wait, poll cadence, and per-page cap are protected properties, so extend the class to tune them:

```php
use Firecrawl\Laravel\Tools\FirecrawlCrawl;

class PatientCrawl extends FirecrawlCrawl
{
    protected int $timeoutSeconds = 120;
    protected int $pollIntervalSeconds = 5;
    protected int $pageCharacterLimit = 30000;
}
```

## Browser [#browser]

The PHP SDK includes Browser Sandbox helpers.

### Create a Session [#create-a-session]

```php
use Firecrawl\Models\BrowserCreateResponse;

$session = $client->browser(ttl: 120, activityTtl: 60, streamWebView: true);
echo $session->getId();
echo $session->getCdpUrl();
echo $session->getLiveViewUrl();
```

### Execute Code [#execute-code]

```php
use Firecrawl\Models\BrowserExecuteResponse;

$run = $client->browserExecute(
    sessionId: $session->getId(),
    code: 'await page.goto("https://example.com"); console.log(await page.title());',
    language: 'node',
    timeout: 60,
);

echo $run->getStdout();
echo $run->getExitCode();
```

### Scrape-Bound Interactive Session [#scrape-bound-interactive-session]

Use a scrape job ID to run follow-up browser code in the same replayed context:

* `interact(...)` runs code in the scrape-bound browser session (and initializes it on first use).
* `stopInteractiveBrowser(...)` explicitly stops the interactive session when you are done.

```php
use Firecrawl\Models\BrowserExecuteResponse;
use Firecrawl\Models\BrowserDeleteResponse;
use Firecrawl\Models\ScrapeOptions;

$doc = $client->scrape(
    'https://example.com',
    ScrapeOptions::with(formats: ['markdown'])
);

$scrapeJobId = $doc->getMetadata()['scrapeId'] ?? null;
if ($scrapeJobId === null) {
    throw new RuntimeException('scrapeId not found in metadata');
}

$scrapeRun = $client->interact(
    jobId: $scrapeJobId,
    code: 'console.log(page.url());',
    language: 'node',
    timeout: 60,
);

echo $scrapeRun->getStdout();

$deleted = $client->stopInteractiveBrowser($scrapeJobId);
echo 'Deleted: ' . ($deleted->isSuccess() ? 'true' : 'false');
```

### List & Close Sessions [#list--close-sessions]

```php
use Firecrawl\Models\BrowserListResponse;
use Firecrawl\Models\BrowserSession;

$active = $client->listBrowsers('active');
foreach ($active->getSessions() as $s) {
    echo $s->getId() . ' - ' . $s->getStatus();
}

$closed = $client->deleteBrowser($session->getId());
echo 'Closed: ' . ($closed->isSuccess() ? 'true' : 'false');
```

## Configuration [#configuration]

`FirecrawlClient::create()` supports the following options:

| Option           | Type                         | Default                                              | Description                              |
| ---------------- | ---------------------------- | ---------------------------------------------------- | ---------------------------------------- |
| `apiKey`         | `string`                     | `FIRECRAWL_API_KEY` env var                          | Your Firecrawl API key                   |
| `apiUrl`         | `string`                     | `https://api.firecrawl.dev` (or `FIRECRAWL_API_URL`) | API base URL                             |
| `timeoutSeconds` | `float`                      | `300`                                                | HTTP request timeout in seconds          |
| `maxRetries`     | `int`                        | `3`                                                  | Automatic retries for transient failures |
| `backoffFactor`  | `float`                      | `0.5`                                                | Exponential backoff factor in seconds    |
| `httpClient`     | `GuzzleHttp\ClientInterface` | Built from timeout                                   | Custom Guzzle-compatible HTTP client     |

```php
use Firecrawl\Client\FirecrawlClient;

$client = FirecrawlClient::create(
    apiKey: 'fc-your-api-key',
    apiUrl: 'https://api.firecrawl.dev',
    timeoutSeconds: 300,
    maxRetries: 3,
    backoffFactor: 0.5,
);
```

### Custom HTTP Client [#custom-http-client]

You can pass a pre-configured `GuzzleHttp\ClientInterface` implementation to control connection pooling, middleware, proxy settings, and other HTTP features. When provided, the `timeoutSeconds` setting is ignored in favor of the client's own configuration.

```php
use Firecrawl\Client\FirecrawlClient;
use GuzzleHttp\Client as GuzzleClient;

$guzzle = new GuzzleClient([
    'proxy' => 'http://proxy.example.com:8080',
    'timeout' => 60,
    'connect_timeout' => 10,
]);

$client = FirecrawlClient::create(
    apiKey: 'fc-your-api-key',
    httpClient: $guzzle,
);
```

## Error Handling [#error-handling]

The SDK throws runtime exceptions under `Firecrawl\Exceptions`.

```php
use Firecrawl\Exceptions\AuthenticationException;
use Firecrawl\Exceptions\FirecrawlException;
use Firecrawl\Exceptions\JobTimeoutException;
use Firecrawl\Exceptions\RateLimitException;

try {
    $doc = $client->scrape('https://example.com');
} catch (AuthenticationException $e) {
    echo 'Auth failed: ' . $e->getMessage();
} catch (RateLimitException $e) {
    echo 'Rate limited: ' . $e->getMessage();
} catch (JobTimeoutException $e) {
    echo 'Job ' . $e->getJobId() . ' timed out after ' . $e->getTimeoutSeconds() . 's';
} catch (FirecrawlException $e) {
    echo 'Error ' . $e->getStatusCode() . ': ' . $e->getMessage();
}
```

> Are you an AI agent that needs a Firecrawl API key? See [firecrawl.dev/agent-onboarding/SKILL.md](https://www.firecrawl.dev/agent-onboarding/SKILL.md) for automated onboarding instructions.
