Installation
composer require qrcommunication/withallo-sdk
ext-json.
The guzzlehttp/guzzle ^7.8 dependency is installed automatically by Composer.
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
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.
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. |
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 |
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);
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.
| Parameter | Type | Description |
|---|---|---|
$alloNumber | string | Required — your Allo number (E.164). |
$contactNumber | ?string | Optional — restrict to one contact. |
$page | int | 0-indexed page. Default 0. |
$size | int | Page size. Default 10, max 100. |
$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): ContactSearchResult | GET /contacts | CONTACTS_READ |
get(string $contactId): Contact | GET /contact/{id} | CONTACTS_READ |
create(CreateContactInput $input): Contact | POST /contacts | CONTACTS_READ_WRITE |
update(string $id, UpdateContactInput $input): Contact | PUT /contacts/{id} | CONTACTS_READ_WRITE |
conversation(string $id, int $page = 0, int $size = 10): ConversationResult | GET /contact/{id}/conversation | CONVERSATIONS_READ |
/contacts
for search and create, but the singular
/contact/{id} for GET and conversation. The SDK abstracts the difference.
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. |
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.
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 |
|---|---|
Webhook | id, alloNumber, enabled, url, topics: WebhookTopic[] |
Call | id, fromNumber, toNumber, lengthInMinutes, type: CallDirection, result: ?CallResult, startDate: DateTimeImmutable, transcript: TranscriptLine[] |
Contact | id, name, lastName, jobTitle, numbers: string[], emails: string[], company: ?Company, createdAt: DateTimeImmutable |
SentSms | fromNumber, senderId, toNumber, type: CallDirection, content, startDate |
PhoneNumber | number, name, country, isSharedNumber |
ConversationEntry | type: ConversationEntryType, call: ?Call, message: ?SmsMessage |
PaginationMetadata | pagination->totalPages, pagination->currentPage |
Input DTOs
| DTO | Constructor |
|---|---|
CreateWebhookInput | alloNumber, url, topics: WebhookTopic[], enabled = true |
CreateContactInput | numbers: string[], name?, lastName?, jobTitle?, website?, emails? |
UpdateContactInput | name?, lastName?, jobTitle?, website?, emails?, numbers? |
SendSmsInput | from, to, message |
SendSmsFranceInput | senderId, to, message |
Enums (native PHP 8.2)
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.
allo_number allow-list.
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).
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 |
|---|---|---|
| 401 | API_KEY_INVALID | Missing or invalid API key |
| 403 | API_KEY_INSUFFICIENT_SCOPE | Key lacks the required scope |
| 404 | WEBHOOK_NOT_FOUND, CONTACT_NOT_FOUND | Resource does not exist |
| 400 | INVALID_SENDER_ID | French sender_id not verified |
| 429 | RATE_LIMITED | Back off and retry after the Retry-After header |
Examples
Paginate through every contact
/** @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
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.
// 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 vendor/qrcommunication/withallo-sdk/skill/install.sh
Manual installation:
cp -r vendor/qrcommunication/withallo-sdk/skill ~/.claude/skills/sdk-withallo