---
title: "Withallo SDK — Documentation JavaScript / TypeScript"
description: "SDK TypeScript isomorphe pour l'API Withallo (Allo). Couvre Webhooks, Calls, Contacts, SMS, Phone Numbers et un WebhookReceiver pour les événements entrants. Utilisable côté Node, navigateur, Deno, React Native et edge runtimes, avec des hooks React optionnels."
package: "@qrcommunication/withallo-sdk"
version: "0.2.0"
language: "TypeScript"
license: "MIT"
requires: "Node.js 18+, zéro dépendance runtime"
github: "https://github.com/QrCommunication/sdk-withallo-js"
npm: "https://www.npmjs.com/package/@qrcommunication/withallo-sdk"
last_updated: "2026-04-14"
tags:
  - withallo
  - allo
  - voip
  - telephony
  - sms
  - webhooks
  - typescript
  - javascript
  - react
  - nextjs
---

# Withallo SDK JavaScript / TypeScript (`@qrcommunication/withallo-sdk`)

SDK TypeScript **isomorphe** pour l'API [Withallo (Allo)](https://help.withallo.com/en/api-reference/introduction). Le core fonctionne partout où `fetch` est disponible : Node 18+, navigateurs modernes, Deno, Bun, React Native et edge runtimes. Un sous-module `/react` fournit un provider et des hooks prêts à l'emploi pour les dashboards admin.

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. [Hooks React](#hooks-react)
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
# npm
npm install @qrcommunication/withallo-sdk

# pnpm
pnpm add @qrcommunication/withallo-sdk

# yarn
yarn add @qrcommunication/withallo-sdk

# bun
bun add @qrcommunication/withallo-sdk
```

**Prérequis :**
- `fetch` global (Node 18+, navigateurs modernes, Deno, Bun)
- TypeScript 5.0+ recommandé
- **Zéro dépendance runtime** — uniquement le `fetch` natif

Pour les runtimes sans `fetch` natif, passez un polyfill via `options.fetch`.

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

---

## Démarrage rapide

```typescript
import { WithalloClient, WebhookTopic } from "@qrcommunication/withallo-sdk";

const client = new WithalloClient({
  apiKey: process.env.WITHALLO_API_KEY!,
});

// Créer un webhook
await client.webhooks.create({
  alloNumber: "+1234567890",
  url: "https://example.com/webhooks/allo",
  topics: [WebhookTopic.CALL_RECEIVED, WebhookTopic.SMS_RECEIVED],
});

// Envoyer un SMS (US / International)
await client.sms.send({
  from: "+1234567890",
  to: "+0987654321",
  message: "Hello from Withallo SDK",
});

// Envoyer un SMS France (Sender ID vérifié requis)
await client.sms.sendFrance({
  senderId: "MyCompany",
  to: "+33612345678",
  message: "Bonjour depuis le SDK Withallo",
});

// Rechercher l'historique d'appels
const page = await client.calls.search({
  alloNumber: "+1234567890",
  size: 50,
});

// Vérifier la connexion et la clé API
await client.testConnection(); // Promise<boolean>
```

---

## Configuration

### `WithalloClientOptions`

```typescript
interface WithalloClientOptions {
  apiKey: string;                    // Clé API Withallo (requise)
  baseUrl?: string;                  // Défaut: 'https://api.withallo.com/v1/api'
  timeout?: number;                  // Timeout en ms. Défaut: 30000
  fetch?: typeof globalThis.fetch;   // Polyfill fetch pour les runtimes sans fetch natif
  userAgent?: string;                // User-Agent HTTP personnalisé
  headers?: Record<string, string>;  // Headers additionnels injectés à chaque requête
}
```

| Propriété | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `apiKey` | `string` | — | Clé API brute (pas de préfixe `Bearer`) |
| `baseUrl` | `string?` | `https://api.withallo.com/v1/api` | URL de base de l'API |
| `timeout` | `number?` | `30000` | Timeout par requête (AbortController) |
| `fetch` | `typeof fetch?` | `globalThis.fetch` | Polyfill pour runtimes sans fetch natif |
| `userAgent` | `string?` | `@qrcommunication/withallo-sdk/{version}` | User-Agent HTTP |
| `headers` | `Record<string,string>?` | `{}` | Headers HTTP additionnels |

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

---

## Ressources

### Architecture

```
WithalloClient
 ├─ webhooks       → WebhooksResource      (scope: WEBHOOKS_READ_WRITE)
 ├─ calls          → CallsResource         (scope: CONVERSATIONS_READ)
 ├─ contacts       → ContactsResource      (scopes: CONTACTS_READ / CONTACTS_READ_WRITE)
 ├─ sms            → SmsResource           (scope: SMS_SEND)
 ├─ phoneNumbers   → PhoneNumbersResource  (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.

```typescript
// Lister
const webhooks = await client.webhooks.list();
// => Webhook[]

// Créer
const webhook = await client.webhooks.create({
  alloNumber: "+1234567890",
  url: "https://example.com/hook",
  topics: [WebhookTopic.CALL_RECEIVED, WebhookTopic.SMS_RECEIVED],
  enabled: true, // défaut true
});
// => Webhook

// Supprimer
await 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

Recherche paginée de l'historique des appels.

```typescript
const page = await client.calls.search({
  alloNumber: "+1234567890",    // requis
  contactNumber: "+0987654321", // optionnel — filtre par interlocuteur
  page: 0,                      // 0-indexed. Défaut: 0
  size: 10,                     // 1..100. Défaut: 10
});

// page.results  → Call[]
// page.metadata → { pagination?: { total_pages, current_page } }
```

---

### Contacts

CRUD complet sur le carnet de contacts.

```typescript
// Lire un contact par ID
const contact = await client.contacts.get("cnt_abc123");

// Rechercher (paginé)
const list = await client.contacts.search(0, 20);

// Conversation liée à un contact
const conversation = await client.contacts.searchConversation("cnt_abc123", 0, 20);

// Créer (au moins un numéro requis)
await 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)
await client.contacts.update("cnt_abc123", {
  last_name: "Smith",
  job_title: "CTO",
  emails: ["john@acme.com", "j.smith@acme.com"],
});
```

---

### SMS

```typescript
// US / International — envoi depuis un numéro Allo
await client.sms.send({
  from: "+1234567890",
  to: "+0987654321",
  message: "Hello",
});

// France — Sender ID vérifié requis
await client.sms.sendFrance({
  senderId: "MyCompany", // 3-11 chars alphanumériques OU short code
  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

```typescript
const numbers = await client.phoneNumbers.list();
// => PhoneNumber[]
// [
//   { number: "+1234567890", name: "Main Line", country: "US", is_shared_number: false },
//   ...
// ]
```

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

---

## Hooks React

Le package expose un point d'entrée séparé `@qrcommunication/withallo-sdk/react` avec un provider et des hooks typés.

```tsx
import {
  WithalloProvider,
  useSendSms,
  useWebhooks,
  useCreateWebhook,
  useDeleteWebhook,
} from "@qrcommunication/withallo-sdk/react";
import { WebhookTopic } from "@qrcommunication/withallo-sdk";

// 1. Envelopper votre app
function App() {
  return (
    <WithalloProvider options={{ apiKey: process.env.NEXT_PUBLIC_WITHALLO_KEY! }}>
      <Dashboard />
    </WithalloProvider>
  );
}

// 2. Utiliser les hooks
function SendSmsForm() {
  const { run, isPending, error, isSuccess } = useSendSms();

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        const form = new FormData(e.currentTarget);
        void run({
          from: form.get("from") as string,
          to: form.get("to") as string,
          message: form.get("message") as string,
        });
      }}
    >
      <input name="from" placeholder="+1..." required />
      <input name="to" placeholder="+1..." required />
      <textarea name="message" required />
      <button disabled={isPending}>
        {isPending ? "Envoi…" : "Envoyer SMS"}
      </button>
      {error && <p role="alert">Erreur : {error.message}</p>}
      {isSuccess && <p>Envoyé ✓</p>}
    </form>
  );
}

