1 Installation
npm install @qrcommunication/withallo-sdk
pnpm add @qrcommunication/withallo-sdk
yarn add @qrcommunication/withallo-sdk
fetch API (available natively since Node.js 18+). No external dependency is bundled.
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
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
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) |
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
// 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);
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
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 |
/contacts for search and create, but the singular /contact/{id} for GET and conversation. The SDK abstracts this away.
Example — Create & update
// 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
// 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
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.
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 } |
@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.
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
Recommended hardening
- HTTPS-only endpoint (no HTTP fallback).
- Unguessable URL (128-bit random token in the path).
- Validate that
allo_numberbelongs to your account before processing. - Rate-limit the endpoint (e.g. 100 req/min per IP).
- If Withallo publishes their egress IPs, whitelist them at the firewall.
Example receiver (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();
// 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.
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 |
|---|---|---|
| 401 | API_KEY_INVALID | Missing or invalid API key |
| 403 | API_KEY_INSUFFICIENT_SCOPE | Key lacks the required scope (see details[].message) |
| 404 | WEBHOOK_NOT_FOUND, CONTACT_NOT_FOUND | Resource does not exist |
| 400 | INVALID_SENDER_ID | French sender_id not verified |
| 429 | RATE_LIMITED | Back off and retry after the Retry-After header value |
9 Advanced Examples
Paginate through every contact
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
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
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
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);