BluetickedDeveloper docs

TypeScript SDK

A typed client generated from the same OpenAPI document this reference is built from.

Available on npm: version 0.8.0

npm install @blueticked/sdk installs 0.8.0: typed, zero dependencies, ESM and CommonJS, server-side only.

Send your first message

The client, a contact, a send and a paginated read, with the test key from the quickstart: nothing is delivered and nothing is charged.

Any other language: generate a client from the OpenAPI document below, or call the REST API directly.

Usage
import { Blueticked } from "@blueticked/sdk";

const apiKey = process.env.BLUETICKED_API_KEY;
if (!apiKey) throw new Error("Set BLUETICKED_API_KEY to your test key");

// baseUrl is the client's default, shown so you know where requests go.
const bt = new Blueticked({
  apiKey,
  baseUrl: "https://app.blueticked.com/api",
});

// to.create makes the recipient a contact in the same call if they are not one yet.

// 202 means QUEUED. Delivery arrives on a webhook, or poll bt.messages.get(id).
const { message_id, status } = await bt.messages.send({
  channel: "whatsapp",
  to: { phone: "+27800000001", create: true, first_name: "Sam" },
  body: "Hello from the Blueticked sandbox.",
});
console.log(message_id, status); // "763c4dc3-d46b-4c10-8bd4-3f233a933ea0" "queued"

// Cursor pagination, walked for you.
for await (const contact of bt.contacts.listAll({ limit: 100 })) {
  console.log(contact.phone); // "+27800000001"
}

Requests go to https://app.blueticked.com/api/v1/…. The client adds /v1 itself, so a baseUrl you pass ends at /api.

When a call is refused

Every refusal throws a BluetickedError.

Branch on its code, never on the message.

Errors
import { Blueticked, BluetickedError } from "@blueticked/sdk";

const apiKey = process.env.BLUETICKED_API_KEY;
if (!apiKey) throw new Error("Set BLUETICKED_API_KEY to your test key");
const bt = new Blueticked({ apiKey });

try {
  // Nobody has created this contact, so the send is refused.
  await bt.messages.send({
    channel: "whatsapp",
    to: { phone: "+27800000099" },
    body: "Hello from the Blueticked sandbox.",
  });
} catch (err) {
  if (!(err instanceof BluetickedError)) throw err;
  console.log(err.status, err.code); // 404 "not_found"
  console.log(err.message);          // names the phone and the fix
  console.log(err.requestId);        // "req_b2e3d610e10f1b07468721ee98f0e1d2": quote it to support
  console.log(err.nextSteps);        // [{ action, by: "caller" | "human" | "waiting" }]
  if (err.needsHuman) {
    // A person has to act (connect a channel, top up); retrying cannot help.
  }
}
code and status
The machine-readable code from the Errors page, and the HTTP status. Status 0 means no answer came back, and the code is the client's own: timeout, network_error, invalid_api_key, browser_context_refused or auto_pagination_limit. retry_unsafe comes either way: a POST that cannot deduplicate failed mid-flight (status 0) or answered a 500, 502, 503 or 504, and it may have gone through, so check before retrying.
requestId
The request's id. Quote it to support and we can find the call.
nextSteps
What to do about it, each step marked by who acts: caller, human or waiting. needsHuman is true when a person must act, so no retry can succeed; isTransient when the remedy is time.
details
Extra context some codes carry, such as what still uses a resource you tried to delete.

What 0.8.0 covers

120 of 127 documented endpoints have a method in 0.8.0.

The 7 below are newer than the package: call them over REST until the next release.