function WebhooksList() {
  const { webhooks, isLoading, error, refresh } = useWebhooks();
  const createWebhook = useCreateWebhook();
  const deleteWebhook = useDeleteWebhook();

  if (isLoading) return <p>Chargement…</p>;
  if (error) return <p role="alert">Erreur : {error.message}</p>;

  return (
    <ul>
      {webhooks.map((w) => (
        <li key={w.id ?? w.url}>
          <code>{w.url}</code> → {(w.topics ?? []).join(", ")}
          {w.id && (
            <button
              disabled={deleteWebhook.isPending}
              onClick={async () => {
                await deleteWebhook.run(w.id!);
                await refresh();
              }}
            >
              Supprimer
            </button>
          )}
        </li>
      ))}
    </ul>
  );
}
```

### Hooks disponibles

| Hook | Description |
|------|-------------|
| `useWithallo()` | Retourne le `WithalloClient` courant (lève si pas de provider) |
| `useSendSms()` | Mutation `client.sms.send()` avec `isPending / data / error` |
| `useSendSmsFrance()` | Mutation `client.sms.sendFrance()` |
| `useWebhooks()` | Query auto-fetch de la liste des webhooks + `refresh()` |
| `useCreateWebhook()` | Mutation `client.webhooks.create()` |
| `useDeleteWebhook()` | Mutation `client.webhooks.delete()` |
| `useAsyncAction(fn)` | Hook bas-niveau pour wrapper n'importe quelle action async |

Tous les hooks de type mutation retournent un objet uniforme :

```typescript
interface MutationResult<TData, TArgs> {
  data: TData | null;
  error: Error | null;
  isPending: boolean;
  isSuccess: boolean;
  isError: boolean;
  run: (args: TArgs) => Promise<TData>;
  reset: () => void;
}
```

> **Sécurité :** ne jamais exposer une clé API via `NEXT_PUBLIC_*` pour une application publique. Les hooks sont destinés à des **dashboards admin authentifiés** ou à des **intégrations Node SSR**. Pour des clients publics, proxyfier les appels par votre backend.

---

## Webhooks entrants — `WebhookReceiver`

Le `WebhookReceiver` parse les payloads entrants et dispatche vers les handlers que vous enregistrez. Pas d'appel réseau, pas d'état.

```typescript
import { WebhookReceiver, WebhookTopic } from "@qrcommunication/withallo-sdk";
import type {
  CallReceivedPayload,
  SmsReceivedPayload,
  ContactEventPayload,
} from "@qrcommunication/withallo-sdk";

