Skip to content

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

Terminal
npm install @blueticked/sdk

Zero 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:

Terminal
npx openapi-typescript https://app.blueticked.com/api/v1/openapi.json   -o ./src/blueticked.d.ts

Quickstart

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.

send.ts
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 POST to a route that deduplicates on Idempotency-Key gets one generated automatically, so a retry can never double-send. Every other POST is not retried — see below. Pass your own idempotencyKey to any method to control it yourself; an explicit key is always sent.
  • Retries with backoff. 429 and 5xx responses are retried with exponential backoff and jitter. Tune with maxRetries (default 2; set 0 to disable) — see Rate limits and retries.
  • Request timeouts. Each request is bounded by timeoutMs (default 30000); a timed-out request rejects rather than hanging.
  • Typed errors. Any non-2xx response throws a BluetickedError exposing code, status, message, details, nextSteps, and requestId — see Error codes.
client.ts
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:

Routes that deduplicate on Idempotency-Key
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-failed

No 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.

retry-unsafe.ts
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:

catch.ts
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.

test-mode.ts
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.

app/api/blueticked/route.ts (Next.js)
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.