> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP client

> Use Laravel's HTTP client to send requests to external APIs with a clean, fluent interface that supports authentication, retries, testing, and concurrent requests.

## What is the HTTP client?

Laravel's HTTP client wraps [Guzzle](https://docs.guzzlephp.org/en/stable/) with a concise, expressive API. Access it through the `Http` facade to send requests to external web services.

```php theme={null}
use Illuminate\Support\Facades\Http;

$response = Http::get('https://api.example.com/users');
```

<Info>
  Guzzle is included with Laravel, so no extra installation is required.
</Info>

## Making requests

### GET

```php theme={null}
use Illuminate\Support\Facades\Http;

$response = Http::get('https://api.example.com/users');

// Pass query parameters as an array
$response = Http::get('https://api.example.com/users', [
    'page'     => 1,
    'per_page' => 20,
]);
```

### POST

Data is sent as `application/json` by default:

```php theme={null}
$response = Http::post('https://api.example.com/users', [
    'name'  => 'Taylor Otwell',
    'email' => 'taylor@example.com',
]);
```

### PUT, PATCH, DELETE

```php theme={null}
$response = Http::put('https://api.example.com/users/1', [
    'name' => 'Taylor Otwell',
]);

$response = Http::patch('https://api.example.com/users/1', [
    'email' => 'taylor@example.com',
]);

$response = Http::delete('https://api.example.com/users/1');
```

## Handling responses

Every request method returns an `Illuminate\Http\Client\Response` instance:

```php theme={null}
$response = Http::get('https://api.example.com/users/1');

// Body
$response->body();        // Raw string
$response->json();        // Decoded array
$response->json('name');  // Single key from JSON
$response->object();      // stdClass object
$response->collect();     // Laravel Collection

// Status
$response->status();      // HTTP status code
$response->successful();  // True for 2xx
$response->failed();      // True for 4xx and 5xx
$response->clientError(); // True for 4xx
$response->serverError(); // True for 5xx

// Common status helpers
$response->ok();                   // 200
$response->created();              // 201
$response->noContent();            // 204
$response->notFound();             // 404
$response->unauthorized();         // 401
$response->forbidden();            // 403
$response->unprocessableEntity();  // 422
$response->tooManyRequests();      // 429
```

JSON responses support array access:

```php theme={null}
$name = Http::get('https://api.example.com/users/1')['name'];
```

## Request options

### Headers

```php theme={null}
$response = Http::withHeaders([
    'X-Api-Version'  => '2',
    'Accept-Language' => 'en',
])->get('https://api.example.com/users');

// Shorthand to accept JSON
$response = Http::acceptJson()->get('https://api.example.com/users');
```

<Info>
  For browser-side AJAX requests to your Laravel app (not outbound server-to-server calls), see [CSRF protection](/en/csrf) for `X-CSRF-TOKEN` and `X-XSRF-TOKEN` usage.
</Info>

### Authentication

```php theme={null}
// Bearer token (most common)
$response = Http::withToken($token)->get('https://api.example.com/me');

// Basic auth
$response = Http::withBasicAuth('user@example.com', 'password')
    ->get('https://api.example.com/private');
```

### Base URL

When you make multiple requests to the same host, set a base URL once:

```php theme={null}
$response = Http::baseUrl('https://api.example.com')
    ->withToken($token)
    ->get('/users/1');
```

### Form data

Send data as `application/x-www-form-urlencoded`:

```php theme={null}
$response = Http::asForm()->post('https://api.example.com/login', [
    'username' => 'taylor',
    'password' => 'secret',
]);
```

### Timeouts

```php theme={null}
// Response timeout (default: 30 seconds)
$response = Http::timeout(10)->get('https://api.example.com/slow-endpoint');

// Connection timeout (default: 10 seconds)
$response = Http::connectTimeout(5)->get('https://api.example.com/endpoint');
```

<Warning>
  Exceeding the timeout throws `Illuminate\Http\Client\ConnectionException`. Always set a timeout when calling external APIs.
</Warning>

### Retries

Automatically retry failed requests:

```php theme={null}
// Retry up to 3 times, waiting 100 ms between attempts
$response = Http::retry(3, 100)->post('https://api.example.com/orders', $data);

// Retry only on connection errors
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\ConnectionException;

$response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) {
    return $exception instanceof ConnectionException;
})->post('https://api.example.com/orders', $data);
```

## Error handling

The HTTP client does **not** throw an exception for 4xx or 5xx responses by default. Check the status manually:

```php theme={null}
$response = Http::get('https://api.example.com/users/999');

if ($response->notFound()) {
    // Handle 404
}

if ($response->failed()) {
    logger()->error('API request failed', ['status' => $response->status()]);
}
```

### Throwing on error