EndpointWhat it does
GET /v1/studio-flowsList Flow Studio flows
GET /v1/studio-flows/{id}Get a Flow Studio flow
POST /v1/studio-flowsCreate a Flow Studio flow
PATCH /v1/studio-flows/{id}Update a Flow Studio flow
POST /v1/studio-flows/{id}/switch-offSwitch a Flow Studio flow off
GET /v1/channels/{id}/whatsapp-spendWhatsApp spend
POST /v1/commerce/orders/{id}/ready-for-collectionMark an order ready for collection
The 120 endpoints with a method
EndpointWhat it doesClient method
GET /v1/meGet workspace contextbt.workspace.getContext()
GET /v1/readinessCheck send readinessbt.workspace.getReadiness()
GET /v1/businesses/{id}/keysList credentials you issued to a customerbt.businesses.listKeys()
DELETE /v1/businesses/{id}/keysRevoke a credential you issued to a customerbt.businesses.revokeKey()
GET /v1/businessesList your customersbt.businesses.list()
POST /v1/businessesOnboard a customerbt.businesses.create()
POST /v1/businesses/{id}/keysIssue a credential for a customerbt.businesses.createKey()
POST /v1/messagesSend one messagebt.messages.send()
GET /v1/messages/{id}Get message statusbt.messages.get()
POST /v1/events/sendSend a flow eventbt.events.send()
POST /v1/contactsCreate or update contactbt.contacts.upsert()
GET /v1/contactsList contactsbt.contacts.list(), bt.contacts.listAll()
GET /v1/contacts/{id}Get a contactbt.contacts.get()
PATCH /v1/contacts/{id}Update a contactbt.contacts.update()
DELETE /v1/contacts/{id}Delete a contactbt.contacts.delete()
POST /v1/contacts/{id}/eraseErase a contact (POPIA)bt.contacts.erase()
POST /v1/documentsUpload a document for a contactbt.documents.upload()
GET /v1/groupsList contact groupsbt.groups.list(), bt.groups.listAll()
POST /v1/groupsCreate contact groupbt.groups.create()
GET /v1/groups/{id}Get a contact groupbt.groups.get()
PATCH /v1/groups/{id}Rename a contact groupbt.groups.update()
DELETE /v1/groups/{id}Delete a contact groupbt.groups.delete()
PUT /v1/groups/{id}/membersAdd contacts to a groupbt.groups.members.add()
DELETE /v1/groups/{id}/membersRemove contacts from a groupbt.groups.members.remove()
GET /v1/segmentsList segmentsbt.segments.list(), bt.segments.listAll()
GET /v1/conversationsList conversationsbt.conversations.list(), bt.conversations.listAll()
PATCH /v1/conversations/{id}Update conversationbt.conversations.update()
GET /v1/conversations/{id}Get a conversationbt.conversations.get()
GET /v1/conversations/{id}/messagesList conversation messagesbt.conversations.messages(), bt.conversations.messagesAll()
GET /v1/conversations/{id}/messages/{messageId}/mediaGet a message's filebt.conversations.messageMedia()
GET /v1/ticketsList ticketsbt.tickets.list(), bt.tickets.listAll()
POST /v1/ticketsRaise a ticketbt.tickets.create()
GET /v1/tickets/{id}Get a ticketbt.tickets.get()
GET /v1/tickets/{id}/commentsList ticket commentsbt.tickets.comments()
PATCH /v1/tickets/{id}Update a ticketbt.tickets.update()
GET /v1/tablesList tablesbt.tables.list()
GET /v1/tables/{id}Get a tablebt.tables.get()
GET /v1/tables/{id}/rowsList rowsbt.tables.rows.list(), bt.tables.rows.listAll()
POST /v1/tables/{id}/rowsAdd a rowbt.tables.rows.create()
GET /v1/tables/{id}/rows/{rowId}Get a rowbt.tables.rows.get()
PATCH /v1/tables/{id}/rows/{rowId}Update a rowbt.tables.rows.update()
DELETE /v1/tables/{id}/rows/{rowId}Delete a rowbt.tables.rows.delete()
POST /v1/campaignsCreate campaignbt.campaigns.create()
GET /v1/campaignsList campaignsbt.campaigns.list(), bt.campaigns.listAll()
GET /v1/campaigns/{id}Get a campaignbt.campaigns.get()
POST /v1/templatesSubmit templatebt.templates.create()
GET /v1/templatesList templatesbt.templates.list(), bt.templates.listAll()
GET /v1/templates/{id}Get a templatebt.templates.get()
POST /v1/templates/{id}/submitSubmit a draft templatebt.templates.submit()
POST /v1/flowsCreate Flow draftbt.flows.create()
POST /v1/flows/{id}/publishPublish Flowbt.flows.publish()
GET /v1/flowsList Flowsbt.flows.list(), bt.flows.listAll()
GET /v1/flows/{id}Get a Flowbt.flows.get()
PATCH /v1/flows/{id}Update a Flowbt.flows.update()
DELETE /v1/flows/{id}Delete a Flowbt.flows.delete()
GET /v1/journeysList journeysbt.journeys.list()
GET /v1/journeys/{id}Get one journeybt.journeys.get()
POST /v1/journeys/{id}/enrollEnroll contacts in a journeybt.journeys.enroll()
GET /v1/ai-agentsList AI agentsbt.aiAgents.list()
GET /v1/ai-agents/{id}Get one AI agentbt.aiAgents.get()
POST /v1/ai-agents/{id}/chatChat with an AI agentbt.aiAgents.chat()
POST /v1/ai-agents/{id}/chat/handoffHand a chat to a humanbt.aiAgents.handoff()
PATCH /v1/ai-agents/{id}Pause an AI agentbt.aiAgents.pause()
GET /v1/sitesList sitesbt.sites.list()
POST /v1/sitesCreate a sitebt.sites.create()
POST /v1/sites/{id}/publishPublish a sitebt.sites.publish()
POST /v1/sites/{id}/unpublishTake a site offlinebt.sites.unpublish()
GET /v1/form-submissionsList form submissionsbt.formSubmissions.list()
PATCH /v1/form-submissions/{id}Mark a submission readbt.formSubmissions.markRead()
GET /v1/channels/whatsapp/readinessWhatsApp readinessbt.channels.whatsapp.readiness()
GET /v1/channelsList channelsbt.channels.list()
POST /v1/channels/connect-sessionsCreate a WhatsApp connect linkbt.channels.createConnectSession()
GET /v1/channels/connect-sessionsList WhatsApp connect linksbt.channels.listConnectSessions()
DELETE /v1/channels/connect-sessions/{id}Revoke a WhatsApp connect linkbt.channels.revokeConnectSession()
POST /v1/webhooksCreate webhook subscriptionbt.webhooks.create()
POST /v1/webhooks/{id}/testSend test webhookbt.webhooks.test()
GET /v1/webhook-deliveriesList webhook deliveriesbt.webhooks.deliveries(), bt.webhooks.deliveriesAll()
POST /v1/webhook-deliveries/{id}/replayReplay webhook deliverybt.webhooks.replayDelivery()
POST /v1/webhooks/{id}/replay-failedReplay failed deliveries since a timebt.webhooks.replayFailed()
GET /v1/webhooksList webhook subscriptionsbt.webhooks.list()
PATCH /v1/webhooks/{id}Update a webhook subscriptionbt.webhooks.rotateSecret(), bt.webhooks.update()
DELETE /v1/webhooks/{id}Delete a webhook subscriptionbt.webhooks.delete()
GET /v1/integrationsList integrationsbt.integrations.list(), bt.integrations.listAll()
POST /v1/integrationsCreate an integrationbt.integrations.create()
GET /v1/integrations/{id}Get an integrationbt.integrations.get()
PATCH /v1/integrations/{id}Update an integrationbt.integrations.update()
DELETE /v1/integrations/{id}Delete an integrationbt.integrations.delete()
GET /v1/integrations/{id}/deliveriesList integration deliveriesbt.integrations.deliveries(), bt.integrations.deliveriesAll()
POST /v1/integrations/{id}/testTest-fire an integrationbt.integrations.test()
GET /v1/suppressionList suppression entriesbt.suppression.list(), bt.suppression.listAll()
POST /v1/suppressionAdd suppression entrybt.suppression.add()
DELETE /v1/suppression/{id}Remove a suppression entrybt.suppression.remove()
GET /v1/email/sending-domainGet email sending identitybt.email.sendingDomain.get()
POST /v1/email/sending-domainAdd an email sending identitybt.email.sendingDomain.register()
POST /v1/email/sending-domain/verifyVerify the email sending identitybt.email.sendingDomain.verify()
DELETE /v1/email/sending-domainRemove the email sending identitybt.email.sendingDomain.remove()
GET /v1/walletRead the prepaid wallet balancebt.wallet.get()
GET /v1/wallet/ledgerList wallet ledger entriesbt.wallet.ledger(), bt.wallet.ledgerAll()
GET /v1/wallet/invoicesList tax invoicesbt.wallet.invoices()
GET /v1/wallet/invoices/{id}Get a tax invoicebt.wallet.invoice()
POST /v1/conversionsRecord a conversionbt.conversions.record()
GET /v1/commerce/products/{id}/salesProduct salesbt.commerce.productSales()
GET /v1/commerce/products/{id}/stock-movementsProduct stock movementsbt.commerce.productStockMovements()
GET /v1/commerce/productsList productsbt.commerce.listProducts()
POST /v1/commerce/productsCreate a productbt.commerce.createProduct()
POST /v1/commerce/products/mediaUpload a product imagebt.commerce.uploadProductImage()
PATCH /v1/commerce/products/{id}Update a product or adjust stockbt.commerce.updateProduct()
DELETE /v1/commerce/products/{id}Remove a productbt.commerce.deleteProduct()
GET /v1/commerce/ordersList ordersbt.commerce.listOrders()
GET /v1/commerce/orders/overviewOrder totalsbt.commerce.ordersOverview()
GET /v1/commerce/orders/{id}Get one orderbt.commerce.getOrder()
PATCH /v1/commerce/orders/{id}/fulfilmentMark an order fulfilledbt.commerce.setOrderFulfilment()
GET /v1/commerce/payment-linksList payment linksbt.commerce.listPaymentLinks()
POST /v1/commerce/payment-linksCreate a payment linkbt.commerce.createPaymentLink()
GET /v1/commerce/fulfilmentGet delivery & collection settingsbt.commerce.getFulfilment()
PUT /v1/commerce/fulfilmentSet delivery & collection settingsbt.commerce.setFulfilment()
GET /v1/commerce/discountsList discount codesbt.commerce.listDiscounts()
POST /v1/commerce/discountsCreate a discount codebt.commerce.createDiscount()
PATCH /v1/commerce/discounts/{id}Pause or resume a discount codebt.commerce.updateDiscount()
DELETE /v1/commerce/discounts/{id}Delete a discount codebt.commerce.deleteDiscount()

