1 Installation
npm install @qrcommunication/withallo-sdk
pnpm add @qrcommunication/withallo-sdk
yarn add @qrcommunication/withallo-sdk
fetch (disponible nativement depuis Node.js 18+). Aucune dépendance externe n'est embarquée.
Prérequis
| Environnement | Version minimale |
|---|---|
| Node.js | 18.0.0+ |
| TypeScript | 5.0+ (recommandé 5.7+) |
| React (optional — hooks) | 18.0+ / 19.x |
| Deno | 1.30+ (via npm specifier) |
| Bun | 1.0+ |
Métadonnées du package
| npm | @qrcommunication/withallo-sdk |
| Version | 0.2.0 |
| Sources | github.com/QrCommunication/sdk-withallo-js |
| Licence | MIT |
| Base URL | https://api.withallo.com/v1/api |
2 Quick Start
import { AlloClient } from '@qrcommunication/withallo-sdk';
// Initialiser le client (clé API envoyée telle quelle, PAS de préfixe Bearer)
const allo = new AlloClient({
apiKey: process.env.ALLO_API_KEY!,
timeout: 30_000,
});
// ─── Lister les numéros connectés au compte ──
const numbers = await allo.numbers.list();
for (const n of numbers) {
console.log(`${n.name} — ${n.number} (${n.country})`);
}
// ─── Envoyer un SMS transactionnel (US / international) ──
const sms = await allo.sms.send({
from: '+1234567890',
to: '+0987654321',
message: 'Hello from Allo',
});
console.log('Sent:', sms.start_date);
// ─── Envoyer un SMS depuis la France (sender_id pré-vérifié obligatoire) ──
await allo.sms.sendFrance({
sender_id: 'MyCompany',
to: '+33612345678',
message: 'Bonjour depuis Allo',
});
// ─── Rechercher l'historique d'appels pour un numéro Allo ──
const calls = await allo.calls.search({
allo_number: '+1234567890',
page: 0,
size: 20,
});
for (const call of calls.results) {
console.log(`${call.type} ${call.from_number} → ${call.to_number} (${call.length_in_minutes} min)`);
}
// ─── Créer puis récupérer un contact ──
const contact = await allo.contacts.create({
numbers: ['+33612345678'],
name: 'Jean',
last_name: 'Dupont',
emails: ['jean.dupont@example.com'],
});
const fetched = await allo.contacts.get(contact.id);
console.log(fetched.name, fetched.last_name, fetched.emails);
// ─── Souscrire un endpoint webhook à deux topics ──
const webhook = await allo.webhooks.create({
allo_number: '+1234567890',
url: 'https://example.com/webhooks/allo',
enabled: true,
topics: ['CALL_RECEIVED', 'SMS_RECEIVED'],
});
console.log('Webhook id:', webhook.id);
3
Configuration — AlloClientOptions
import { AlloClient } from '@qrcommunication/withallo-sdk';
const allo = new AlloClient({
// Clé API (obligatoire). Envoyée telle quelle dans l'en-tête Authorization, SANS préfixe Bearer.
apiKey: process.env.ALLO_API_KEY!,
// URL de base de l'API REST Withallo
// Défaut : 'https://api.withallo.com/v1/api'
baseUrl: 'https://api.withallo.com/v1/api',
// Timeout par requête en millisecondes
// Défaut : 30000 (30 secondes)
timeout: 30_000,
// Headers HTTP supplémentaires envoyés avec chaque requête
headers: {
'X-App-Name': 'my-application',
},
// Implémentation fetch personnalisée (tests / proxies / polyfill Node < 18)
fetch: globalThis.fetch,
});
Interface AlloClientOptions
| Propriété | Type | Défaut | Description |
|---|---|---|---|
apiKey |
string |
— | Obligatoire. Clé API brute (sans préfixe Bearer). |
baseUrl |
string? |
'https://api.withallo.com/v1/api' |
URL de base de l'API Withallo |
timeout |
number? |
30000 |
Timeout en millisecondes par requête (AbortController) |
headers |
Record<string, string>? |
{} |
Headers HTTP supplémentaires ajoutés à chaque requête |
fetch |
typeof fetch? |
globalThis.fetch |
Implémentation fetch personnalisée (tests, proxies, polyfills) |
WEBHOOKS_READ_WRITE, CONVERSATIONS_READ, CONTACTS_READ, CONTACTS_READ_WRITE, SMS_SEND. Un scope insuffisant retourne HTTP 403 avec { code: 'API_KEY_INSUFFICIENT_SCOPE' }.
4 API Reference
allo.webhooks
WEBHOOKS_READ_WRITE
Gérer les souscriptions webhook. Withallo pousse des événements (CALL_RECEIVED, SMS_RECEIVED, CONTACT_CREATED, CONTACT_UPDATED) vers l'URL HTTPS que vous enregistrez.
Méthodes
| Méthode | HTTP | Description |
|---|---|---|
list(): Promise<Webhook[]> |
GET /webhooks |
Liste toutes les configurations webhook du compte. |
create(input): Promise<Webhook> |
POST /webhooks |
Souscrire une URL à un ou plusieurs topics pour un numéro Allo. |
delete(webhookId): Promise<void> |
DELETE /webhooks/{id} |
Supprime une configuration webhook. |
Exemple
// Souscrire un endpoint aux appels & SMS entrants
const webhook = await allo.webhooks.create({
allo_number: '+1234567890',
url: 'https://example.com/webhooks/allo',
enabled: true,
topics: ['CALL_RECEIVED', 'SMS_RECEIVED'],
});
// Lister toutes les configurations
const all = await allo.webhooks.list();
console.log(`${all.length} webhooks configured`);
// Supprimer quand obsolète
await allo.webhooks.delete(webhook.id);
allo_number (snake_case) sur GET et alloNumber (camelCase) sur POST. Le SDK normalise les deux vers un unique allo_number .
allo.calls
CONVERSATIONS_READ
Rechercher l'historique des appels. Pagination 0-indexée, plafond de 100 par page.
Signature
search(input: {
allo_number: string; // Obligatoire — votre numéro Allo (E.164)
contact_number?: string; // Optionnel — limiter à un contact
page?: number; // Défaut 0
size?: number; // Défaut 10, max 100
}): Promise<{ results: Call[]; metadata: PaginationMetadata }>
Exemple
const { results, metadata } = await allo.calls.search({
allo_number: '+1234567890',
page: 0,
size: 20,
});
console.log(`Page ${metadata.pagination.current_page + 1}/${metadata.pagination.total_pages}`);
for (const call of results) {
console.log(`${call.type} ${call.from_number} → ${call.to_number}`);
console.log(` ${call.length_in_minutes} min — ${call.start_date}`);
if (call.recording_url) console.log(` Recording: ${call.recording_url}`);
// Transcript disponible après la fin de l'appel
for (const line of call.transcript ?? []) {
console.log(` [${line.source}] ${line.text}`);
}
}
allo.contacts
CONTACTS_READ / CONTACTS_READ_WRITE
CRUD sur les contacts et historique de conversation (appels + SMS fusionnés).
Méthodes
| Méthode | HTTP | Scope |
|---|---|---|
search({ page?, size? }) |
GET /contacts |
CONTACTS_READ |
get(contactId) |
GET /contact/{id} |
CONTACTS_READ |
create(input) |
POST /contacts |
CONTACTS_READ_WRITE |
update(contactId, input) |
PUT /contacts/{id} |
CONTACTS_READ_WRITE |
conversation(contactId, { page?, size? }) |
GET /contact/{id}/conversation |
CONVERSATIONS_READ |
/contacts pour la recherche et la création, mais le singulier /contact/{id} pour GET et conversation. Le SDK masque cette différence.
Exemple — Création & mise à jour
// Création — tableau numbers obligatoire (au moins un E.164)
const contact = await allo.contacts.create({
numbers: ['+33612345678'],
name: 'Jean',
last_name: 'Dupont',
job_title: 'CTO',
emails: ['jean.dupont@example.com'],
});
// Mise à jour — emails / numbers REMPLACENT les tableaux existants
const updated = await allo.contacts.update(contact.id, {
job_title: 'CEO',
emails: ['jean@example.com', 'contact@example.com'],
});
// Récupérer la conversation (appels + SMS fusionnés chronologiquement)
const { results } = await allo.contacts.conversation(contact.id, { page: 0, size: 20 });
for (const entry of results) {
if (entry.type === 'CALL') {
console.log(`CALL ${entry.call?.start_date} — ${entry.call?.length_in_minutes} min`);
} else {
console.log(`SMS ${entry.message?.start_date} — ${entry.message?.content}`);
}
}
allo.sms
SMS_SEND
Envoyer des SMS sortants. Deux variantes coexistent car le payload diffère selon la destination.
Méthodes
| Méthode | Payload | Cas d'usage |
|---|---|---|
send({ from, to, message }) |
{ from, to, message } |
US / international. from doit être l'un de vos numéros Allo. |
sendFrance({ sender_id, to, message }) |
{ sender_id, to, message } |
France. Exige un sender alphanumérique pré-vérifié (3–11 caractères) ou un short code — décision ARCEP (janvier 2023) bloquant les SMS professionnels depuis un numéro mobile standard. |
Exemple
// SMS standard
const sms = await allo.sms.send({
from: '+1234567890',
to: '+0987654321',
message: 'Hello from Allo',
});
console.log(sms.content, sms.start_date);
// France — sender_id doit être pré-validé par le support Allo
try {
await allo.sms.sendFrance({
sender_id: 'MyCompany',
to: '+33612345678',
message: 'Bonjour depuis Allo',
});
} catch (error) {
if (error instanceof Error && error.message.includes('INVALID_SENDER_ID')) {
console.error('Sender non vérifié — contacter le support Allo.');
}
throw error;
}
allo.numbers
CONVERSATIONS_READ
Lister les numéros Allo connectés à votre compte.
Signature
list(): Promise<PhoneNumber[]>
Exemple
const numbers = await allo.numbers.list();
for (const n of numbers) {
const shared = n.is_shared_number ? ' (shared)' : '';
console.log(`${n.country} ${n.name}${shared} — ${n.number}`);
}
5 Hooks React
Un point d'entrée React optionnel expose des hooks typés construits sur le client principal. Tree-shaké des bundles non-React.
import { AlloProvider, useContacts, useNumbers, useSendSms } from '@qrcommunication/withallo-sdk/react';
// Envelopper votre arbre avec le provider (une seule fois)
<AlloProvider apiKey={process.env.NEXT_PUBLIC_ALLO_API_KEY!}>
<ContactList />
</AlloProvider>
function ContactList() {
const { data: contacts, isLoading } = useContacts({ page: 0, size: 20 });
const { data: numbers } = useNumbers();
const { mutate: send, isPending } = useSendSms();
if (isLoading) return <p>{ 'Chargement…' }</p>;
return (
<ul>
{contacts?.results.map((c) => (
<li key={c.id}>
{c.name} {c.last_name} — {c.numbers[0]}
<button
disabled={isPending}
onClick={() =>
send({ from: numbers![0].number, to: c.numbers[0], message: 'Hi!' })
}
>
{ 'Envoyer SMS' }
</button>
</li>
))}
</ul>
);
}
| Hook | Basé sur | Retourne |
|---|---|---|
useWebhooks() | allo.webhooks.list() | { data, isLoading, error, refetch } |
useCalls(input) | allo.calls.search() | { data, isLoading, error, refetch } |
useContacts(input) | allo.contacts.search() | { data, isLoading, error, refetch } |
useContact(id) | allo.contacts.get() | { data, isLoading, error } |
useConversation(id, input) | allo.contacts.conversation() | { data, isLoading, error } |
useSendSms() | allo.sms.send() | { mutate, isPending, error } |
useNumbers() | allo.numbers.list() | { data, isLoading, error } |
@qrcommunication/withallo-sdk/react (export subpath). Le client principal reste sous @qrcommunication/withallo-sdk.
6 Types TypeScript
Chaque réponse est entièrement typée. Voici les principaux types publics réexportés depuis la racine du package.
export type E164Number = string; // ex. +33612345678
export type WebhookTopic =
| 'CALL_RECEIVED'
| 'SMS_RECEIVED'
| 'CONTACT_CREATED'
| 'CONTACT_UPDATED';
export interface Webhook {
id: string;
allo_number: E164Number;
enabled: boolean;
url: string;
topics: WebhookTopic[];
}
export interface Call {
id: string;
from_number: E164Number;
to_number: E164Number;
length_in_minutes: number;
type: 'INBOUND' | 'OUTBOUND';
result?: 'ANSWERED' | 'VOICEMAIL' | 'TRANSFERRED_AI' | 'TRANSFERRED_EXTERNAL' | 'BLOCKED' | 'FAILED';
summary: string | null;
tag: string | null;
recording_url: string | null;
start_date: string;
transcript: Array<{
source: 'AGENT' | 'EXTERNAL' | 'USER';
text: string;
time: string;
start_seconds: number;
end_seconds: number;
}>;
}
export interface Contact {
id: string;
name: string | null;
last_name: string | null;
job_title: string | null;
website: string | null;
status: string | null;
is_archived: boolean;
created_at: string;
updated_at: string;
numbers: E164Number[];
emails: string[];
company: { id: string; name: string } | null;
engagement: string | null;
last_activity_date: string | null;
}
export interface ConversationEntry {
type: 'CALL' | 'TEXT_MESSAGE';
call: Call | null;
message: {
from_number: E164Number;
to_number: E164Number;
type: 'INBOUND' | 'OUTBOUND';
content: string;
start_date: string;
} | null;
}
export interface SentSms {
from_number: E164Number | null;
sender_id: string | null;
to_number: E164Number;
type: 'OUTBOUND';
content: string;
start_date: string;
}
export interface PhoneNumber {
number: E164Number;
name: string;
country: string; // ISO 3166-1 alpha-2 (ex. FR, US)
is_shared_number: boolean;
}
// Enveloppe webhook entrante (envoyée PAR Withallo VERS votre endpoint HTTPS)
export interface WebhookEnvelope<T = unknown> {
topic: WebhookTopic;
data: T;
}
7 Sécurité des Webhooks
Durcissement recommandé
- Endpoint HTTPS uniquement (pas de fallback HTTP).
- URL non devinable (token aléatoire 128 bits dans le chemin).
- Valider que
allo_numberappartient bien à votre compte avant traitement. - Rate-limiter l'endpoint (ex. 100 req/min par IP).
- Si Withallo publie leurs IPs de sortie, les whitelister au firewall.
Receiver d'exemple (Express)
import express from 'express';
import type { WebhookEnvelope, CallReceivedPayload } from '@qrcommunication/withallo-sdk';
const router = express.Router();
const MY_ALLO_NUMBERS = new Set(['+1234567890']);
// POST /webhooks/allo/{unguessable-token}
router.post('/webhooks/allo/:token', async (req, res) => {
if (req.params.token !== process.env.ALLO_WEBHOOK_TOKEN) {
return res.status(404).end();
}
const envelope = req.body as WebhookEnvelope;
switch (envelope.topic) {
case 'CALL_RECEIVED': {
const payload = envelope.data as CallReceivedPayload;
if (!MY_ALLO_NUMBERS.has(payload.to)) return res.status(403).end();
// Persister, notifier, enqueuer l'analyse du transcript…
break;
}
case 'SMS_RECEIVED': /* … */ break;
case 'CONTACT_CREATED':
case 'CONTACT_UPDATED': /* … */ break;
}
res.status(200).end();
});
export default router;
8 Gestion des erreurs
Chaque méthode du SDK lance une AlloApiError sur les réponses 4xx/5xx. L'erreur expose le statut HTTP, le code machine et les détails de validation.
import { AlloClient, AlloApiError } from '@qrcommunication/withallo-sdk';
const allo = new AlloClient({ apiKey: process.env.ALLO_API_KEY! });
try {
await allo.sms.sendFrance({ sender_id: 'Unverified', to: '+33612345678', message: 'Hi' });
} catch (error) {
if (error instanceof AlloApiError) {
console.error('HTTP', error.status); // 400
console.error('code', error.code); // 'INVALID_SENDER_ID'
console.error('details', error.details); // [{ message, field }]
switch (error.code) {
case 'API_KEY_INVALID': /* 401 */ break;
case 'API_KEY_INSUFFICIENT_SCOPE': /* 403 */ break;
case 'RATE_LIMITED': /* 429 */ break;
case 'INVALID_SENDER_ID': /* 400 */ break;
}
}
throw error;
}
Codes d'erreur courants
| HTTP | code |
Signification |
|---|---|---|
| 401 | API_KEY_INVALID | Clé API absente ou invalide |
| 403 | API_KEY_INSUFFICIENT_SCOPE | La clé n'a pas le scope requis (voir details[].message) |
| 404 | WEBHOOK_NOT_FOUND, CONTACT_NOT_FOUND | Ressource inexistante |
| 400 | INVALID_SENDER_ID | sender_id France non vérifié |
| 429 | RATE_LIMITED | Attendre et réessayer après Retry-After (valeur de l'en-tête) |
9 Exemples avancés
Parcourir tous les contacts
import { AlloClient, type Contact } from '@qrcommunication/withallo-sdk';
const allo = new AlloClient({ apiKey: process.env.ALLO_API_KEY! });
async function fetchAllContacts(): Promise<Contact[]> {
const size = 100;
let page = 0;
const all: Contact[] = [];
while (true) {
const { results, metadata } = await allo.contacts.search({ page, size });
all.push(...results);
if (page + 1 >= metadata.pagination.total_pages) break;
page++;
}
return all;
}
const contacts = await fetchAllContacts();
console.log(`${contacts.length} contacts`);
Retry avec back-off exponentiel sur rate limiting
import { AlloApiError } from '@qrcommunication/withallo-sdk';
async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 5): Promise<T> {
let attempt = 0;
while (true) {
try {
return await fn();
} catch (error) {
if (error instanceof AlloApiError && error.code === 'RATE_LIMITED' && attempt < maxAttempts) {
const wait = Math.min(1000 * 2 ** attempt, 30_000);
await new Promise((r) => setTimeout(r, wait));
attempt++;
continue;
}
throw error;
}
}
}
await withRetry(() => allo.sms.send({ from, to, message }));
Utilisation dans un Route Handler Next.js 16
import 'server-only';
import { NextRequest, NextResponse } from 'next/server';
import { AlloClient, AlloApiError } from '@qrcommunication/withallo-sdk';
import { z } from 'zod';
const allo = new AlloClient({ apiKey: process.env.ALLO_API_KEY! });
const Body = z.object({
from: z.string().regex(/^\+[1-9]\d{1,14}$/),
to: z.string().regex(/^\+[1-9]\d{1,14}$/),
message: z.string().min(1).max(1000),
});
export async function POST(request: NextRequest) {
const parsed = Body.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json({ error: 'VALIDATION' }, { status: 422 });
}
try {
const sms = await allo.sms.send(parsed.data);
return NextResponse.json({ sms });
} catch (error) {
if (error instanceof AlloApiError) {
return NextResponse.json({ error: error.code }, { status: error.status });
}
return NextResponse.json({ error: 'INTERNAL' }, { status: 500 });
}
}
Utilisation avec Deno
import { AlloClient } from 'npm:@qrcommunication/withallo-sdk';
const allo = new AlloClient({ apiKey: Deno.env.get('ALLO_API_KEY')! });
const numbers = await allo.numbers.list();
console.log(numbers);