john-wink/telli-laravel-wrapper

A Laravel wrapper for the telli API v2 - AI phone call agents

Maintainers

Package info

github.com/john-wink/telli-laravel-wrapper

pkg:composer/john-wink/telli-laravel-wrapper

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-07-11 16:01 UTC

This package is auto-updated.

Last update: 2026-07-11 16:02:19 UTC


README

Latest Version on Packagist GitHub Tests Action Status Total Downloads

A Laravel wrapper for the telli API v2 - AI phone call agents. Fully typed DTOs via spatie/laravel-data, a typed exception per documented API error code, cursor pagination as lazy collections, and an idempotent contact create.

Requirements

  • PHP 8.3+
  • Laravel 12

Installation

composer require john-wink/telli-laravel-wrapper

Publish the config file (optional):

php artisan vendor:publish --tag="telli-config"

Publish the translations (optional, ships with en and de):

php artisan vendor:publish --tag="telli-translations"

Set your API key in .env:

TELLI_API_KEY=your-api-key

Usage

Contacts

use JohnWink\Telli\Facades\Telli;
use JohnWink\Telli\Data\CreateContactData;
use JohnWink\Telli\Data\UpdateContactData;
use JohnWink\Telli\Data\ContactPropertyInputData;
use Spatie\LaravelData\DataCollection;

$contact = Telli::contacts()->create(new CreateContactData(
    firstName: 'Max',
    lastName: 'Mustermann',
    phoneNumber: '+4915112345678',
    externalId: 'lead-4711',
    properties: new DataCollection(ContactPropertyInputData::class, [
        new ContactPropertyInputData(key: 'lead_score', value: 87),
    ]),
));

$page = Telli::contacts()->list(limit: 100);          // one page + pageInfo/meta
Telli::contacts()->all()->each(fn ($contact) => ...); // lazy, follows the cursor

$contact = Telli::contacts()->get($id);
$contact = Telli::contacts()->getByExternalId('lead-4711');
$contact = Telli::contacts()->update($id, new UpdateContactData(email: 'new@example.com'));
Telli::contacts()->delete($id);

Replace semantics: Telli::contacts()->replace($id, $data) issues a PUT. Optional fields you omit are reset to null (respectively [] for properties) on the server - unlike update(), which only touches the fields you send.

Idempotent create: when externalId is set and the API answers with a 5xx or the connection drops, the wrapper looks the contact up by external ID before retrying - you never create duplicates. Without an externalId there is nothing to check against, so a failed create is NOT retried; set an externalId whenever you can.

Contact properties

use JohnWink\Telli\Data\CreateContactPropertyData;
use JohnWink\Telli\Data\UpdateContactPropertyData;
use JohnWink\Telli\Enums\PropertyDataType;

$properties = Telli::contactProperties()->list();
$property = Telli::contactProperties()->create(new CreateContactPropertyData(
    key: 'lead_score',
    dataType: PropertyDataType::Number,
    label: 'Lead Score',
));
$property = Telli::contactProperties()->get('lead_score');
$property = Telli::contactProperties()->update('lead_score', new UpdateContactPropertyData(label: 'Score'));

Agents & account health

$agents = Telli::agents()->all();          // lazy collection over all pages
$agent = Telli::agents()->get($agentId);
$health = Telli::account()->health();      // AccountHealthStatus::Operational|Degraded

Multi-tenant / per-team API keys

The config key is just the default account. Scope any call to a different telli account at runtime - withApiKey() returns a NEW client and never mutates the shared singleton, so there is no key bleed between requests, queued jobs or Octane workers:

Telli::withApiKey($team->telli_api_key)->contacts()->create($data);

Error handling

Every documented telli error code maps to its own exception with typed fields, all extending TelliRequestException (which extends TelliException):

use JohnWink\Telli\Exceptions\ContactNotFoundException;
use JohnWink\Telli\Exceptions\TelliValidationException;
use JohnWink\Telli\Exceptions\TelliException;

try {
    Telli::contacts()->get($id);
} catch (ContactNotFoundException $exception) {
    $exception->contactId;      // typed payload from the API
} catch (TelliValidationException $exception) {
    $exception->issues;         // list of ValidationIssueData (code, message, path)
} catch (TelliException $exception) {
    // configuration, connection or any other API error
}

Exception messages are translated (en, de); the raw API message stays available via $exception->apiMessage, the raw code via $exception->rawCode.

Retries

429 responses are retried for every request; connection errors and 5xx responses are retried for non-POST requests only — a blind POST retry could create duplicates. Failed POST creates recover through the verify-then-retry mechanism instead (see "Idempotent create" above) when a natural key (externalId / property key) is present. The backoff is configurable (config/telli.php); a Retry-After header on 429 responses takes precedence.

Testing your integration

The wrapper uses Laravel's HTTP client, so Http::fake() works out of the box:

Http::fake(['api.telli.com/*' => Http::response([...])]);

Or swap the whole client with a mock:

use JohnWink\Telli\Facades\Telli;

Telli::swap($mock);

Changelog

See CHANGELOG.

License

MIT, see LICENSE.