Call `throw()` to turn an error response into an `Illuminate\Http\Client\RequestException`:

```php theme={null}
// Throws for any 4xx / 5xx response
$response = Http::post('https://api.example.com/users', $data)->throw();

// Throw only when a condition is true
$response->throwIf($response->status() === 422);

// Throw unless the response matches the expected status
$response->throwUnlessStatus(201);
```

Because `throw()` returns the response instance, you can chain further calls:

```php theme={null}
$user = Http::post('https://api.example.com/users', $data)
    ->throw()
    ->json();
```

Catch the exceptions separately:

```php theme={null}
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;

try {
    $response = Http::timeout(5)
        ->post('https://api.example.com/users', $data)
        ->throw();
} catch (ConnectionException $e) {
    // Timeout or network failure
} catch (RequestException $e) {
    // 4xx / 5xx error — $e->response holds the response
    logger()->error('API error', ['status' => $e->response->status()]);
}
```

## Concurrent requests

Send multiple requests simultaneously with `pool()`:

```php theme={null}
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;

$responses = Http::pool(fn (Pool $pool) => [
    $pool->as('users')->get('https://api.example.com/users'),
    $pool->as('posts')->get('https://api.example.com/posts'),
    $pool->as('comments')->get('https://api.example.com/comments'),
]);

$users    = $responses['users']->json();
$posts    = $responses['posts']->json();
$comments = $responses['comments']->json();
```

<Tip>
  Concurrent requests are ideal for dashboards that aggregate data from multiple endpoints. See the [Concurrency](/en/concurrency) guide for running other types of concurrent tasks beyond HTTP requests.
</Tip>

## Testing

### Faking responses

Use `Http::fake()` to intercept requests in tests without hitting real URLs:

```php theme={null}
use Illuminate\Support\Facades\Http;

// Return 200 for every request
Http::fake();

// Return specific responses per URL pattern
Http::fake([
    'api.example.com/users/*' => Http::response(['id' => 1, 'name' => 'Taylor'], 200),
    'api.example.com/posts/*' => Http::response(['error' => 'Not Found'], 404),
    '*'                        => Http::response('OK', 200),
]);
```

Return responses in sequence:

```php theme={null}
Http::fake([
    'api.example.com/*' => Http::sequence()
        ->push(['id' => 1], 200)
        ->push(['id' => 2], 200)
        ->pushStatus(429),
]);
```

### Asserting requests

Verify that your code sent the expected requests:

```php theme={null}
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;

Http::fake();

Http::withToken('my-token')->post('https://api.example.com/users', [
    'name' => 'Taylor',
]);

Http::assertSent(function (Request $request) {
    return $request->url() === 'https://api.example.com/users'
        && $request->hasHeader('Authorization', 'Bearer my-token')
        && $request['name'] === 'Taylor';
});

Http::assertNotSent(fn (Request $request) => str_contains($request->url(), '/admin'));

Http::assertSentCount(1);
```

<Tip>
  Add `Http::preventStrayRequests()` to your test setup. Any request that does not match a fake URL will throw an exception, catching accidental real HTTP calls.
</Tip>

## Service class example

Encapsulate HTTP client logic in a dedicated service class for better testability and reuse.

