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.
composer require guzzlehttp/guzzle<?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:
<?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.
<?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.
<?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:
ZUKKO_API_KEY=fetch_your_key
ZUKKO_API_URL=https://api.zukko.pro/v1Extend config/services.php:
'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):
<?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:
| Model | Price in/out per 1M | Calculation | Total |
|---|---|---|---|
claude-sonnet-5 | $0.3 / $0.3 | 0.5 × 0.3 + 0.3 × 0.3 | $0.24 |
gpt-5.6-luna | $0.1 / $0.1 | 0.5 × 0.1 + 0.3 × 0.1 | $0.08 |
glm-5.3-flash | $0.04 / $0.04 | 0.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.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Prompt Engineering Examples: 12 Ready-Made Templates for WorkTwelve prompt templates for everyday tasks: what each one says, why it is built that way, and what a single API call costs.