const receiver = new WebhookReceiver();

receiver
  .on(WebhookTopic.CALL_RECEIVED, async (event) => {
    const payload = event.data as CallReceivedPayload;
    console.log(`Appel ${payload.id} — ${payload.result} — ${payload.length}s`);
  })
  .on(WebhookTopic.SMS_RECEIVED, async (event) => {
    const payload = event.data as SmsReceivedPayload;
    if (payload.direction === "INBOUND") {
      console.log(`SMS entrant de ${payload.from_number} : ${payload.content}`);
    }
  })
  .on(WebhookTopic.CONTACT_CREATED, async (event) => {
    const payload = event.data as ContactEventPayload;
    console.log(`Contact créé : ${payload.id}`);
  })
  .on(WebhookTopic.CONTACT_UPDATED, async (event) => {
    const payload = event.data as ContactEventPayload;
    console.log(`Contact mis à jour : ${payload.id}`);
  });
```

### Exemple : Next.js App Router

```typescript
// app/api/webhooks/allo/route.ts
import { WebhookReceiver, WebhookTopic } from "@qrcommunication/withallo-sdk";

const receiver = new WebhookReceiver();

receiver.on(WebhookTopic.CALL_RECEIVED, async (event) => {
  // Persister l'appel, déclencher des alertes, mettre à jour le CRM...
});

export async function POST(request: Request): Promise<Response> {
  // Filtre d'origine léger : token secret dans le path/query
  const url = new URL(request.url);
  if (url.searchParams.get("token") !== process.env.WEBHOOK_SECRET) {
    return new Response(null, { status: 404 });
  }

  const rawBody = await request.text();

  try {
    await receiver.handle(rawBody);
    return new Response(null, { status: 200 });
  } catch (err) {
    console.error("[withallo] webhook processing failed:", err);
    return new Response(null, { status: 400 });
  }
}

// Désactive le parsing automatique de Next.js — le body brut est requis
export const dynamic = "force-dynamic";
```

### Exemple : Express

```typescript
import express from "express";
import { WebhookReceiver, WebhookTopic } from "@qrcommunication/withallo-sdk";

const app = express();
const receiver = new WebhookReceiver();

receiver.on(WebhookTopic.SMS_RECEIVED, async (event) => {
  // logique métier
});

app.post("/webhooks/allo", express.text({ type: "*/*" }), async (req, res) => {
  try {
    await receiver.handle(req.body as string);
    res.status(200).end();
  } catch {
    res.status(400).end();
  }
});
```

### API `WebhookEvent`

| Propriété / méthode | Description |
|---------------------|-------------|
| `event.topic` | `WebhookTopic` du message |
| `event.data` | Payload typé spécifique au topic (`TData` générique) |
| `event.raw` | Enveloppe JSON complète `{ topic, data }` |
| `event.isCall()` | `true` si `CALL_RECEIVED` |
| `event.isSms()` | `true` si `SMS_RECEIVED` |
| `event.isContactCreated()` | `true` si `CONTACT_CREATED` |
| `event.isContactUpdated()` | `true` si `CONTACT_UPDATED` |
| `event.get<T>(path, fallback?)` | Accès dot-notation sûr (`event.get<string>("from_number")`) |

---

## Gestion des erreurs

Toutes les exceptions héritent de `WithalloError` :

```
WithalloError
├── ApiError (httpStatus, responseBody, getErrorCode(), getDetails())
│   ├── AuthenticationError  → 401 (API_KEY_INVALID)
│   ├── ForbiddenError       → 403 (requiredScopes(): string[])
│   ├── ValidationError      → 400/422 (errors(): Record<string, string>)
│   ├── NotFoundError        → 404
│   └── RateLimitError       → 429 (retryAfterSeconds: number | null)
└── InvalidWebhookPayloadError → payload webhook malformé
```

### Exemple exhaustif avec retry sur rate limit

```typescript
import {
  WithalloClient,
  WebhookTopic,
  ApiError,
  AuthenticationError,
  ForbiddenError,
  NotFoundError,
  RateLimitError,
  ValidationError,
} from "@qrcommunication/withallo-sdk";

