"> Aller au contenu principal
PHP PHP PHP v0.2.0 MIT

Withallo PHP SDK

qrcommunication/withallo-sdk

PHP 8.2+, guzzlehttp/guzzle ^7.8

Installation

bash
composer require qrcommunication/withallo-sdk
Requirements : PHP 8.2+, ext-json. The guzzlehttp/guzzle ^7.8 dependency is installed automatically by Composer.
API key required. Generate your API key on web.withallo.com/settings/api. It is sent verbatim in the Authorization header — there is NO Bearer prefix.
Packagist qrcommunication/withallo-sdk
Version 0.2.0
Source github.com/QrCommunication/sdk-withallo-php
License MIT
Base URL https://api.withallo.com/v1/api

Quick Start

Five resources exposed on a single client: webhooks, calls, contacts, SMS and phone numbers. Every response is mapped to a strongly-typed DTO; every enum value is a native PHP 8.2 enum.

php
<?php

use QrCommunication\Withallo\AlloClient;
use QrCommunication\Withallo\AlloClientOptions;
use QrCommunication\Withallo\Dto\CreateContactInput;
use QrCommunication\Withallo\Dto\CreateWebhookInput;
use QrCommunication\Withallo\Dto\SendSmsInput;
use QrCommunication\Withallo\Dto\SendSmsFranceInput;
use QrCommunication\Withallo\Enum\WebhookTopic;
use QrCommunication\Withallo\Exception\AlloApiException;

$allo = new AlloClient(new AlloClientOptions(
    apiKey: $_ENV['ALLO_API_KEY'],
));

// ── Lister les numéros connectés ─────────────────────────────────

foreach ($allo->numbers->list() as $n) {
    echo "{$n->name} — {$n->number} ({$n->country})\n";
}

// ── Envoyer un SMS (US / international) ─────────────────────────

$sms = $allo->sms->send(new SendSmsInput(
    from: '+1234567890',
    to: '+0987654321',
    message: 'Hello from Allo',
));
echo $sms->content . ' @ ' . $sms->startDate->format(DATE_ATOM) . "\n";

// ── Envoyer un SMS depuis la France (sender_id pré-vérifié) ─────

$allo->sms->sendFrance(new SendSmsFranceInput(
    senderId: 'MyCompany',
    to: '+33612345678',
    message: 'Bonjour depuis Allo',
));

// ── Rechercher l'historique des appels ──────────────────────────

$calls = $allo->calls->search(
    alloNumber: '+1234567890',
    page: 0,
    size: 20,
);
foreach ($calls->results as $call) {
    echo "{$call->type->value} {$call->fromNumber} → {$call->toNumber} "
       . "({$call->lengthInMinutes} min)\n";
}

// ── Créer puis récupérer un contact ─────────────────────────────

try {
    $contact = $allo->contacts->create(new CreateContactInput(
        numbers: ['+33612345678'],
        name: 'Jean',
        lastName: 'Dupont',
        emails: ['jean.dupont@example.com'],
    ));

    $fetched = $allo->contacts->get($contact->id);
    echo "{$fetched->name} {$fetched->lastName}\n";
} catch (AlloApiException $e) {
    echo "Allo error [{$e->code}] : {$e->getMessage()}\n";
}

// ── Souscrire un webhook aux événements CALL_RECEIVED + SMS_RECEIVED ──

$webhook = $allo->webhooks->create(new CreateWebhookInput(
    alloNumber: '+1234567890',
    url: 'https://example.com/webhooks/allo',
    topics: [WebhookTopic::CALL_RECEIVED, WebhookTopic::SMS_RECEIVED],
    enabled: true,
));
echo "Webhook id : {$webhook->id}\n";

Configuration

AlloClientOptions is a readonly value object passed to the AlloClient constructor. Only apiKey is mandatory — every other field has a sensible default.

php
use QrCommunication\Withallo\AlloClientOptions;

$options = new AlloClientOptions(
    apiKey:  $_ENV['ALLO_API_KEY'],                // Required — raw key, NO Bearer prefix
    baseUrl: 'https://api.withallo.com/v1/api',    // Default
    timeout: 30.0,                                 // Guzzle timeout in seconds
    headers: ['X-App-Name' => 'my-application'],   // Extra HTTP headers
);
Parameter Type Default Description
apiKey string Required. Raw API key (no Bearer prefix).
baseUrl string 'https://api.withallo.com/v1/api' Base URL of the Withallo API
timeout float 30.0 Guzzle HTTP timeout in seconds
headers array<string,string> [] Additional HTTP headers added to every request
httpClient ?ClientInterface null Custom PSR-18 client (testing, proxies). Defaults to a Guzzle client built from the options.
Scopes. Each endpoint requires a specific scope on the API key: WEBHOOKS_READ_WRITE, CONVERSATIONS_READ, CONTACTS_READ, CONTACTS_READ_WRITE, SMS_SEND. An insufficient scope returns HTTP 403, raised as AlloApiException with code API_KEY_INSUFFICIENT_SCOPE.

