---
title: "Withallo SDK — Documentation PHP"
description: "SDK PHP complet pour l'API Withallo (Allo). Couvre Webhooks, Calls, Contacts, SMS, Phone Numbers et inclut un WebhookReceiver pour les événements entrants. Compatible Laravel, Symfony et tout projet PHP moderne."
package: "qrcommunication/withallo-sdk"
version: "0.2.0"
language: "PHP"
license: "MIT"
requires: "PHP 8.2+, ext-json, ext-curl, guzzlehttp/guzzle ^7.8"
github: "https://github.com/QrCommunication/sdk-withallo-php"
packagist: "https://packagist.org/packages/qrcommunication/withallo-sdk"
last_updated: "2026-04-14"
tags:
  - withallo
  - allo
  - voip
  - telephony
  - sms
  - webhooks
  - php
  - laravel
  - symfony
---

# Withallo SDK PHP (`qrcommunication/withallo-sdk`)

SDK PHP complet pour l'API [Withallo (Allo)](https://help.withallo.com/en/api-reference/introduction). Architecture **Resource** : `$client->webhooks->create()`, `$client->sms->send()`, etc. Compatible Laravel, Symfony ou tout projet PHP moderne (PHP 8.2+).

Le SDK couvre l'intégralité de l'API publique en **5 ressources** — Webhooks, Calls, Contacts, SMS, Phone Numbers — et inclut un `WebhookReceiver` pour parser et dispatcher les payloads entrants (`CALL_RECEIVED`, `SMS_RECEIVED`, `CONTACT_CREATED`, `CONTACT_UPDATED`).

---

## Table des matières

