Claude API in PHP: Guzzle and curl for Zukko

How to call Claude and GPT from PHP: Guzzle, built-in curl, error handling, streaming, and Laravel integration.

4 min · · development

PHP still runs a large share of websites and CRM customizations, and wiring the Claude API into them is simple: it's a regular HTTPS request with JSON. Below are working examples for Zukko using Guzzle and the built-in curl_* functions, error handling, a streaming response, and a Laravel version. Requests go to the OpenAI-compatible endpoint https://api.zukko.pro/v1/chat/completions, so the same code works for Claude, GPT, and GLM.

Method 1: Guzzle

Guzzle is the most widely used HTTP client in PHP and already ships with Laravel and many frameworks.

bash
composer require guzzlehttp/guzzle
bash
<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://api.zukko.pro/v1/',
    'timeout'  => 60,
]);

$response = $client->post('chat/completions', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('ZUKKO_API_KEY'),
    ],

The json option serializes the array and sets the Content-Type: application/json header. Note the base_uri: it ends with /, and the path chat/completions has no leading slash. Otherwise, under URL resolution rules, Guzzle drops /v1 and the request goes to the wrong place. By default Guzzle throws an exception on 4xx and 5xx responses, which we use below.

Method 2: built-in curl

If you can't install dependencies, the curl extension, enabled on almost any host, is enough:

bash
<?php

$apiKey = getenv('ZUKKO_API_KEY');

$payload = json_encode([
    'model'      => 'claude-sonnet-5',
    'max_tokens' => 500,
    'messages'   => [
        ['role' => 'user', 'content' => 'Explain what a REST API is in three sentences'],
    ],
], JSON_UNESCAPED_UNICODE);

$ch = curl_init('https://api.zukko.pro/v1/chat/completions');
curl_setopt_array($ch, [

curl_exec returns false on a network failure (DNS, timeout, TLS), and you need to check that separately from the HTTP status. The API error text is in the error.message field.

Error handling in Guzzle

Status codes tell you what to do: 401 — invalid key, 402 — balance ran out, 403 — model not allowed for the key, 413 — request too large, 429 — too many requests, 503 — model provider temporarily unavailable. Only 429 and 503 are worth retrying.

bash
<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\ConnectException;

function askModel(Client $client, string $prompt, int $attempts = 3): string
{
    for ($i = 1; $i <= $attempts; $i++) {
        try {
            $response = $client->post('chat/completions', [
                'headers' => ['Authorization' => 'Bearer ' . getenv('ZUKKO_API_KEY')],
                'json'    => [

BadResponseException is the common parent of ClientException (4xx) and ServerException (5xx), so one block catches both groups. ConnectException covers network failures where no response arrives at all.

Streaming responses

To show text as it's generated, pass "stream": true in the request body and the Guzzle option 'stream' => true so Guzzle doesn't wait for the full response. The server sends data: {...} lines and finally data: [DONE]; the last chunk may have an empty choices array.

bash
<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new Client(['base_uri' => 'https://api.zukko.pro/v1/', 'timeout' => 120]);

$response = $client->post('chat/completions', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('ZUKKO_API_KEY')],
    'json'    => [
        'model'    => 'claude-sonnet-5',
        'stream'   => true,
        'messages' => [['role' => 'user', 'content' => 'Write a short poem about a cat']],
    ],

Network chunks don't line up with line boundaries, so data accumulates in $buffer and is parsed only as whole lines. If the script streams to a browser through PHP-FPM and nginx, make sure output buffering is off, or the text will arrive in one piece at the end.

Laravel integration

Put the key in .env:

bash
ZUKKO_API_KEY=fetch_your_key
ZUKKO_API_URL=https://api.zukko.pro/v1

Extend config/services.php:

bash
'zukko' => [
    'key' => env('ZUKKO_API_KEY'),
    'url' => env('ZUKKO_API_URL', 'https://api.zukko.pro/v1'),
],

A controller using Laravel's built-in HTTP client (it runs on top of Guzzle):

bash
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class ChatController extends Controller
{
    public function chat(Request $request)
    {
        $validated = $request->validate(['message' => 'required|string|max:4000']);

        $response = Http::withToken(config('services.zukko.key'))

The key stays on the server and the frontend gets only the answer text. Don't pass the full API response to the client — it contains service fields the browser doesn't need.

What it costs

Zukko pricing is per token. Example: 1,000 requests with about 500 input and 300 output tokens each, 0.5M input and 0.3M output in total:

ModelPrice in/out per 1MCalculationTotal
claude-sonnet-5$0.3 / $0.30.5 × 0.3 + 0.3 × 0.3$0.24
gpt-5.6-luna$0.1 / $0.10.5 × 0.1 + 0.3 × 0.1$0.08
glm-5.3-flash$0.04 / $0.040.5 × 0.04 + 0.3 × 0.04$0.032

Check the real token counts in the usage field of each response. Which model handles your task well is something to test on your own data: run the same set of requests through two or three models and compare.

FAQ

Where do I get an API key?

Create a key in the dashboard. The full key is shown once, at creation — save it to .env or an environment variable right away. More on the authorization header is in the Authentication section.

How do I set a timeout?

In Guzzle, use the timeout (whole request) and connect_timeout (connection setup) options. In curl, CURLOPT_TIMEOUT and CURLOPT_CONNECTTIMEOUT. In Laravel, the HTTP client's timeout() method. For long answers set 60–120 seconds or use streaming.

"SSL certificate problem" error?

PHP can't find an up-to-date root certificate bundle. Update the system ca-certificates package or point php.ini at the certificate file (curl.cainfo and openssl.cafile). Don't disable certificate verification, even on a test server — it's the easiest way to leak your key.

Can I use other models?

Yes, change the model field: claude-opus-5-5, gpt-5.6-sol, gpt-5.6-luna, glm-5.3, glm-5.3-flash. The current list is at GET https://api.zukko.pro/v1/models or on the "Models" page in the dashboard; reference is in the Chat Completions docs.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*