What it gives you

Typed requests and responses
The types are written by hand and checked against the OpenAPI document as it stood when 0.8.0 was released. Endpoints added since are in the table above; for any language, generate types from the live document below.
Pagination you do not write
listAll() walks the cursor for you. Cursors stay opaque: the SDK never constructs one.
Retries and idempotency
GET, PATCH, PUT and DELETE, and the POSTs that deduplicate (the client adds their Idempotency-Key itself), are retried up to twice by default (maxRetries) on 429, 500, 502, 503 and 504. A POST that cannot deduplicate is retried only on 429, and a refusal a person must clear is never retried, so a retry never sends twice.
Server-side only
Your API key is a bearer secret, so the client refuses to start in a browser. Call Blueticked from your server.

Options and escape hatches

Runs on Node 18 and later, in serverless functions and on edge runtimes.

MIT licensed; the README on npm lists every method.

Client and per-call options
import { Blueticked } from "@blueticked/sdk";

const bt = new Blueticked({
  apiKey: process.env.BLUETICKED_API_KEY!,
  timeoutMs: 15000, // default 30000
  maxRetries: 0, // default 2; 0 turns retries off
});

// Your own Idempotency-Key for one call:
await bt.contacts.upsert({ phone: "+27821234567" }, { idempotencyKey: "crm-sync-1001" });

// An endpoint newer than your package, with the same auth, retries and errors:
const res = await bt.request<{ ok: boolean }>("GET", "/v1/readiness");

// Verify and parse a webhook delivery in one call (throws on a bad signature):
const event = await bt.webhooks.constructEvent(rawBody, req.headers, process.env.BLUETICKED_WEBHOOK_SECRET!);
timeoutMs
How long one request may take, in milliseconds. The default is 30000.
maxRetries
How many times a retryable failure is tried again. The default is 2; 0 turns retries off.
idempotencyKey
A per-call option: your own Idempotency-Key, always sent when you give one (the client otherwise makes one for the POSTs that deduplicate).
bt.request()
Calls any endpoint, including one newer than the package, with the same auth, retries and error type. Declare the response type yourself.
bt.isTestMode
True when the client holds a blu_test_ key.
bt.webhooks.constructEvent()
Verifies a delivery and parses it in one call. Pass the raw body and the whole header set; it throws on a bad or stale signature, so answer 401 from your catch.

Generate your own client

Works today, in any language with an OpenAPI generator.

The document is public and needs no key.

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

Prefer to let an agent write the integration? Hand it the whole API in one fetch.