1. [Installation](#installation)
2. [Démarrage rapide](#démarrage-rapide)
3. [Configuration](#configuration)
4. [Ressources](#ressources)
   - [Webhooks](#webhooks)
   - [Calls](#calls)
   - [Contacts](#contacts)
   - [SMS](#sms)
   - [Phone Numbers](#phone-numbers)
5. [Intégration Laravel / Symfony](#intégration-laravel--symfony)
6. [Webhooks entrants — `WebhookReceiver`](#webhooks-entrants--webhookreceiver)
7. [Gestion des erreurs](#gestion-des-erreurs)
8. [Sécurité des webhooks](#sécurité-des-webhooks)
9. [Tests](#tests)

---

## Installation

```bash
composer require qrcommunication/withallo-sdk
```

**Prérequis :**
- PHP 8.2+
- Extensions `ext-json` et `ext-curl`
- `guzzlehttp/guzzle ^7.8` (installé automatiquement)

Génération de la clé API : [web.withallo.com/settings/api](https://web.withallo.com/settings/api).

---

## Démarrage rapide

```php
<?php

use QrCommunication\Withallo\WithalloClient;
use QrCommunication\Withallo\Enums\WebhookTopic;

$client = new WithalloClient(apiKey: 'votre-api-key');

// Créer un webhook
$webhook = $client->webhooks->create(
    alloNumber: '+1234567890',
    url: 'https://example.com/webhooks/allo',
    topics: [
        WebhookTopic::CALL_RECEIVED,
        WebhookTopic::SMS_RECEIVED,
    ],
);

// Envoyer un SMS (US / International)
$client->sms->send(
    from: '+1234567890',
    to: '+0987654321',
    message: 'Hello from Withallo SDK',
);

// Envoyer un SMS France (Sender ID vérifié requis)
$client->sms->sendFrance(
    senderId: 'MyCompany',
    to: '+33612345678',
    message: 'Bonjour depuis le SDK Withallo',
);

// Lire un contact
$contact = $client->contacts->get('cnt_abc123');

// Rechercher l'historique d'appels
$result = $client->calls->search(alloNumber: '+1234567890', size: 50);

// Tester la connexion
$client->testConnection(); // bool
```

---

## Configuration

### Constructeur `WithalloClient`

```php
new WithalloClient(
    string $apiKey,                          // Clé API (requise)
    ?string $environment = null,             // 'production' (défaut) | personnalisé
    ?float $timeout = 30.0,                  // Timeout requête en secondes
    ?float $connectTimeout = 10.0,           // Timeout connexion en secondes
    ?string $userAgent = null,               // User-Agent HTTP personnalisé
    ?HttpClientInterface $httpClient = null, // Injection client HTTP (tests)
);
```

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `apiKey` | `string` | — | Clé API brute (pas de préfixe `Bearer`) |
| `environment` | `?string` | `'production'` | Environnement cible |
| `timeout` | `?float` | `30.0` | Timeout total en **secondes** |
| `connectTimeout` | `?float` | `10.0` | Timeout de connexion en **secondes** |
| `userAgent` | `?string` | `qrcommunication/withallo-sdk/{version}` | User-Agent HTTP |
| `httpClient` | `?HttpClientInterface` | Guzzle interne | Client HTTP custom (utile en test) |

**À noter :** l'authentification se fait via un header `Authorization: <API_KEY>` **sans** préfixe `Bearer`. Le SDK s'en occupe — ne pas l'ajouter manuellement.

---

## Ressources

### Architecture

```
WithalloClient
 ├─ webhooks       → Webhooks          (scope: WEBHOOKS_READ_WRITE)
 ├─ calls          → Calls             (scope: CONVERSATIONS_READ)
 ├─ contacts       → Contacts          (scope: CONTACTS_READ / CONTACTS_READ_WRITE)
 ├─ sms            → Sms               (scope: SMS_SEND)
 ├─ phoneNumbers   → PhoneNumbers      (scope: CONVERSATIONS_READ)
 └─ webhookReceiver() → WebhookReceiver (parsing + dispatch, pas de HTTP)
```

Chaque ressource exige un ou plusieurs **scopes** sur la clé API. Configurez-les depuis [web.withallo.com/settings/api](https://web.withallo.com/settings/api).

---

### Webhooks

Gère les abonnements webhook configurés sur votre compte Allo.

```php
use QrCommunication\Withallo\Enums\WebhookTopic;

// Lister
/** @var list<array<string, mixed>> $webhooks */
$webhooks = $client->webhooks->list();

// Créer
$webhook = $client->webhooks->create(
    alloNumber: '+1234567890',
    url: 'https://example.com/webhooks/allo',
    topics: [WebhookTopic::CALL_RECEIVED, WebhookTopic::SMS_RECEIVED],
    enabled: true, // optionnel, défaut true
);
// => ['id' => 'web-abc', 'alloNumber' => '+...', 'enabled' => true, ...]

// Supprimer
$client->webhooks->delete('web-2NfDKEm9sF8xK3pQr1Zt');
```

**Topics disponibles :**

| Topic | Déclenchement |
|-------|---------------|
| `WebhookTopic::CALL_RECEIVED` | Nouvel appel reçu (résumé IA inclus) |
| `WebhookTopic::SMS_RECEIVED` | SMS entrant ou sortant |
| `WebhookTopic::CONTACT_CREATED` | Nouveau contact ajouté |
| `WebhookTopic::CONTACT_UPDATED` | Contact modifié |

---

### Calls

```php
$result = $client->calls->search(
    alloNumber: '+1234567890',    // requis
    contactNumber: '+0987654321', // optionnel
    page: 0,                      // 0-indexed, défaut 0
    size: 10,                     // 1..100, défaut 10
);

// $result['results']  → list<array>
// $result['metadata'] → ['total_pages' => X, 'current_page' => Y]
```

---

### Contacts

```php
// Lire
$contact = $client->contacts->get('cnt_abc123');

// Rechercher (paginé)
$result = $client->contacts->search(page: 0, size: 20);

// Conversation associée à un contact
$conversation = $client->contacts->searchConversation('cnt_abc123', page: 0, size: 20);

// Créer (au moins un numéro requis)
$contact = $client->contacts->create(
    numbers: ['+15551234567'],
    name: 'John',
    lastName: 'Doe',
    jobTitle: 'CEO',
    website: 'https://acme.com',
    emails: ['john@acme.com'],
);

// Mettre à jour (PUT — emails/numbers remplacent l'existant)
$contact = $client->contacts->update('cnt_abc123', [
    'last_name' => 'Smith',
    'job_title' => 'CTO',
    'emails' => ['john@acme.com', 'j.smith@acme.com'],
]);
```

---

### SMS

```php
// États-Unis / International
$client->sms->send(
    from: '+1234567890',  // votre numéro Allo
    to: '+0987654321',
    message: 'Hello',
);

// France (Sender ID vérifié requis)
$client->sms->sendFrance(
    senderId: 'MyCompany', // 3-11 chars A-Z/0-9, ou shortcode
    to: '+33612345678',
    message: 'Bonjour',
);
```

> **Note France :** les opérateurs français bloquent les SMS émis depuis des numéros mobiles standards. Un **Alphanumeric Sender ID** ou un **Short Code** doit être obtenu auprès du support Allo avant utilisation.

---

### Phone Numbers

```php
$numbers = $client->phoneNumbers->list();
// [
//   ['number' => '+1234567890', 'name' => 'Main Line', 'country' => 'US', 'is_shared_number' => false],
//   ...
// ]
```

Utile au démarrage pour alimenter une whitelist de numéros Allo (cf. [Sécurité des webhooks](#sécurité-des-webhooks)).

---

## Intégration Laravel / Symfony

### Laravel — Service Provider

Enregistrez le client comme singleton dans un `ServiceProvider` pour profiter du container et de l'injection de dépendances :

```php
// app/Providers/AppServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use QrCommunication\Withallo\WithalloClient;

final class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(WithalloClient::class, fn () => new WithalloClient(
            apiKey: config('services.withallo.api_key'),
        ));
    }
}
```

```php
// config/services.php
return [
    // ...
    'withallo' => [
        'api_key' => env('WITHALLO_API_KEY'),
        'webhook_secret' => env('WITHALLO_WEBHOOK_SECRET'),
    ],
];
```

### Laravel — Service métier

```php
namespace App\Services;

use Illuminate\Support\Facades\Log;
use QrCommunication\Withallo\Exceptions\ApiException;
use QrCommunication\Withallo\Exceptions\ForbiddenException;
use QrCommunication\Withallo\Exceptions\RateLimitException;
use QrCommunication\Withallo\Exceptions\ValidationException;
use QrCommunication\Withallo\WithalloClient;

final readonly class SmsService
{
    public function __construct(private WithalloClient $client) {}

    /**
     * Envoie un SMS France avec retry automatique sur rate-limit.
     *
     * @return array<string, mixed>
     * @throws \RuntimeException si toutes les tentatives échouent
     */
    public function sendFranceWithRetry(
        string $senderId,
        string $to,
        string $message,
        int $maxAttempts = 3,
    ): array {
        for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
            try {
                return $this->client->sms->sendFrance(
                    senderId: $senderId,
                    to: $to,
                    message: $message,
                );
            } catch (RateLimitException $e) {
                $wait = $e->retryAfterSeconds ?? 2 ** $attempt;
                Log::warning('Withallo rate limit', [
                    'attempt' => $attempt,
                    'retry_after_s' => $wait,
                ]);
                sleep($wait);
            } catch (ValidationException $e) {
                Log::error('Withallo payload rejeté', ['errors' => $e->errors()]);
                throw $e; // pas de retry — payload invalide
            } catch (ForbiddenException $e) {
                Log::critical('Scope SMS_SEND manquant', [
                    'required' => $e->requiredScopes(),
                ]);
                throw $e;
            } catch (ApiException $e) {
                Log::error('Withallo API error', [
                    'http' => $e->httpStatus,
                    'code' => $e->getErrorCode(),
                ]);
                throw $e;
            }
        }

        throw new \RuntimeException(
            "Retries rate-limit épuisés après {$maxAttempts} tentatives",
        );
    }
}
```

### Symfony — Services

```yaml
# config/services.yaml
services:
    QrCommunication\Withallo\WithalloClient:
        arguments:
            $apiKey: '%env(WITHALLO_API_KEY)%'
```

Puis injectez `WithalloClient` directement dans vos controllers ou services via autowiring.

---

## Webhooks entrants — `WebhookReceiver`

`WebhookReceiver` parse les payloads entrants dans votre contrôleur et dispatche vers les handlers. Pas d'appel réseau, pas d'état.

```php
<?php

use QrCommunication\Withallo\Enums\WebhookTopic;
use QrCommunication\Withallo\Webhooks\WebhookEvent;
use QrCommunication\Withallo\Webhooks\WebhookReceiver;

$receiver = new WebhookReceiver;

$receiver
    ->on(WebhookTopic::CALL_RECEIVED, function (WebhookEvent $event): void {
        $callId = $event->get('id');
        $summary = $event->get('one_sentence_summary');
        // Persister l'appel, déclencher des alertes, mettre à jour le CRM...
    })
    ->on(WebhookTopic::SMS_RECEIVED, function (WebhookEvent $event): void {
        if ($event->get('direction') === 'INBOUND') {
            $content = $event->get('content');
            $from = $event->get('from_number');
            // Logique de réponse, routage par mot-clé...
        }
    })
    ->on(WebhookTopic::CONTACT_CREATED, fn (WebhookEvent $e) => /* ... */)
    ->on(WebhookTopic::CONTACT_UPDATED, fn (WebhookEvent $e) => /* ... */);

// Dans votre endpoint HTTP (PHP natif, Laravel, Symfony...)
$rawBody = file_get_contents('php://input');
$event = $receiver->handle($rawBody); // parse + dispatch

// TOUJOURS répondre 200 en moins de 30 secondes
http_response_code(200);
```

### Exemple : Controller Laravel

```php
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Jobs\ProcessAlloCall;
use App\Jobs\ProcessAlloInboundSms;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use QrCommunication\Withallo\Enums\WebhookTopic;
use QrCommunication\Withallo\Exceptions\InvalidWebhookPayloadException;
use QrCommunication\Withallo\Webhooks\WebhookEvent;
use QrCommunication\Withallo\Webhooks\WebhookReceiver;

final class WithalloWebhookController extends Controller
{
    public function handle(Request $request, string $token): JsonResponse
    {
        // Filtre d'origine léger — comparaison à temps constant
        if (! hash_equals(config('services.withallo.webhook_secret', ''), $token)) {
            return response()->json([], 404);
        }

        $receiver = (new WebhookReceiver)
            ->on(WebhookTopic::CALL_RECEIVED, function (WebhookEvent $e): void {
                dispatch(new ProcessAlloCall($e->get('id'), $e->data));
            })
            ->on(WebhookTopic::SMS_RECEIVED, function (WebhookEvent $e): void {
                if ($e->get('direction') === 'INBOUND') {
                    dispatch(new ProcessAlloInboundSms($e->data));
                }
            });

        try {
            $receiver->handle($request->getContent());
        } catch (InvalidWebhookPayloadException $e) {
            Log::warning('Webhook Withallo rejeté (payload malformé)', [
                'reason' => $e->getMessage(),
            ]);

            return response()->json(['error' => 'invalid_payload'], 400);
        } catch (\Throwable $e) {
            Log::error('Handler webhook Withallo en échec', [
                'exception' => $e::class,
                'message' => $e->getMessage(),
            ]);

            // 200 quand l'échec vient de notre code (sinon Withallo rejoue en boucle)
            return response()->json(['ok' => false], 200);
        }

        return response()->json(['ok' => true], 200);
    }
}
```

**Route associée :**

```php
// routes/web.php
Route::post('/webhooks/allo/{token}', [WithalloWebhookController::class, 'handle'])
    ->where('token', '[A-Za-z0-9]{32,}');
```

### API `WebhookEvent`

| Propriété / méthode | Description |
|---------------------|-------------|
| `$event->topic` | Le `WebhookTopic` du message |
| `$event->data` | `array<string, mixed>` payload spécifique au topic |
| `$event->raw` | `array<string, mixed>` enveloppe complète `{ topic, data }` |
| `$event->isCall()` | `true` si topic `CALL_RECEIVED` |
| `$event->isSms()` | `true` si topic `SMS_RECEIVED` |
| `$event->isContactCreated()` | `true` si topic `CONTACT_CREATED` |
| `$event->isContactUpdated()` | `true` si topic `CONTACT_UPDATED` |
| `$event->get('path.to.field', $default)` | Accès dot-notation sûr |

---

## Gestion des erreurs

Toutes les exceptions héritent de `QrCommunication\Withallo\Exceptions\WithalloException` :

```
WithalloException (RuntimeException)
├── ApiException            → erreur HTTP générique
│   │                         (httpStatus, responseBody, getErrorCode(), getDetails())
│   ├── AuthenticationException → 401 (API_KEY_INVALID)
│   ├── ForbiddenException      → 403 (API_KEY_INSUFFICIENT_SCOPE) + requiredScopes(): list<string>
│   ├── ValidationException     → 400/422 + errors(): array<string, string>
│   ├── NotFoundException       → 404
│   └── RateLimitException      → 429 + retryAfterSeconds: ?int
└── InvalidWebhookPayloadException → payload webhook malformé (parse)
```

### Exemple exhaustif avec retry sur rate limit

```php
<?php

use QrCommunication\Withallo\Enums\WebhookTopic;
use QrCommunication\Withallo\Exceptions\ApiException;
use QrCommunication\Withallo\Exceptions\AuthenticationException;
use QrCommunication\Withallo\Exceptions\ForbiddenException;
use QrCommunication\Withallo\Exceptions\NotFoundException;
use QrCommunication\Withallo\Exceptions\RateLimitException;
use QrCommunication\Withallo\Exceptions\ValidationException;
use QrCommunication\Withallo\WithalloClient;

$client = new WithalloClient(apiKey: getenv('WITHALLO_API_KEY') ?: '');

function createWithRetry(WithalloClient $client, string $alloNumber, string $url, int $maxAttempts = 3): array
{
    for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
        try {
            return $client->webhooks->create(
                alloNumber: $alloNumber,
                url: $url,
                topics: [WebhookTopic::CALL_RECEIVED],
            );
        } catch (RateLimitException $e) {
            $wait = $e->retryAfterSeconds ?? 2 ** $attempt;
            fwrite(STDERR, "rate-limit, retry dans {$wait}s ({$attempt}/{$maxAttempts})\n");
            sleep($wait);
        }
    }

    throw new RuntimeException('Retries rate-limit épuisés');
}

try {
    $webhook = createWithRetry($client, '+33188833451', 'https://example.com/hook');
    echo "Webhook créé id={$webhook['id']}\n";
} catch (AuthenticationException $e) {
    fwrite(STDERR, "Clé API invalide ou révoquée.\n");
    exit(1);
} catch (ForbiddenException $e) {
    $missing = implode(', ', $e->requiredScopes());
    fwrite(STDERR, "Scopes manquants : {$missing}\n");
    exit(1);
} catch (ValidationException $e) {
    fwrite(STDERR, "Payload rejeté :\n");
    foreach ($e->errors() as $field => $message) {
        fwrite(STDERR, "  - {$field} : {$message}\n");
    }
    exit(2);
} catch (NotFoundException $e) {
    fwrite(STDERR, "Ressource introuvable.\n");
    exit(3);
} catch (ApiException $e) {
    fwrite(STDERR, sprintf(
        "Erreur API non gérée (HTTP %d) : %s\n",
        $e->httpStatus,
        $e->getErrorCode() ?? $e->getMessage(),
    ));
    exit(10);
}
```

### Codes HTTP renvoyés par Withallo

| Status | Exception | Cas typique |
|--------|-----------|-------------|
| `400` | `ValidationException` | Payload mal formé |
| `401` | `AuthenticationException` | Clé API invalide ou révoquée |
| `403` | `ForbiddenException` | Scope manquant sur la clé (`API_KEY_INSUFFICIENT_SCOPE`) |
| `404` | `NotFoundException` | Ressource inexistante |
| `422` | `ValidationException` | Validation métier échouée |
| `429` | `RateLimitException` | Quota dépassé — lire `retryAfterSeconds` |
| `5xx` | `ApiException` | Erreur serveur — retry raisonnable |

---

## Sécurité des webhooks

> **Important :** à la date de publication de ce SDK (avril 2026), la documentation publique de Withallo **ne spécifie aucun en-tête de signature HMAC** permettant de vérifier cryptographiquement l'origine des webhooks. Le SDK valide la *forme* du payload mais ne peut pas authentifier l'expéditeur.

**Durcissement recommandé :**

- Servir l'endpoint webhook **en HTTPS uniquement**
- Utiliser une **URL secrète** avec un token non-devinable dans le path (≥ 32 caractères), comparé via `hash_equals()` pour éviter les timing attacks
- **Whitelist des IPs egress Withallo** au firewall si elles sont publiées par Withallo
- Rejeter les payloads dont le `allo_number` (ou `from_number` / `to_number`) ne **correspond pas à vos propres numéros** (récupérés via `$client->phoneNumbers->list()` au démarrage)
- Répondre `200 OK` en **moins de 30 secondes**, sinon Withallo considère la livraison comme échouée et rejoue l'événement

Si Withallo publie un schéma de signature, une méthode `WebhookReceiver::verifySignature(string $rawBody, string $signatureHeader, string $secret)` sera ajoutée sans breaking change.

---

## Tests

Le SDK est testé avec PHPUnit + Guzzle Mock Handler. Pour tester votre intégration :

```php
<?php

use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use QrCommunication\Withallo\WithalloClient;

final class WebhooksListTest extends TestCase
{
    public function test_list_returns_array(): void
    {
        $mock = new MockHandler([
            new Response(200, [], json_encode(['data' => [
                ['allo_number' => '+1234567890', 'enabled' => true, 'url' => '...'],
            ]])),
        ]);

        $httpClient = new Client(['handler' => HandlerStack::create($mock)]);
        $client = new WithalloClient(apiKey: 'test-key', httpClient: $httpClient);

        $webhooks = $client->webhooks->list();

        $this->assertCount(1, $webhooks);
        $this->assertSame('+1234567890', $webhooks[0]['allo_number']);
    }
}
```

### Test du `WebhookReceiver`

```php
<?php

use QrCommunication\Withallo\Enums\WebhookTopic;
use QrCommunication\Withallo\Webhooks\WebhookEvent;
use QrCommunication\Withallo\Webhooks\WebhookReceiver;

final class WebhookReceiverTest extends TestCase
{
    public function test_dispatches_call_received(): void
    {
        $received = null;
        $receiver = (new WebhookReceiver)
            ->on(WebhookTopic::CALL_RECEIVED, function (WebhookEvent $e) use (&$received) {
                $received = $e;
            });

        $rawBody = json_encode([
            'topic' => 'CALL_RECEIVED',
            'data' => [
                'id' => 'call_123',
                'from_number' => '+33612345678',
                'result' => 'ANSWERED',
                'length' => 42,
            ],
        ]);

        $receiver->handle($rawBody);

        $this->assertNotNull($received);
        $this->assertSame(WebhookTopic::CALL_RECEIVED, $received->topic);
        $this->assertSame('call_123', $received->get('id'));
    }
}
```

### Commandes utiles

```bash
# Tests
composer test

# Analyse statique (PHPStan niveau 8)
composer analyse

# Formatage (Laravel Pint)
composer lint
```

---

## Liens

- **Packagist** : https://packagist.org/packages/qrcommunication/withallo-sdk
- **GitHub** : https://github.com/QrCommunication/sdk-withallo-php
- **CHANGELOG** : https://github.com/QrCommunication/sdk-withallo-php/blob/main/CHANGELOG.md
- **SDK JavaScript / React** : https://github.com/QrCommunication/sdk-withallo-js
- **Documentation API Withallo (FR)** : https://help.withallo.com/fr/api-reference/introduction
- **Console API keys** : https://web.withallo.com/settings/api
