# Consuming Third-Party APIs: HTTP Client with Guzzle

> Connecting to external services in PHP with the Guzzle HTTP client — covering timeouts, error handling, and building a reliable request structure.

- Published: 2017-07-02
- Category: Web Development
- Tags: PHP
- Reading time: 4 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/consuming-third-party-apis-http-client-with-guzzle/
- Language: en-US
- Author: Muhammet Şafak

---
As an application grows, connecting to external services becomes inevitable. Payment providers, SMS gateways, weather APIs, shipment tracking services — if each of these integrations is built on its own pile of `curl_init()` calls, understanding how any of them behaves eventually becomes a real challenge.

**Guzzle** is an HTTP client library for PHP. It supports PSR-7-compliant messages, handles both synchronous and asynchronous requests, and provides a consistent error handling model. Frameworks like Laravel and Symfony already depend on it — chances are it's already installed in your project.

## Installation

If it isn't installed yet, adding it via Composer is all it takes:

```bash
composer require guzzlehttp/guzzle
```

## A Simple GET Request

```php
use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://api.example.com',
]);

$response = $client->get('/users/42');
$data = json_decode($response->getBody(), true);
```

The `base_uri` parameter means you don't have to repeat the full URL on every request. I appreciate this small detail on integrations where all requests go to the same base address.

## Setting Timeouts

External services don't always respond quickly. Making a request without a timeout can leave your server waiting indefinitely for a response. In any critical flow, that's unacceptable.

```php
$client = new Client([
    'base_uri'        => 'https://api.example.com',
    'timeout'         => 5.0,   // throws an exception if no response within 5 seconds
    'connect_timeout' => 2.0,   // maximum time allowed to establish the connection
]);
```

There are two distinct timeouts: `connect_timeout` limits how long it takes to reach the server, while `timeout` limits how long to wait for a response after the connection is established. Setting both is good practice.

When choosing timeout values, think about the "worst normal scenario." If a payment provider typically responds in 800 ms, a 5-second timeout is reasonable. But if you set it to 30 seconds, users will sit staring at the screen for half a minute whenever that service slows down. I learned this the hard way with a shipment integration: there was no default timeout set, and the day that service got sluggish, every order page locked up.

## Error Handling

Guzzle throws exceptions by default for 4xx and 5xx responses. You need to catch them with `try/catch`:

```php
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\RequestException;

try {
    $response = $client->post('/orders', [
        'json' => ['product_id' => 5, 'quantity' => 2],
    ]);
} catch (ClientException $e) {
    // 4xx: client error (invalid request, unauthorized, etc.)
    $statusCode = $e->getResponse()->getStatusCode();
    // Log it or return an appropriate message to the user
} catch (ServerException $e) {
    // 5xx: error on the external service's side
    // Retry logic can be evaluated here
} catch (RequestException $e) {
    // Connection error, timeout, and other network-level issues
}
```

Each exception type carries a different meaning, and distinguishing between them matters. 4xx errors usually indicate that the data your application sent was incorrect; 5xx errors point to a problem on the external service's end. Treating them the same way can be misleading.

There's another trap worth knowing: some APIs always return HTTP 200 and write error information into the response body instead of using proper status codes. In that case, Guzzle won't throw an exception — you have to parse the response and check it yourself. Before starting an integration, read the service's documentation for its error behavior; otherwise you'll end up silently processing a response that came back as 200 but contained `{"success": false}` inside.

## Headers and Authentication

Most APIs require a token or API key for authentication:

```php
$client = new Client([
    'base_uri' => 'https://api.example.com',
    'headers'  => [
        'Authorization' => 'Bearer ' . config('services.example.token'),
        'Accept'        => 'application/json',
    ],
]);
```

Pulling sensitive values like tokens from your `.env` file rather than hardcoding them is important. Leaving a plaintext token in source code is an easy mistake to make and a late one to catch.

## Wrapping It in a Service Class

Rather than instantiating Guzzle with `new Client()` everywhere, writing a dedicated service class for each integration keeps things organized:

```php
class ExampleApiService
{
    private Client $client;

    public function __construct()
    {
        $this->client = new Client([
            'base_uri' => config('services.example.base_uri'),
            'timeout'  => 5.0,
            'headers'  => [
                'Authorization' => 'Bearer ' . config('services.example.token'),
            ],
        ]);
    }

    public function getUser(int $id): array
    {
        $response = $this->client->get("/users/{$id}");
        return json_decode($response->getBody(), true);
    }
}
```

With this structure, if you ever swap Guzzle for another HTTP library or need to use a mock client, the change stays confined to a single class.

## Validating the Response

When an API claims it returns a specific format, checking that format rather than blindly trusting it is good practice. Failing early when an expected key is missing beats an unexpected null error surfacing hours later:

```php
public function getUser(int $id): array
{
    $response = $this->client->get("/users/{$id}");
    $data = json_decode($response->getBody(), true);

    if (!isset($data['id'], $data['email'])) {
        throw new \UnexpectedValueException(
            "API returned an unexpected format: " . $response->getBody()
        );
    }

    return $data;
}
```

The most commonly overlooked detail when integrating external services is timeout values. If every integration is set up from the start with a sensible timeout and proper error handling, the rest of your application stays unaffected when that service eventually slows down or becomes unreachable. After the freeze I experienced with the shipment integration, I added timeout configuration and response validation as defaults to my service class template for every new integration.