Resources

$allo->webhooks

WEBHOOKS_READ_WRITE

Manage webhook subscriptions. Withallo pushes events (CALL_RECEIVED, SMS_RECEIVED, CONTACT_CREATED, CONTACT_UPDATED) to your registered HTTPS URL.

Method HTTP Returns
list(): Webhook[] GET /webhooks array<Webhook>
create(CreateWebhookInput $input): Webhook POST /webhooks Webhook
delete(string $webhookId): void DELETE /webhooks/{id} void
php
use QrCommunication\Withallo\Dto\CreateWebhookInput;
use QrCommunication\Withallo\Enum\WebhookTopic;

$webhook = $allo->webhooks->create(new CreateWebhookInput(
    alloNumber: '+1234567890',
    url:        'https://example.com/webhooks/allo',
    topics:     [WebhookTopic::CALL_RECEIVED, WebhookTopic::SMS_RECEIVED],
    enabled:    true,
));

foreach ($allo->webhooks->list() as $w) {
    echo "{$w->id} — {$w->url} — " . implode(',', array_map(fn($t) => $t->value, $w->topics)) . "\n";
}

$allo->webhooks->delete($webhook->id);
Field casing caveat. The REST endpoint returns allo_number (snake_case) on GET and alloNumber (camelCase) on POST. The SDK normalises both into the DTO property Webhook::$alloNumber.

$allo->calls

CONVERSATIONS_READ
search(string $alloNumber, ?string $contactNumber = null, int $page = 0, int $size = 10): CallSearchResult

Search call history filtered by Allo number and optional contact number. Pagination is 0-indexed, capped at 100 per page.

ParameterTypeDescription
$alloNumberstringRequired — your Allo number (E.164).
$contactNumber?stringOptional — restrict to one contact.
$pageint0-indexed page. Default 0.
$sizeintPage size. Default 10, max 100.
php
$calls = $allo->calls->search(
    alloNumber: '+1234567890',
    page: 0,
    size: 20,
);

$pagination = $calls->metadata->pagination;
echo "Page {$pagination->currentPage}/{$pagination->totalPages}\n";

foreach ($calls->results as $call) {
    echo "{$call->type->value} {$call->fromNumber} → {$call->toNumber}\n";
    echo "  {$call->lengthInMinutes} min — {$call->startDate->format(DATE_ATOM)}\n";
    if ($call->recordingUrl) {
        echo "  Recording: {$call->recordingUrl}\n";
    }
    foreach ($call->transcript as $line) {
        echo "    [{$line->source->value}] {$line->text}\n";
    }
}

$allo->contacts

CONTACTS_READ / CONTACTS_READ_WRITE

CRUD over contacts plus the merged conversation history (calls + SMS).

Method HTTP Scope
search(int $page = 0, int $size = 10): ContactSearchResultGET /contactsCONTACTS_READ
get(string $contactId): ContactGET /contact/{id}CONTACTS_READ
create(CreateContactInput $input): ContactPOST /contactsCONTACTS_READ_WRITE
update(string $id, UpdateContactInput $input): ContactPUT /contacts/{id}CONTACTS_READ_WRITE
conversation(string $id, int $page = 0, int $size = 10): ConversationResultGET /contact/{id}/conversationCONVERSATIONS_READ
Path quirk. Withallo uses the plural /contacts for search and create, but the singular /contact/{id} for GET and conversation. The SDK abstracts the difference.
php
use QrCommunication\Withallo\Dto\CreateContactInput;
use QrCommunication\Withallo\Dto\UpdateContactInput;
use QrCommunication\Withallo\Enum\ConversationEntryType;

$contact = $allo->contacts->create(new CreateContactInput(
    numbers:  ['+33612345678'],
    name:     'Jean',
    lastName: 'Dupont',
    jobTitle: 'CTO',
    emails:   ['jean.dupont@example.com'],
));

// emails / numbers REPLACE the existing arrays on update
$updated = $allo->contacts->update($contact->id, new UpdateContactInput(
    jobTitle: 'CEO',
    emails: ['jean@example.com', 'contact@example.com'],
));

$conv = $allo->contacts->conversation($contact->id, page: 0, size: 20);
foreach ($conv->results as $entry) {
    if ($entry->type === ConversationEntryType::CALL) {
        echo "CALL {$entry->call->startDate->format(DATE_ATOM)} — {$entry->call->lengthInMinutes} min\n";
    } else {
        echo "SMS  {$entry->message->startDate->format(DATE_ATOM)} — {$entry->message->content}\n";
    }
}

$allo->sms

SMS_SEND