<Steps>
  <Step title="Create a service class">
    ```php theme={null}
    <?php

    namespace App\Services;

    use Illuminate\Http\Client\ConnectionException;
    use Illuminate\Http\Client\RequestException;
    use Illuminate\Support\Facades\Http;

    class GitHubService
    {
        private string $baseUrl = 'https://api.github.com';

        public function __construct(
            private readonly string $token,
        ) {}

        /**
         * @throws ConnectionException
         * @throws RequestException
         */
        public function getUser(string $username): array
        {
            return Http::baseUrl($this->baseUrl)
                ->withToken($this->token)
                ->acceptJson()
                ->timeout(10)
                ->get("/users/{$username}")
                ->throw()
                ->json();
        }

        /**
         * @throws ConnectionException
         * @throws RequestException
         */
        public function getRepositories(string $username, int $page = 1): array
        {
            return Http::baseUrl($this->baseUrl)
                ->withToken($this->token)
                ->acceptJson()
                ->timeout(10)
                ->retry(2, 500)
                ->get("/users/{$username}/repos", [
                    'page'     => $page,
                    'per_page' => 30,
                    'sort'     => 'updated',
                ])
                ->throw()
                ->json();
        }
    }
    ```
  </Step>

  <Step title="Register in a service provider">
    ```php theme={null}
    // app/Providers/AppServiceProvider.php
    use App\Services\GitHubService;

    public function register(): void
    {
        $this->app->singleton(GitHubService::class, function () {
            return new GitHubService(
                token: config('services.github.token'),
            );
        });
    }
    ```

    ```php theme={null}
    // config/services.php
    'github' => [
        'token' => env('GITHUB_TOKEN'),
    ],
    ```
  </Step>

  <Step title="Use in a controller">
    ```php theme={null}
    <?php

    namespace App\Http\Controllers;

    use App\Services\GitHubService;
    use Illuminate\Http\Client\ConnectionException;
    use Illuminate\Http\Client\RequestException;
    use Illuminate\Http\JsonResponse;

    class GitHubController extends Controller
    {
        public function __construct(
            private readonly GitHubService $github,
        ) {}

        public function show(string $username): JsonResponse
        {
            try {
                $user = $this->github->getUser($username);
                return response()->json($user);
            } catch (ConnectionException) {
                return response()->json(['error' => 'Could not connect to GitHub API.'], 503);
            } catch (RequestException $e) {
                if ($e->response->status() === 404) {
                    return response()->json(['error' => 'User not found.'], 404);
                }
                return response()->json(['error' => 'GitHub API error.'], 502);
            }
        }
    }
    ```
  </Step>

  <Step title="Write a test">
    ```php theme={null}
    <?php

    namespace Tests\Unit\Services;

    use App\Services\GitHubService;
    use Illuminate\Support\Facades\Http;
    use Tests\TestCase;

    class GitHubServiceTest extends TestCase
    {
        private GitHubService $service;

        protected function setUp(): void
        {
            parent::setUp();

            Http::fake([
                'api.github.com/users/octocat' => Http::response([
                    'login'        => 'octocat',
                    'name'         => 'The Octocat',
                    'public_repos' => 8,
                ], 200),
                'api.github.com/users/notfound' => Http::response(
                    ['message' => 'Not Found'],
                    404
                ),
            ]);

            $this->service = new GitHubService(token: 'test-token');
        }

        public function test_it_fetches_a_user(): void
        {
            $user = $this->service->getUser('octocat');

            $this->assertEquals('octocat', $user['login']);
            $this->assertEquals('The Octocat', $user['name']);

            Http::assertSent(function ($request) {
                return $request->url() === 'https://api.github.com/users/octocat'
                    && $request->hasHeader('Authorization', 'Bearer test-token');
            });
        }
    }
    ```
  </Step>
</Steps>

## Quick reference

<AccordionGroup>
  <Accordion title="Request methods">
    | Method                       | Purpose                 |
    | ---------------------------- | ----------------------- |
    | `Http::get($url, $query)`    | GET request             |
    | `Http::post($url, $data)`    | POST request (JSON)     |
    | `Http::put($url, $data)`     | PUT request             |
    | `Http::patch($url, $data)`   | PATCH request           |
    | `Http::delete($url)`         | DELETE request          |
    | `->withToken($token)`        | Bearer authentication   |
    | `->withHeaders($headers)`    | Custom headers          |
    | `->timeout($seconds)`        | Response timeout        |
    | `->connectTimeout($seconds)` | Connection timeout      |
    | `->retry($times, $sleep)`    | Automatic retries       |
    | `->throw()`                  | Throw on error response |
    | `Http::fake()`               | Mock responses in tests |
    | `Http::pool($callback)`      | Concurrent requests     |
  </Accordion>

  <Accordion title="Response status helpers">
    | Method                  | Status code |
    | ----------------------- | ----------- |
    | `successful()`          | 2xx         |
    | `failed()`              | 4xx or 5xx  |
    | `clientError()`         | 4xx         |
    | `serverError()`         | 5xx         |
    | `ok()`                  | 200         |
    | `created()`             | 201         |
    | `notFound()`            | 404         |
    | `unauthorized()`        | 401         |
    | `forbidden()`           | 403         |
    | `unprocessableEntity()` | 422         |
    | `tooManyRequests()`     | 429         |
  </Accordion>

  <Accordion title="Best practices">
    * Wrap HTTP client logic in a service class, not directly in controllers.
    * Always set a timeout. External APIs can hang indefinitely without one.
    * Use `retry()` for transient errors such as network blips or rate limiting.
    * Call `Http::fake()` at the start of every test that involves HTTP calls.
    * Add `Http::preventStrayRequests()` to your test setup to catch accidental real requests.
    * Store API tokens in environment variables and reference them through `config/services.php`.
  </Accordion>
</AccordionGroup>


## Related topics

- [BlueskyManager and HasShortHand](/en/packages/laravel-bluesky/bluesky-manager.md)
- [Laravel Telescope: Practical Dev Techniques](/en/blog/telescope-introduction.md)
- [Upgrading From Laravel 8 to 9](/en/blog/upgrade-8-to-9.md)
- [Mail](/en/mail.md)
- [Testing](/en/packages/laravel-bluesky/testing.md)
