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

Withallo TypeScript SDK

@qrcommunication/withallo-sdk

Node.js 18+, zero runtime dependencies

1 Installation

npm
npm install @qrcommunication/withallo-sdk
pnpm
pnpm add @qrcommunication/withallo-sdk
yarn
yarn add @qrcommunication/withallo-sdk
Zéro dépendance runtime. Le SDK utilise uniquement l'API native fetch (disponible nativement depuis Node.js 18+). Aucune dépendance externe n'est embarquée.
Clé API requise. Générez votre clé API sur web.withallo.com/settings/api. Chaque clé porte des scopes (WEBHOOKS_READ_WRITE, CONTACTS_READ, CONVERSATIONS_READ, SMS_SEND, …) qui régissent les endpoints accessibles.

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

TypeScript main.ts
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

TypeScript
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)
Scopes. Chaque endpoint exige un scope spécifique sur la clé API : 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

TypeScript
// 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);
Casse des champs. L'endpoint REST retourne 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

TypeScript
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
Subtilité de routage. Withallo utilise le pluriel /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

TypeScript
// 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

TypeScript
// 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

TypeScript
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.

TypeScript ContactList.tsx
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 }
Chemin d'import. Les hooks React sont exportés depuis @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.

TypeScript
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

Pas de signature HMAC. À date (avril 2026), Withallo ne publie ni secret de signature, ni header de signature pour les webhooks entrants. Vous devez durcir votre receiver vous-même.

Durcissement recommandé

  1. Endpoint HTTPS uniquement (pas de fallback HTTP).
  2. URL non devinable (token aléatoire 128 bits dans le chemin).
  3. Valider que allo_number appartient bien à votre compte avant traitement.
  4. Rate-limiter l'endpoint (ex. 100 req/min par IP).
  5. Si Withallo publie leurs IPs de sortie, les whitelister au firewall.

Receiver d'exemple (Express)

TypeScript routes/allo-webhook.ts
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.

TypeScript
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
401API_KEY_INVALIDClé API absente ou invalide
403API_KEY_INSUFFICIENT_SCOPELa clé n'a pas le scope requis (voir details[].message)
404WEBHOOK_NOT_FOUND, CONTACT_NOT_FOUNDRessource inexistante
400INVALID_SENDER_IDsender_id France non vérifié
429RATE_LIMITEDAttendre et réessayer après Retry-After (valeur de l'en-tête)

9 Exemples avancés

Parcourir tous les contacts

TypeScript
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

TypeScript
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

TypeScript app/api/allo/send/route.ts
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

TypeScript main.ts (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);