const client = new WithalloClient({ apiKey: process.env.WITHALLO_API_KEY! });

async function createWebhookWithRetry(alloNumber: string, url: string) {
  const maxAttempts = 3;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await client.webhooks.create({
        alloNumber,
        url,
        topics: [WebhookTopic.CALL_RECEIVED],
      });
    } catch (err) {
      if (err instanceof RateLimitError) {
        const wait = (err.retryAfterSeconds ?? 2 ** attempt) * 1000;
        console.warn(`Rate-limited, retry dans ${wait}ms (${attempt}/${maxAttempts})`);
        await new Promise((r) => setTimeout(r, wait));
        continue;
      }
      throw err;
    }
  }
  throw new Error("Retries rate-limit épuisés");
}

try {
  await createWebhookWithRetry("+33188833451", "https://example.com/hook");
} catch (err) {
  if (err instanceof AuthenticationError) {
    console.error("Clé API invalide ou révoquée.");
  } else if (err instanceof ForbiddenError) {
    console.error("Scopes manquants :", err.requiredScopes().join(", "));
  } else if (err instanceof ValidationError) {
    console.error("Payload rejeté :", err.errors());
    // ex: { url: "must be https", topics: "at least one required" }
  } else if (err instanceof NotFoundError) {
    console.error("Ressource introuvable.");
  } else if (err instanceof ApiError) {
    console.error(`HTTP ${err.httpStatus} — ${err.getErrorCode()}`, err.getDetails());
  } else {
    throw err;
  }
}
```

### Codes HTTP renvoyés par Withallo

| Status | Exception | Cas typique |
|--------|-----------|-------------|
| `400` | `ValidationError` | Payload mal formé |
| `401` | `AuthenticationError` | Clé API invalide ou révoquée |
| `403` | `ForbiddenError` | Scope manquant sur la clé (`API_KEY_INSUFFICIENT_SCOPE`) |
| `404` | `NotFoundError` | Ressource inexistante |
| `422` | `ValidationError` | Validation métier échouée |
| `429` | `RateLimitError` | Quota dépassé — lire `retryAfterSeconds` |
| `5xx` | `ApiError` | 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 ou la query string (≥ 32 caractères)
- **Whitelist des IPs egress Withallo** au firewall si elles sont publiées par Withallo
- Rejeter les payloads dont `allo_number` ne fait **pas partie de 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(rawBody, signature, secret)` sera ajoutée au SDK sans breaking change.

---

## Tests

Le SDK est testé avec Vitest contre un client HTTP mocké. Pour tester votre intégration :

```typescript
import { WithalloClient } from "@qrcommunication/withallo-sdk";

// Injection d'un fetch custom pour les tests
const fakeFetch = vi.fn().mockResolvedValue(
  new Response(JSON.stringify({ data: [] }), { status: 200 }),
);

const client = new WithalloClient({
  apiKey: "test-key",
  fetch: fakeFetch,
});

await client.webhooks.list();
expect(fakeFetch).toHaveBeenCalledWith(
  expect.stringContaining("/webhooks"),
  expect.objectContaining({ method: "GET" }),
);
```

### Test du `WebhookReceiver`

```typescript
import { WebhookReceiver, WebhookTopic } from "@qrcommunication/withallo-sdk";

const receiver = new WebhookReceiver();
const handler = vi.fn();

receiver.on(WebhookTopic.CALL_RECEIVED, handler);

const rawBody = JSON.stringify({
  topic: "CALL_RECEIVED",
  data: { id: "call_123", from_number: "+33612345678", result: "ANSWERED", length: 42 },
});

await receiver.handle(rawBody);

expect(handler).toHaveBeenCalledWith(
  expect.objectContaining({
    topic: WebhookTopic.CALL_RECEIVED,
    data: expect.objectContaining({ id: "call_123" }),
  }),
);
```

---

## Liens

- **npm** : https://www.npmjs.com/package/@qrcommunication/withallo-sdk
- **GitHub** : https://github.com/QrCommunication/sdk-withallo-js
- **CHANGELOG** : https://github.com/QrCommunication/sdk-withallo-js/blob/main/CHANGELOG.md
- **SDK PHP** : https://github.com/QrCommunication/sdk-withallo-php
- **Documentation API Withallo (FR)** : https://help.withallo.com/fr/api-reference/introduction
- **Console API keys** : https://web.withallo.com/settings/api