Send outbound SMS. Two flavors coexist because the payload shape depends on the destination country.

Method Input Use case
send(SendSmsInput $input): SentSms SendSmsInput(from, to, message) US / international. from must be one of your Allo numbers.
sendFrance(SendSmsFranceInput $input): SentSms SendSmsFranceInput(senderId, to, message) France. Requires a pre-verified alphanumeric sender (3–11 chars) — ARCEP ruling (Jan 2023) blocks business SMS from standard mobile numbers.
php
use QrCommunication\Withallo\Dto\SendSmsInput;
use QrCommunication\Withallo\Dto\SendSmsFranceInput;
use QrCommunication\Withallo\Exception\AlloApiException;

$sms = $allo->sms->send(new SendSmsInput(
    from:    '+1234567890',
    to:      '+0987654321',
    message: 'Hello from Allo',
));
echo "Sent : {$sms->content} @ {$sms->startDate->format(DATE_ATOM)}\n";

// France — sender_id must be pre-approved by Allo support
try {
    $allo->sms->sendFrance(new SendSmsFranceInput(
        senderId: 'MyCompany',
        to:       '+33612345678',
        message:  'Bonjour depuis Allo',
    ));
} catch (AlloApiException $e) {
    if ($e->code === 'INVALID_SENDER_ID') {
        echo "Sender not verified — contact Allo support.\n";
    }
    throw $e;
}

$allo->numbers

CONVERSATIONS_READ
list(): array<PhoneNumber>

List the Allo phone numbers connected to your account.

php
foreach ($allo->numbers->list() as $n) {
    $shared = $n->isSharedNumber ? ' (shared)' : '';
    echo "{$n->country}  {$n->name}{$shared} — {$n->number}\n";
}

DTOs & Enums

Every response is deserialised into readonly DTOs. Every enum value is a native PHP 8.2 backed enum — autocomplete-friendly and statically analysable.

Main DTOs

DTO Key fields
Webhookid, alloNumber, enabled, url, topics: WebhookTopic[]
Callid, fromNumber, toNumber, lengthInMinutes, type: CallDirection, result: ?CallResult, startDate: DateTimeImmutable, transcript: TranscriptLine[]
Contactid, name, lastName, jobTitle, numbers: string[], emails: string[], company: ?Company, createdAt: DateTimeImmutable
SentSmsfromNumber, senderId, toNumber, type: CallDirection, content, startDate
PhoneNumbernumber, name, country, isSharedNumber
ConversationEntrytype: ConversationEntryType, call: ?Call, message: ?SmsMessage
PaginationMetadatapagination->totalPages, pagination->currentPage

Input DTOs

DTOConstructor
CreateWebhookInputalloNumber, url, topics: WebhookTopic[], enabled = true
CreateContactInputnumbers: string[], name?, lastName?, jobTitle?, website?, emails?
UpdateContactInputname?, lastName?, jobTitle?, website?, emails?, numbers?
SendSmsInputfrom, to, message
SendSmsFranceInputsenderId, to, message

Enums (native PHP 8.2)

php
namespace QrCommunication\Withallo\Enum;

enum WebhookTopic: string {
    case CALL_RECEIVED    = 'CALL_RECEIVED';
    case SMS_RECEIVED     = 'SMS_RECEIVED';
    case CONTACT_CREATED  = 'CONTACT_CREATED';
    case CONTACT_UPDATED  = 'CONTACT_UPDATED';
}

enum CallDirection: string {
    case INBOUND  = 'INBOUND';
    case OUTBOUND = 'OUTBOUND';
}

enum CallResult: string {
    case ANSWERED             = 'ANSWERED';
    case VOICEMAIL            = 'VOICEMAIL';
    case TRANSFERRED_AI       = 'TRANSFERRED_AI';
    case TRANSFERRED_EXTERNAL = 'TRANSFERRED_EXTERNAL';
    case BLOCKED              = 'BLOCKED';
    case FAILED               = 'FAILED';
}

enum TranscriptSource: string {
    case AGENT    = 'AGENT';
    case EXTERNAL = 'EXTERNAL';
    case USER     = 'USER';
}

enum ConversationEntryType: string {
    case CALL         = 'CALL';
    case TEXT_MESSAGE = 'TEXT_MESSAGE';
}

WebhookReceiver

The SDK ships a framework-agnostic WebhookReceiver that parses incoming JSON, dispatches to typed handlers, and validates the allo_number value.

No HMAC signature. As of April 2026 Withallo does not publish a signing secret for incoming webhooks. Harden your endpoint with HTTPS, an unguessable URL, and an allo_number allow-list.
php
use QrCommunication\Withallo\Webhook\WebhookReceiver;
use QrCommunication\Withallo\Webhook\Payload\CallReceivedPayload;
use QrCommunication\Withallo\Webhook\Payload\SmsReceivedPayload;
use QrCommunication\Withallo\Webhook\Payload\ContactEventPayload;

