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.
/**
* 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! });
baseUrldefaults tohttps://api.bubbl3.com.- Every public call has a typed method, grouped by section:
client.messages.create,client.contacts.list,client.lines.list,client.webhooks.createand 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
429or a5xx, with a randomised backoff or after theretry-afterthe API sends. Change it withmaxRetries. Aretry-afterlonger than a minute, such as a daily limit, is thrown straight away as aBubbl3RateLimitErrorwithretryAfterSeconds. - 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:
| Class | When |
|---|---|
Bubbl3ValidationError | 400 or 422, with the failing fields in details |
Bubbl3AuthenticationError | 401 |
Bubbl3PermissionError | 403 |
Bubbl3NotFoundError | 404 |
Bubbl3ConflictError | 409, including idempotency conflicts |
Bubbl3RateLimitError | 429, with retryAfterSeconds |
Bubbl3ServerError | 5xx |
Bubbl3ConnectionError | No 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.