"> 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
Zero runtime dependencies. The SDK uses only the native fetch API (available natively since Node.js 18+). No external dependency is bundled.
API key required. Generate your API key on web.withallo.com/settings/api. Each key carries scopes (WEBHOOKS_READ_WRITE, CONTACTS_READ, CONVERSATIONS_READ, SMS_SEND, …) that gate individual endpoints.

Prerequisites

Environment Minimum version
Node.js 18.0.0+
TypeScript 5.0+ (recommended 5.7+)
React (optional — hooks) 18.0+ / 19.x
Deno 1.30+ (via npm specifier)
Bun 1.0+

Package Metadata

npm @qrcommunication/withallo-sdk
Version 0.2.0
Source github.com/QrCommunication/sdk-withallo-js
License MIT
Base URL https://api.withallo.com/v1/api

2 Quick Start

TypeScript main.ts
import { AlloClient } from '@qrcommunication/withallo-sdk';

// Initialise the client (API key is sent verbatim, NO Bearer prefix)
const allo = new AlloClient({
  apiKey: process.env.ALLO_API_KEY!,
  timeout: 30_000,
});

// ─── List phone numbers connected to your account ──
const numbers = await allo.numbers.list();
for (const n of numbers) {
  console.log(`${n.name} — ${n.number} (${n.country})`);
}

// ─── Send a transactional SMS (US / international) ──
const sms = await allo.sms.send({
  from: '+1234567890',
  to: '+0987654321',
  message: 'Hello from Allo',
});
console.log('Sent:', sms.start_date);

// ─── Send an SMS from France (pre-verified sender_id required) ──
await allo.sms.sendFrance({
  sender_id: 'MyCompany',
  to: '+33612345678',
  message: 'Bonjour depuis Allo',
});

