PHP SDK
Install the official PHP SDK, use it in Laravel, and work with chat, web search, agents, and semantic search on HiNow.
Updated on Aug 09, 2026
The official PHP SDK for the HiNow API. It works in any PHP 8.0+ project and registers itself automatically in Laravel.
The API speaks the OpenAI protocol, and the SDK follows the same format. If you have already integrated with OpenAI, the shape of the calls is what you are familiar with.
composer require hinow-ai/hinow-aiKeep the key in HINOW_API_KEY and the SDK finds it automatically. Passing the key in the constructor also works, but avoid leaving the value in the code.
export HINOW_API_KEY="hi_sua_chave_aqui"<?php
require 'vendor/autoload.php';
use Hinow\Hinow;
// With no arguments, the client reads the key from HINOW_API_KEY.
$client = new Hinow();
$answer = $client->chat->completions->create([
'model' => 'hinow/himax',
'messages' => [
['role' => 'system', 'content' => 'You answer in English.'],
['role' => 'user', 'content' => 'What is an API? Answer in one paragraph.'],
],
]);
echo $answer['choices'][0]['message']['content'], "\n";
// What it cost, in tokens.
$uso = $answer['usage'];
echo "\ninput: {$uso['prompt_tokens']} · output: {$uso['completion_tokens']}\n";The hinow/ prefix is part of the model name
Sending himax instead of hinow/himax returns 404 model_not_found, and the message does not make it clear that the prefix is missing. This applies to all: hinow/himax, hinow/hinova, hinow/higenesis.
The package registers itself automatically. Put the key in .env and use the facade, or receive the client via dependency injection in your service constructor.
HINOW_API_KEY=hi_sua_chave_aquiuse Hinow\Facades\Hinow;
$resposta = Hinow::chat()->completions->create([
'model' => 'hinow/himax',
'messages' => [
['role' => 'user', 'content' => $request->input('pergunta')],
],
]);
return $resposta['choices'][0]['message']['content'];use Hinow\Hinow;
class Atendimento
{
public function __construct(private Hinow $hinow)
{
}
public function responder(string $pergunta): string
{
$resposta = $this->hinow->chat->completions->create([
'model' => 'hinow/himax',
'messages' => [['role' => 'user', 'content' => $pergunta]],
]);
return $resposta['choices'][0]['message']['content'];
}
}To adjust the timeout or the number of retries, publish the configuration file with php artisan vendor:publish --tag=hinow-config.
createStream() returns a generator: you iterate with foreach and each iteration brings a new chunk. In a chat interface, this changes the perception of speed more than switching models.
<?php
require 'vendor/autoload.php';
use Hinow\Hinow;
$client = new Hinow();
$stream = $client->chat->completions->createStream([
'model' => 'hinow/hinova',
'messages' => [
['role' => 'user', 'content' => 'Write three lines about the sea at dawn.'],
],
]);
foreach ($stream as $chunk) {
// Note the "delta": each chunk carries the new fragment, not the whole answer.
echo $chunk['choices'][0]['delta']['content'] ?? '';
flush();
}
echo "\n";It is delta, not message
In the complete response, the text comes in message.content. In streaming, each chunk brings only the new snippet, in delta.content — putting it all together is up to you.
Responds instantly, without a job to track, at US$ 0.005 per call. The third argument accepts country and lang to guide the results.
<?php
require 'vendor/autoload.php';
use Hinow\Hinow;
$client = new Hinow();
// Answers on the spot: there is no job to follow.
$search = $client->tools->search('best beaches in northeast Brazil', 'search', [
'country' => 'br',
'lang' => 'pt-br',
]);
foreach ($search['results'] as $item) {
echo "{$item['position']}. {$item['title']}\n";
echo " {$item['url']}\n";
}
printf("\n%d results · US$ %.3f\n", count($search['results']), $search['cost']);type | What comes in each result |
|---|---|
search, scholar, patents | position, title, url, snippet |
news | the above plus source, date, image_url |
images | link, image_url, thumbnail_url, width, height |
videos | channel, duration, date, thumbnail_url |
places | address, category, phone, website, rating, coordinates |
shopping | price, delivery, rating, source |
autocomplete | suggestions, an array of strings — there is no results |
Scans one or more sites for emails, phones, and social profiles, and each finding comes with the exact page where it appeared. Since the scan takes time, this runs as a job.
<?php
require 'vendor/autoload.php';
use Hinow\Hinow;
$client = new Hinow();
// The crawl takes a while, so it runs as a job. This method waits for you.
$job = $client->tools->websiteContactsAndWait(['https://www.mongodb.com'], [
'maxDepth' => 2,
'maxLinksPerPage' => 10,
]);
printf(
"status: %s · US$ %.3f%s\n\n",
$job['status'],
$job['cost'],
!empty($job['cached']) ? ' (from cache)' : ''
);
// Each item carries the type, the value and the exact page it was found on.
foreach ($job['result']['items'] as $item) {
$onde = $item['sourceUrl'];
if ($item['type'] === 'email') {
echo "e-mail {$item['value']}\n em {$onde}\n";
} elseif ($item['type'] === 'phone') {
echo "telefone {$item['value']}\n em {$onde}\n";
} else {
echo "{$item['platform']} {$item['url']}\n";
}
}Running the same crawl twice is not charged again
The job returns cached: true when the result came from the cache. If you want to control the cycle yourself, use websiteContacts() and track it with tools->jobs->retrieve($job['job_id']) — note that the field is job_id, not id.
Validate what comes before using it
The scan returns what it found in the page HTML, as it is. Truncated or repeated addresses appear — treat the list as raw material and validate before saving to your database.
Assistants, threads, and runs in the same format as the OpenAI API. You declare the functions, and the model decides when to call them: the run pauses, you execute, and return the result.
<?php
require 'vendor/autoload.php';
use Hinow\Hinow;
$client = new Hinow();
// Your real function: an array here, just for the example.
function getOrder(string $code): array
{
$orders = [
'A-1001' => ['status' => 'delivered', 'delivered' => '2026-08-02'],
'A-1002' => ['status' => 'in transit', 'estimated' => '2026-08-12'],
];
return $orders[$code] ?? ['error' => 'order not found'];
}
// 1. The assistant: model, instructions and the functions it may call.
$assistant = $client->beta->assistants->create([
'model' => 'hinow/himax',
'name' => 'Support',
'instructions' => 'You answer questions about orders. Check the tool before stating any status.',
'tools' => [[
'type' => 'function',
'function' => [
'name' => 'get_order',
'description' => 'Look up an order by its code.',
'parameters' => [
'type' => 'object',
'properties' => [
'code' => ['type' => 'string', 'description' => 'Order code, e.g. A-1001'],
],
'required' => ['code'],
],
],
]],
]);
// 2. The conversation and the question.
$thread = $client->beta->threads->create();
$client->beta->threads->messages->create($thread['id'], 'Has order A-1002 arrived?');
// 3. Run it and wait.
$run = $client->beta->threads->runs->createAndPoll($thread['id'], $assistant['id']);
// 4. While the model asks for functions, run them and hand the result back.
while ($run['status'] === 'requires_action') {
$outputs = [];
foreach ($run['required_action']['submit_tool_outputs']['tool_calls'] as $call) {
$args = json_decode($call['function']['arguments'], true);
echo "-> the model called {$call['function']['name']}({$args['code']})\n";
$outputs[] = [
'tool_call_id' => $call['id'],
'output' => json_encode(getOrder($args['code'])),
];
}
$client->beta->threads->runs->submitToolOutputs($thread['id'], $run['id'], $outputs);
$run = $client->beta->threads->runs->poll($thread['id'], $run['id']);
}
// 5. The final answer is the last message on the thread.
$messages = $client->beta->threads->messages->list($thread['id'], ['order' => 'desc', 'limit' => 1]);
echo "\n", $messages['data'][0]['content'][0]['text']['value'], "\n";
$client->beta->assistants->delete($assistant['id']);requires_action is not an error
It is the run handing control back to you to execute a function. That's why poll() returns in this state instead of continuing to spin: read the required_action, call submitToolOutputs(), and resume monitoring.
Upload files, group them into a vector store, and search by meaning. Indexing is asynchronous — searching before it finishes returns zero results, without an error.
<?php
require 'vendor/autoload.php';
use Hinow\Hinow;
$client = new Hinow();
// 1. Upload the document.
$file = $client->files->create('returns-policy.txt', 'assistants');
echo "file: {$file['id']} ({$file['bytes']} bytes)\n";
// 2. Create the store and attach the file to it.
$base = $client->vectorStores->create(['name' => 'Support base']);
$client->vectorStores->files->create($base['id'], $file['id']);
echo "store: {$base['id']}\n";
// 3. Indexing is asynchronous. Searching before it finishes returns nothing
// resultado, sem error nenhum — por isso vale esperar.
do {
sleep(2);
$state = $client->vectorStores->files->retrieve($base['id'], $file['id']);
echo "indexing: {$state['status']}\n";
} while ($state['status'] === 'in_progress');
// 4. Search by meaning, not by exact word.
$hits = $client->rag->search('How many days do I have to return an item?', [
'rag_id' => $base['id'],
'top_k' => 3,
]);
echo "\n";
foreach ($hits['results'] as $achado) {
printf("%.2f %s\n", $achado['score'], $achado['source']);
echo ' ', str_replace("\n", ' ', mb_substr($achado['text'], 0, 120)), "…\n";
}The filter for vector stores is called rag_id
Passing vector_store_id does not generate an error: the search simply scans all documents in the account instead of the store you intended. It's the kind of detail that makes it seem like the search is returning garbage.
Each failure becomes its own class, so you can handle by type instead of comparing message strings. All inherit from Hinow\HinowException, and those originating from the API carry status, type, and the response body.
<?php
require 'vendor/autoload.php';
use Hinow\Hinow;
use Hinow\AuthenticationException;
use Hinow\NotFoundException;
use Hinow\RateLimitException;
use Hinow\InvalidRequestException;
use Hinow\ConnectionException;
use Hinow\ApiException;
$client = new Hinow();
try {
$client->chat->completions->create([
'model' => 'himax', // the hinow/ prefix is missing
'messages' => [['role' => 'user', 'content' => 'Olá']],
]);
} catch (AuthenticationException $e) {
echo "Invalid or expired key. Check HINOW_API_KEY.\n";
} catch (NotFoundException $e) {
echo "Not found: {$e->getMessage()}\n";
} catch (InvalidRequestException $e) {
echo "Invalid request: {$e->getMessage()}\n";
} catch (RateLimitException $e) {
echo "Rate limited. The SDK already retried; wait a moment.\n";
} catch (ConnectionException $e) {
// Nothing reached the API, so nothing was charged.
echo "Sem answer da API: {$e->getMessage()}\n";
} catch (ApiException $e) {
// Safety net: any other error the API reported.
echo "Error {$e->status}: {$e->getMessage()}\n";
}| Class | When it occurs |
|---|---|
AuthenticationException | 401 — missing, invalid, or revoked key |
PermissionException | 403 — the key does not have access to this resource |
NotFoundException | 404 — non-existent model, file, or assistant |
InvalidRequestException | 400 or 422 — a field is missing or a value is out of range |
RateLimitException | 429 — limit reached or balance exhausted |
ServerException | 5xx — the failure is on the API side |
ConnectionException | the request did not arrive; nothing was charged |
<?php
require 'vendor/autoload.php';
use Hinow\Hinow;
$client = new Hinow();
// Account credit, in US dollars.
$balance = $client->getBalance();
printf("balance: US$ %.2f\n\n", $balance['balance']);
// One specific model. The id is namespaced: hinow/himax, not himax.
$model = $client->models->retrieve('hinow/himax');
echo "{$model['name']} ({$model['id']})\n";
echo 'categories: ', implode(', ', $model['category']), "\n";| Resource | Purpose |
|---|---|
chat->completions | Chat, streaming, function calling, JSON mode |
embeddings | Vectors for semantic search |
images · audio · video | Generation |
models | Catalog, pricing, and features for each model |
tools | Web search and site contacts |
files | Document uploads |
vectorStores | Searchable knowledge bases |
rag | Semantic search on your documents |
beta->assistants · beta->threads | Server-side agents |
getBalance() | Account balance |
use Hinow\Hinow;
$client = new Hinow(
apiKey: getenv('HINOW_API_KEY'), // ou deixe em branco e use a variável
baseUrl: 'https://api.hinow.ai', // ou HINOW_BASE_URL
timeout: 120, // segundos
maxRetries: 2, // repete 429 e 5xx
);Up to 1.0.1, the SDK wrapped temperature, max_tokens, top_p, and repetition_penalty inside a parameters object before sending. The API accepts this format and ignores it, so these options never took effect: requesting max_tokens => 10 returned the full response.
Starting with 2.0, everything goes at the root level, as the API expects. Your code doesn't change — but calls that silently ignored a limit will now respect it, so review prompts that relied on the old behavior.
Two other changes: errors are now typed, keeping HinowException as the base class so existing catch blocks still work; and $client->chat->completions became a property as well as a method, so ->completions->create() and ->completions()->create() work the same.
Choosing between HiMax, HiNova, and HiGenesis
What each model does well, how much it costs, and how to write prompts for each.