$receiver = (new WebhookReceiver())
    ->allowedAlloNumbers(['+1234567890'])
    ->onCallReceived(function (CallReceivedPayload $p): void {
        // Persist, notify, enqueue transcript analysis…
    })
    ->onSmsReceived(function (SmsReceivedPayload $p): void {
        // …
    })
    ->onContactCreated(function (ContactEventPayload $p): void {
        // …
    })
    ->onContactUpdated(function (ContactEventPayload $p): void {
        // …
    });

// In your controller (Laravel example):
Route::post('/webhooks/allo/{token}', function (Request $request, string $token) use ($receiver) {
    if ($token !== config('services.allo.webhook_token')) {
        abort(404);
    }
    $receiver->handle($request->getContent());
    return response()->noContent();
});

Error Handling

Every SDK call throws AlloApiException on 4xx/5xx. The exception exposes code (machine-readable), status (HTTP) and details (validation errors).

php
use QrCommunication\Withallo\Exception\AlloApiException;

try {
    $allo->sms->sendFrance(new SendSmsFranceInput(
        senderId: 'Unverified',
        to: '+33612345678',
        message: 'Hi',
    ));
} catch (AlloApiException $e) {
    echo "HTTP {$e->status} — code={$e->code} — {$e->getMessage()}\n";

    foreach ($e->details as $detail) {
        echo "  · {$detail->field} : {$detail->message}\n";
    }

    match ($e->code) {
        'API_KEY_INVALID'            => /* 401 */ null,
        'API_KEY_INSUFFICIENT_SCOPE' => /* 403 */ null,
        'RATE_LIMITED'               => /* 429 — respect Retry-After */ null,
        'INVALID_SENDER_ID'          => /* 400 France */ null,
        default                      => throw $e,
    };
}

Common error codes

HTTP code Meaning
401API_KEY_INVALIDMissing or invalid API key
403API_KEY_INSUFFICIENT_SCOPEKey lacks the required scope
404WEBHOOK_NOT_FOUND, CONTACT_NOT_FOUNDResource does not exist
400INVALID_SENDER_IDFrench sender_id not verified
429RATE_LIMITEDBack off and retry after the Retry-After header

Examples

Paginate through every contact

php
/** @var list<Contact> $all */
$all = [];
$page = 0;
$size = 100;

do {
    $batch = $allo->contacts->search(page: $page, size: $size);
    $all = array_merge($all, $batch->results);
    $page++;
} while ($page < $batch->metadata->pagination->totalPages);

echo count($all) . " contacts\n";

Retry with exponential back-off on rate limiting

php
use QrCommunication\Withallo\Exception\AlloApiException;

function withRetry(callable $fn, int $maxAttempts = 5): mixed
{
    $attempt = 0;
    while (true) {
        try {
            return $fn();
        } catch (AlloApiException $e) {
            if ($e->code !== 'RATE_LIMITED' || $attempt >= $maxAttempts) {
                throw $e;
            }
            $waitMs = min(1000 * (2 ** $attempt), 30_000);
            usleep($waitMs * 1000);
            $attempt++;
        }
    }
}

$sms = withRetry(fn () => $allo->sms->send(new SendSmsInput(
    from: '+1234567890', to: '+0987654321', message: 'Hi',
)));

Laravel integration (service provider)

Bind the client as a singleton so the Guzzle connection and the API key configuration are shared across the application.

php
// config/services.php
'allo' => [
    'api_key'        => env('ALLO_API_KEY'),
    'webhook_token'  => env('ALLO_WEBHOOK_TOKEN'),
],

// app/Providers/AppServiceProvider.php
use QrCommunication\Withallo\AlloClient;
use QrCommunication\Withallo\AlloClientOptions;

public function register(): void
{
    $this->app->singleton(AlloClient::class, function () {
        return new AlloClient(new AlloClientOptions(
            apiKey:  config('services.allo.api_key'),
            timeout: 30.0,
        ));
    });
}

// Usage in any controller / service:
public function send(AlloClient $allo, SendSmsRequest $request)
{
    $sms = $allo->sms->send(new SendSmsInput(
        from:    $request->validated('from'),
        to:      $request->validated('to'),
        message: $request->validated('message'),
    ));

    return response()->json(['sent_at' => $sms->startDate->format(DATE_ATOM)]);
}

AI Skill

The SDK includes an AI skill that installs complete documentation directly into your code agent (Claude Code, Cursor, Codex, Windsurf, Cline, Aider, Gemini CLI).

bash
bash vendor/qrcommunication/withallo-sdk/skill/install.sh

Manual installation: cp -r vendor/qrcommunication/withallo-sdk/skill ~/.claude/skills/sdk-withallo