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.phpStream 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
| Status | What to do |
|---|---|
400, 413 | Fix the request; error names the problem. |
401, 403 | Check the key and its restrictions. |
402 | quota_exhausted: see Handle 402. |
429 | Wait for the Retry-After header, then retry. |
500, 502, 503 | Retry with exponential back-off; see Retries with back-off. |
Next steps
- Guides
- Chat completions reference
- Other languages: the planned official PHP client.
Was this page helpful?