Skip to main content

SDK quick-start

@bubbl3/sdk is a typed TypeScript client for the bubbl3 API, generated from the same OpenAPI document as the API reference, plus webhook verification. It runs on Node 20 or later.

Send a message​

This is the SDK's own example. It's the exact file bubbl3's test suite runs against a local API on every change, so it always matches the SDK.

packages/sdk/examples/send-message.ts
/**
* Sends one message and prints its status.
*
* BUBBL3_API_KEY=b3_test_... BUBBL3_TO=+13475550142 pnpm --filter @bubbl3/sdk example
*
* Use a test key: its messages go to the device simulator, never to a real phone. `BUBBL3_API_URL`
* defaults to the local API.
*/
import { Bubbl3, Bubbl3ApiError } from '@bubbl3/sdk';

const apiKey = process.env.BUBBL3_API_KEY;
const to = process.env.BUBBL3_TO;
if (!apiKey || !to) {
console.error('Set BUBBL3_API_KEY and BUBBL3_TO');
process.exit(2);
}

const client = new Bubbl3({
apiKey,
baseUrl: process.env.BUBBL3_API_URL ?? 'http://localhost:4000',
});

try {
const lines = await client.lines.list();
const message = await client.messages.create(
{ to, body: 'Hello from the bubbl3 SDK example.' },
// The same key on a retry returns this message instead of sending another.
{ idempotencyKey: process.env.BUBBL3_IDEMPOTENCY_KEY ?? `example-${Date.now()}` },
);
const latest = await client.messages.get(message.id);
const events = await client.messages.listEvents(message.id);
console.log(
JSON.stringify({
lines: lines.data.length,
message_id: message.id,
is_test: latest.is_test,
status: latest.status,
events: events.data.map((e) => e.type),
}),
);
} catch (error) {
if (error instanceof Bubbl3ApiError) {
console.error(`${error.status} ${error.code}: ${error.message} (request ${error.requestId})`);
process.exit(1);
}
throw error;
}

Run it with a test key and a test number, so the message only reaches the device simulator:

BUBBL3_API_KEY=b3_test_... BUBBL3_TO=+13475550142 BUBBL3_API_URL=https://api.bubbl3.com \
npx tsx send-message.ts

It prints the number of lines, the message id, is_test, its status and its delivery events.

The client​

import { Bubbl3 } from '@bubbl3/sdk';

const client = new Bubbl3({ apiKey: process.env.BUBBL3_API_KEY! });
  • baseUrl defaults to https://api.bubbl3.com.
  • Every public call has a typed method, grouped by section: client.messages.create, client.contacts.list, client.lines.list, client.webhooks.create and so on. Inputs and results are typed from the OpenAPI document. Path ids come first, then the input.
  • Writes take { idempotencyKey } as their last argument. Pass your own on every send, named after what you're doing; see idempotency. If you don't, the client makes one per call (autoIdempotencyKeys, on by default), which protects its own retries but not yours.
  • Reads and writes with an idempotency key are retried twice after a network error, a 429 or a 5xx, with a randomised backoff or after the retry-after the API sends. Change it with maxRetries. A retry-after longer than a minute, such as a daily limit, is thrown straight away as a Bubbl3RateLimitError with retryAfterSeconds.
  • Lists return { data, next_cursor }.

Errors​

A failed call throws a Bubbl3ApiError with status, code, message, requestId and details. Subclasses let you catch the common cases:

ClassWhen
Bubbl3ValidationError400 or 422, with the failing fields in details
Bubbl3AuthenticationError401
Bubbl3PermissionError403
Bubbl3NotFoundError404
Bubbl3ConflictError409, including idempotency conflicts
Bubbl3RateLimitError429, with retryAfterSeconds
Bubbl3ServerError5xx
Bubbl3ConnectionErrorNo response: DNS, connection, TLS or a timeout

Webhooks​

verifyWebhook(rawBody, header, secret) checks a delivery's bubbl3-signature. See the webhooks quick-start for a full receiver.