// ─── Search call history for a given Allo number ──
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)`);
}

// ─── Create and fetch a 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);

// ─── Subscribe a webhook endpoint to two 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({
  // API key (mandatory). Sent verbatim in the Authorization header, NO Bearer prefix.
  apiKey: process.env.ALLO_API_KEY!,

  // Base URL of the Withallo REST API
  // Default: 'https://api.withallo.com/v1/api'
  baseUrl: 'https://api.withallo.com/v1/api',

  // Timeout per request in milliseconds
  // Default: 30000 (30 seconds)
  timeout: 30_000,

  // Additional HTTP headers sent with every request
  headers: {
    'X-App-Name': 'my-application',
  },

  // Custom fetch implementation (for testing / proxies / Node < 18 polyfill)
  fetch: globalThis.fetch,
});

Interface AlloClientOptions

Property 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 number? 30000 Timeout in milliseconds per request (AbortController)
headers Record<string, string>? {} Additional HTTP headers added to every request
fetch typeof fetch? globalThis.fetch Custom fetch implementation (testing, proxies, polyfills)
Scopes. Each endpoint requires a specific scope on the API key: WEBHOOKS_READ_WRITE, CONVERSATIONS_READ, CONTACTS_READ, CONTACTS_READ_WRITE, SMS_SEND. An insufficient scope returns HTTP 403 with { code: 'API_KEY_INSUFFICIENT_SCOPE' }.

4 API Reference

allo.webhooks WEBHOOKS_READ_WRITE

Manage webhook subscriptions. Withallo pushes events (CALL_RECEIVED, SMS_RECEIVED, CONTACT_CREATED, CONTACT_UPDATED) to the HTTPS URL you register.

Methods

Method HTTP Description
list(): Promise<Webhook[]> GET /webhooks List every webhook configuration on the account.
create(input): Promise<Webhook> POST /webhooks Subscribe a URL to one or more topics for an Allo number.
delete(webhookId): Promise<void> DELETE /webhooks/{id} Delete a webhook configuration.

Example

TypeScript
// Subscribe an endpoint to incoming calls & SMS
const webhook = await allo.webhooks.create({
  allo_number: '+1234567890',
  url: 'https://example.com/webhooks/allo',
  enabled: true,
  topics: ['CALL_RECEIVED', 'SMS_RECEIVED'],
});

// List all configurations
const all = await allo.webhooks.list();
console.log(`${all.length} webhooks configured`);

// Delete when no longer needed
await allo.webhooks.delete(webhook.id);
Field casing caveat. The REST endpoint returns allo_number (snake_case) on GET and alloNumber (camelCase) on POST. The SDK normalises both into a single allo_number property.

allo.calls CONVERSATIONS_READ

Search call history. Pagination is 0-indexed and capped at 100 per page.

Signature

search(input: {
  allo_number: string;       // Required — your Allo number (E.164)
  contact_number?: string;   // Optional — restrict to one contact
  page?: number;             // Default 0
  size?: number;             // Default 10, max 100
}): Promise<{ results: Call[]; metadata: PaginationMetadata }>

Example

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 available after the call ends
  for (const line of call.transcript ?? []) {
    console.log(`    [${line.source}] ${line.text}`);
  }
}

allo.contacts CONTACTS_READ / CONTACTS_READ_WRITE

CRUD operations over contacts plus conversation history (calls + SMS mixed).

Methods

Method 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
Path quirk. Withallo uses the plural /contacts for search and create, but the singular /contact/{id} for GET and conversation. The SDK abstracts this away.

Example — Create & update

TypeScript
// Create — numbers array is required (at least one E.164 number)
const contact = await allo.contacts.create({
  numbers: ['+33612345678'],
  name: 'Jean',
  last_name: 'Dupont',
  job_title: 'CTO',
  emails: ['jean.dupont@example.com'],
});

// Update — emails / numbers REPLACE the existing arrays
const updated = await allo.contacts.update(contact.id, {
  job_title: 'CEO',
  emails: ['jean@example.com', 'contact@example.com'],
});

// Fetch conversation (calls + SMS merged chronologically)
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

Send outbound SMS. Two flavors are exposed because the payload shape differs by destination.

Methods

Method Payload Use case
send({ from, to, message }) { from, to, message } US / international. from must be one of your Allo numbers.
sendFrance({ sender_id, to, message }) { sender_id, to, message } France. Requires a pre-verified alphanumeric sender (3–11 chars) or short code — ARCEP ruling (Jan 2023) blocks business SMS from standard mobile numbers.

Example

TypeScript
// Standard SMS
const sms = await allo.sms.send({
  from: '+1234567890',
  to: '+0987654321',
  message: 'Hello from Allo',
});
console.log(sms.content, sms.start_date);

// France — sender_id must be pre-approved by Allo support
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 not verified — contact Allo support.');
  }
  throw error;
}

allo.numbers CONVERSATIONS_READ

List the Allo phone numbers connected to your account.

Signature

list(): Promise<PhoneNumber[]>

Example

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 React Hooks

An optional React entrypoint exposes typed hooks built on top of the core client. Tree-shaken out of non-React bundles.

TypeScript ContactList.tsx
import { AlloProvider, useContacts, useNumbers, useSendSms } from '@qrcommunication/withallo-sdk/react';

// Wrap your tree with the provider (once)
<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>{ 'Loading…' }</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!' })
            }
          >
            { 'Send SMS' }
          </button>
        </li>
      ))}
    </ul>
  );
}
Hook Wraps Returns
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 }
Import path. React hooks ship from @qrcommunication/withallo-sdk/react (subpath export). The core client is unchanged under @qrcommunication/withallo-sdk.

6 TypeScript Types

Every response is fully typed. Below are the main public types re-exported from the package root.

TypeScript
export type E164Number = string; // e.g. +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 (e.g. FR, US)
  is_shared_number: boolean;
}

// Incoming webhook envelope (sent BY Withallo TO your HTTPS endpoint)
export interface WebhookEnvelope<T = unknown> {
  topic: WebhookTopic;
  data: T;
}

7 Webhook Security

No HMAC signature. As of April 2026, Withallo does not publish a signing secret or signature header for incoming webhooks. You must harden your receiver yourself.

Recommended hardening

  1. HTTPS-only endpoint (no HTTP fallback).
  2. Unguessable URL (128-bit random token in the path).
  3. Validate that allo_number belongs to your account before processing.
  4. Rate-limit the endpoint (e.g. 100 req/min per IP).
  5. If Withallo publishes their egress IPs, whitelist them at the firewall.

Example receiver (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();
      // Persist, notify, enqueue transcript analysis…
      break;
    }
    case 'SMS_RECEIVED': /* … */ break;
    case 'CONTACT_CREATED':
    case 'CONTACT_UPDATED': /* … */ break;
  }

  res.status(200).end();
});

export default router;

8 Error Handling

Every SDK method throws an AlloApiError on 4xx/5xx responses. The error exposes the HTTP status, the machine-readable code and the validation details.

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;
}

Common error codes

HTTP code Meaning
401API_KEY_INVALIDMissing or invalid API key
403API_KEY_INSUFFICIENT_SCOPEKey lacks the required scope (see details[].message)
404WEBHOOK_NOT_FOUND, CONTACT_NOT_FOUNDResource does not exist
400INVALID_SENDER_IDFrench sender_id not verified
429RATE_LIMITEDBack off and retry after the Retry-After header value

9 Advanced Examples

Paginate through every contact

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 with exponential back-off on 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 }));

Usage in Next.js 16 Route Handler

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 });
  }
}

Usage with 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);