PHP SDK
datalumo/php is the official PHP client for the Datalumo API. It handles authentication, retries, streaming, and turns every response into a typed object, so you are not parsing JSON by hand.
On Laravel? Use the Laravel package instead. It is built on this client and adds model syncing and Blade components.
Requires PHP 8.2 or newer.
#Install
composer require datalumo/php
use Datalumo\Client;
$client = new Client(
token: getenv('DATALUMO_TOKEN'),
organisation: getenv('DATALUMO_ORG'),
);
organisation is your organisation's id, not its name. Create an API key and copy the id from the API keys page in your dashboard, and give the key the permissions you need (see Authentication).
Self-hosting Datalumo? Pass baseUrl: 'https://datalumo.example.com'.
#Check the connection
$me = $client->me()->get();
$me->organisation->name;
$me->token->abilities; // ['pages.write', 'search']
$me->token->sourceScope; // null when the key can reach every source
foreach ($me->sources as $source) {
echo $source->slug;
}
A good first call in any setup script: it proves the key works and tells you exactly what it may do.
#Push content
$page = $client->pages('docs')->upsert([
'external_id' => 'post-42',
'name' => 'Hello world',
'content' => '# Hello world',
'content_mime' => 'markdown',
'source_url' => 'https://example.com/hello-world',
'meta' => ['author' => 'Jane'],
]);
$page->status; // "pending" until indexing finishes
docs is the slug (or id) of the source you are writing to. external_id is your own id for the record: send the same one again and Datalumo updates that page instead of creating a second one.
content_mime accepts the short forms markdown, md, html, plain, and text as well as the full types.
Up to 50 pages at a time:
$pages = $client->pages('docs')->batchUpsert([
['external_id' => 'post-1', 'name' => 'One', 'content' => '…'],
['external_id' => 'post-2', 'name' => 'Two', 'content' => '…'],
]);
Removing one:
$client->pages('docs')->delete('post-42');
#Listing what is there
$list = $client->pages('docs')->list(['status' => 'failed']);
foreach ($list->data as $page) {
echo $page->externalId.': '.$page->error;
}
Walking every page:
$cursor = null;
do {
$list = $client->pages('docs')->list(array_filter([
'per_page' => 100,
'cursor' => $cursor,
]));
foreach ($list->data as $page) {
// …
}
$cursor = $list->nextCursor;
} while ($list->hasMorePages());
#Waiting for indexing
Pushing is not indexing. A write is accepted straight away and the content becomes searchable a little later. In a script or a test, you can wait for it:
$page = $client->pages('docs')->waitUntilIndexed('post-42', timeoutSeconds: 60);
It throws if indexing fails or the wait runs out. Do not use it in a web request; for anything user facing, treat "pending" as normal and let the dashboard show progress.
#Search
$result = $client->widgets($widgetId)->search([
'query' => 'how does pricing work',
'limit' => 5,
]);
foreach ($result->data as $hit) {
echo $hit->name; // title
echo $hit->url; // link to show
echo $hit->snippet; // matching text
echo $hit->score;
echo $hit->externalId;
}
$result->sessionId; // pass along to tie clicks to this search
$widgetId is the widget's own id, without the organisation/ prefix used by the browser embed. Search does not use answer credits.
Optional parameters mirror the search endpoint: filters, sort, limit, and session_id.
#Chat and summaries
$reply = $client->widgets($widgetId)->chat([
'query' => 'How do refunds work?',
'conversation_id' => $conversationId, // optional, continues a conversation
]);
$reply->text;
$reply->conversationId;
$reply->sources; // the pages the answer used
Streaming, so visitors see words as they arrive:
$stream = $client->widgets($widgetId)->chatStream(['query' => 'How do refunds work?']);
foreach ($stream->text() as $chunk) {
echo $chunk;
flush();
}
$conversationId = $stream->conversationId();
If you want the individual events (status updates, errors) rather than just the words:
foreach ($stream as $event) {
if ($event->isStatus()) { /* "searching" and friends */ }
if ($event->isToken()) { echo $event->text(); }
if ($event->isDone()) { /* $event->data['conversation_id'] */ }
}
summarize() and summarizeStream() work the same way and produce a short answer over search results.
Both chat and summarize use answer credits. When they run out, the call throws QuotaExceededException and the body carries free search hits, so you can still show results.
#Signed visitors and conversations
To let a logged-in user pick up their previous conversations, sign their id on your server with the widget's signing secret:
use Datalumo\Identity;
$user = Identity::userPayload((string) $user->id, $signingSecret);
$client->widgets($widgetId)->chat([
'query' => $message,
'user' => $user,
]);
Then their history is available:
$conversations = $client->widgets($widgetId)->conversations();
$conversations->list($userId, $hash);
$conversations->get($conversationId, $userId, $hash);
$conversations->keep($conversationId, true, $userId, $hash);
$conversations->delete($conversationId, $userId, $hash);
$conversations->deleteAll($userId, $hash);
Identity::sign($userId, $secret) gives you the hash on its own, and Identity::conversationParams() the user and hash pair these methods expect. The signing secret stays on your server.
#Analytics and widget config
$client->widgets($widgetId)->recordEvent([
'type' => 'click',
'session_id' => $result->sessionId,
'meta' => ['url' => $hit->url, 'rank' => 0],
]);
$config = $client->widgets($widgetId)->config(); // colours, greeting, language
Recording clicks against the search's session_id is what makes analytics in your dashboard tell the whole story: which query led to which result.
#Errors
Every failure throws something that extends DatalumoException, so you can catch broadly or precisely:
| Exception | When |
|---|---|
AuthenticationException |
401, key missing or invalid |
AuthorizationException |
403, key lacks a permission or the source is out of scope |
QuotaExceededException |
402, no answer credits left |
NotFoundException |
404, unknown source, page, or widget |
ValidationException |
422, something in your payload was rejected |
RateLimitException |
429, too many requests |
DatalumoException |
Anything else, including network problems |
use Datalumo\Exceptions\ValidationException;
use Datalumo\Exceptions\DatalumoException;
try {
$client->pages('docs')->upsert($page);
} catch (ValidationException $e) {
print_r($e->errors()); // field => messages
} catch (DatalumoException $e) {
echo $e->status().': '.$e->getMessage();
}
RateLimitException::retryAfterSeconds() tells you how long to wait, and body() gives the decoded response on any of them.
#Timeouts and retries
Rate limited requests are retried automatically, up to three times, honouring Retry-After. Chat and summarize are left out of that by default, because a retry there could spend another answer credit.
Change any of it:
use Datalumo\Client;
use Datalumo\HttpClientOptions;
$client = new Client(
token: $token,
organisation: $org,
options: new HttpClientOptions(
timeoutSeconds: 30,
connectTimeoutSeconds: 10,
maxRetries: 3,
retryOn: [429],
retryChatAndSummarize: false,
logger: $psrLogger,
),
);
Pass your own configured Guzzle client as guzzle if you need proxies, custom middleware, or want to fake responses in tests.
#Related
- Introduction for the other ways to integrate
- Laravel package, built on this client
- Authentication for API keys, permissions, and widget access
- Pages · Search · Chat · Events for the endpoints behind each method