TypeScript SDK
@blueticked/sdk is the official client for the Blueticked API. It has zero dependencies and runs on Node 18+, serverless functions, and edge runtimes. It wraps /api/v1 with typed methods, automatic idempotency, retries, and a single error type you can catch.
Install
npm install @blueticked/sdkZero dependencies, MIT licensed, ESM and CommonJS, Node 18+. Not using TypeScript or JavaScript? Generate a typed client from our public OpenAPI document instead — it works today in any language and needs no API key to read:
npx openapi-typescript https://app.blueticked.com/api/v1/openapi.json -o ./src/blueticked.d.tsQuickstart
Construct the client with an API key, then send an approved WhatsApp template. Keep the key in an environment variable — never inline it in source.
import { Blueticked } from "@blueticked/sdk";
const bt = new Blueticked({ apiKey: process.env.BLUETICKED_API_KEY! });
const message = await bt.messages.send({
channel: "whatsapp",
to: { phone: "+27821234567" },
template_id: "<an approved template id>",
template_variables: { "1": "Sam" },
});
console.log(message.status); // "queued"What the client handles for you
- Idempotency, on the routes that honour it. A
POSTto a route that deduplicates onIdempotency-Keygets one generated automatically, so a retry can never double-send. Every otherPOSTis not retried — see below. Pass your ownidempotencyKeyto any method to control it yourself; an explicit key is always sent. - Retries with backoff.
429and5xxresponses are retried with exponential backoff and jitter. Tune withmaxRetries(default2; set0to disable) — see Rate limits and retries. - Request timeouts. Each request is bounded by
timeoutMs(default30000); a timed-out request rejects rather than hanging. - Typed errors. Any non-2xx response throws a
BluetickedErrorexposingcode,status,message,details,nextSteps, andrequestId— see Error codes.
const bt = new Blueticked({
apiKey: process.env.BLUETICKED_API_KEY!,
maxRetries: 4, // default 2; set 0 to handle retries yourself
timeoutMs: 15000, // default 30000
});Which POSTs are retried
A retry is only safe when the server deduplicates it. The client used to stamp an Idempotency-Key on every POST and retry every 5xx — on a route that ignores the header, a 502-502-200 sequence therefore created the resource three times. So the SDK now retries a POST only on the routes that genuinely deduplicate:
POST /v1/campaigns
POST /v1/contacts
POST /v1/events/send
POST /v1/flows
POST /v1/flows/{id}/publish
POST /v1/groups
POST /v1/integrations
POST /v1/messages
POST /v1/studio-flows
POST /v1/tables/{id}/rows
POST /v1/templates
POST /v1/templates/{id}/submit
POST /v1/webhook-deliveries/{id}/replay
POST /v1/webhooks
POST /v1/webhooks/{id}/replay-failedNo customer message can be duplicated — messages, campaigns and events/send are all on that list. Any other POST that fails with a 5xx or a dropped connection throws immediately with code: "retry_unsafe" instead of being retried: the first attempt may well have succeeded before the error reached you, so a retry would create a second product, discount or payment link — the kind of failure you find in your own data a week later rather than in an error.
try {
await bt.request("POST", "/v1/commerce/products", product);
} catch (err) {
if (err instanceof BluetickedError && err.code === "retry_unsafe") {
// The write may have landed. Check before sending it again —
// do not blindly retry.
}
}A 429 is always retried regardless: it means the request was rejected before any work happened, so there is nothing to duplicate.
Handling errors
Catch BluetickedError to branch on a stable code instead of parsing messages:
import { Blueticked, BluetickedError } from "@blueticked/sdk";
try {
await bt.messages.send({
channel: "whatsapp",
to: { phone: "+27821234567" },
template_id: "<template id>",
});
} catch (err) {
if (err instanceof BluetickedError) {
console.error(err.code); // e.g. "validation_failed"
console.error(err.status); // e.g. 400
console.error(err.message); // human-readable
console.error(err.details); // structured context on some codes
console.error(err.requestId); // include when contacting support
// The one branch an unattended integration must have: a person has
// to act, so no rewrite of this request will ever succeed.
if (err.needsHuman) alertYourOperator(err);
} else {
throw err;
}
}err.needsHuman is true when any next_steps entry has by: "human" — an empty wallet, a spend cap, a disconnected channel. Retrying those burns your budget on a request that cannot succeed until somebody opens a browser. err.isTransient is the opposite signal: back off and try again.
Test mode
A key prefixed blu_test_ puts the client in sandbox mode — bt.isTestMode is true, and sends never reach WhatsApp, never bill, and are flagged as test data. Use the magic numbers to exercise delivery, failure, and inbound-reply paths deterministically.
const bt = new Blueticked({ apiKey: "blu_test_..." });
if (bt.isTestMode) {
// send to +27800000002 to force a message.failed, etc.
}Verifying webhooks
Verify signed deliveries with constructEvent. Always pass the raw request body and the whole header set (req.headers): it checks the timestamped x-blueticked-signature-v2 and refuses one more than 5 minutes old. It throws on a bad signature, so return 401 in your catch. This matches the Webhooks and signing guide.
export async function POST(req: Request) {
const rawBody = await req.text(); // do not re-serialize
try {
const event = await bt.webhooks.constructEvent(
rawBody,
req.headers,
process.env.BLUETICKED_WEBHOOK_SECRET!,
);
if (event.event === "message.delivered") {
// update your order, mark the statement sent, etc.
}
return new Response("ok");
} catch {
return new Response("bad signature", { status: 401 });
}
}Resources
The client exposes a typed resource per API area: messages, contacts, campaigns, templates, flows, conversations, channels, email, suppression, groups, wallet, events, integrations, and webhooks. Each follows the same shape (list, get, create, and so on) and is fully typed.
The SDK surface is expanding — for the complete, up-to-date method reference and every field, see the package README on npm. Prefer raw HTTP? Every method maps to a documented endpoint in the OpenAPI spec.