Datalumo
Docs · PHP
View markdown

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).

#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',
    'thumbnail' => 'https://example.com/hello-world.jpg',
    '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.

$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 plain 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.