Quickstart: PHP

This quickstart uses PHP 8.1 or later with the curl and json extensions, which most PHP builds include. No Composer packages are needed.

Check the extensions

php -m | grep -E '^(curl|json)$'

Send a request

<?php
// hello.php
function syntic_complete(array $body): array
{
    $ch = curl_init('https://app.syntic.ai/v1/chat/completions');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER => true,
        CURLOPT_TIMEOUT => 300,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('SYNTIC_API_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode($body + ['stream' => false], JSON_THROW_ON_ERROR),
    ]);
    $raw = curl_exec($ch);
    if ($raw === false) {
        throw new RuntimeException('network error: ' . curl_error($ch));
    }
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    curl_close($ch);
 
    $headers = substr($raw, 0, $headerSize);
    $data = json_decode(substr($raw, $headerSize), true);
    if ($status !== 200) {
        preg_match('/^x-syntic-request-id:\s*(\S+)/mi', $headers, $m);
        throw new RuntimeException(sprintf('%d %s (request %s)', $status, $data['error'] ?? '?', $m[1] ?? '?'));
    }
    return $data;
}
 
$reply = syntic_complete([
    'model' => 'Amara S1.2',
    'max_tokens' => 300,
    'messages' => [
        ['role' => 'system', 'content' => 'Answer in two sentences.'],
        ['role' => 'user', 'content' => 'What makes a good cup of tea?'],
    ],
]);
echo $reply['choices'][0]['message']['content'], PHP_EOL;
echo 'tokens: ', $reply['usage']['total_tokens'], PHP_EOL;

$body + ['stream' => false] adds stream: false unless the caller set it.

Run it

php hello.php

Stream the reply

CURLOPT_WRITEFUNCTION receives the body in pieces as it arrives. Buffer partial lines, then handle each complete event:

<?php
function syntic_stream(array $body, callable $onText): void
{
    $buffer = '';
    $ch = curl_init('https://app.syntic.ai/v1/chat/completions');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('SYNTIC_API_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode($body + ['stream' => true]),
        CURLOPT_WRITEFUNCTION => function ($ch, string $data) use (&$buffer, $onText): int {
            if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) !== 200) {
                $buffer .= $data;             // an ordinary JSON error body; read it after curl_exec
                return strlen($data);
            }
            $buffer .= $data;
            while (($pos = strpos($buffer, "\n")) !== false) {
                $line = rtrim(substr($buffer, 0, $pos), "\r");
                $buffer = substr($buffer, $pos + 1);
                if (!str_starts_with($line, 'data: ') || $line === 'data: [DONE]') {
                    continue;
                }
                $event = json_decode(substr($line, 6), true);
                if (isset($event['error'])) {
                    throw new RuntimeException('stream failed: ' . ($event['detail'] ?? ''));
                }
                $piece = $event['choices'][0]['delta']['content'] ?? '';
                if ($piece !== '') {
                    $onText($piece);
                }
            }
            return strlen($data);
        },
    ]);
    curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($status !== 200) {
        throw new RuntimeException("$status $buffer");
    }
}
 
syntic_stream(
    ['model' => 'Amara Quick', 'messages' => [['role' => 'user', 'content' => 'Write a short toast for a friend’s birthday.']]],
    function (string $text) { echo $text; flush(); }
);
echo PHP_EOL;

To stop early, return 0 from the write function; curl then aborts the transfer and the connection closes.

Call a function

<?php
$tools = [[
    'type' => 'function',
    'function' => [
        'name' => 'room_available',
        'description' => 'Whether a meeting room is free for a time range today.',
        'parameters' => [
            'type' => 'object',
            'properties' => [
                'room' => ['type' => 'string'],
                'from' => ['type' => 'string', 'description' => 'HH:MM'],
                'to' => ['type' => 'string', 'description' => 'HH:MM'],
            ],
            'required' => ['room', 'from', 'to'],
        ],
    ],
]];
 
$messages = [['role' => 'user', 'content' => 'Is the Oak room free from 14:00 to 15:00?']];
$reply = syntic_complete(['model' => 'Amara S1.2', 'tools' => $tools, 'messages' => $messages]);
$msg = $reply['choices'][0]['message'];
 
if (!empty($msg['tool_calls'])) {
    $messages[] = $msg;                                   // the assistant turn, unchanged
    foreach ($msg['tool_calls'] as $call) {
        $args = json_decode($call['function']['arguments'], true) ?? [];
        $result = ['free' => true];                       // replace with a real lookup
        $messages[] = ['role' => 'tool', 'tool_call_id' => $call['id'], 'content' => json_encode($result)];
    }
    $reply = syntic_complete(['model' => 'Amara S1.2', 'tools' => $tools, 'messages' => $messages]);
}
echo $reply['choices'][0]['message']['content'], PHP_EOL;

If you use an OpenAI-compatible PHP client, set its base URI to https://app.syntic.ai/v1 and include 'stream' => false in the parameters of non-streaming calls.

Handle errors

StatusWhat to do
400, 413Fix the request; error names the problem.
401, 403Check the key and its restrictions.
402quota_exhausted: see Handle 402.
429Wait for the Retry-After header, then retry.
500, 502, 503Retry with exponential back-off; see Retries with back-off.

Next steps

Was this page helpful?