TypeScript SDK
A typed client generated from the same OpenAPI document this reference is built from.
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.
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.
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.
| Endpoint | What it does |
|---|---|
| GET /v1/studio-flows | List Flow Studio flows |
| GET /v1/studio-flows/{id} | Get a Flow Studio flow |
| POST /v1/studio-flows | Create a Flow Studio flow |
| PATCH /v1/studio-flows/{id} | Update a Flow Studio flow |
| POST /v1/studio-flows/{id}/switch-off | Switch a Flow Studio flow off |
| GET /v1/channels/{id}/whatsapp-spend | WhatsApp spend |
| POST /v1/commerce/orders/{id}/ready-for-collection | Mark an order ready for collection |
The 120 endpoints with a method
| Endpoint | What it does | Client method |
|---|---|---|
| GET /v1/me | Get workspace context | bt.workspace.getContext() |
| GET /v1/readiness | Check send readiness | bt.workspace.getReadiness() |
| GET /v1/businesses/{id}/keys | List credentials you issued to a customer | bt.businesses.listKeys() |
| DELETE /v1/businesses/{id}/keys | Revoke a credential you issued to a customer | bt.businesses.revokeKey() |
| GET /v1/businesses | List your customers | bt.businesses.list() |
| POST /v1/businesses | Onboard a customer | bt.businesses.create() |
| POST /v1/businesses/{id}/keys | Issue a credential for a customer | bt.businesses.createKey() |
| POST /v1/messages | Send one message | bt.messages.send() |
| GET /v1/messages/{id} | Get message status | bt.messages.get() |
| POST /v1/events/send | Send a flow event | bt.events.send() |
| POST /v1/contacts | Create or update contact | bt.contacts.upsert() |
| GET /v1/contacts | List contacts | bt.contacts.list(), bt.contacts.listAll() |
| GET /v1/contacts/{id} | Get a contact | bt.contacts.get() |
| PATCH /v1/contacts/{id} | Update a contact | bt.contacts.update() |
| DELETE /v1/contacts/{id} | Delete a contact | bt.contacts.delete() |
| POST /v1/contacts/{id}/erase | Erase a contact (POPIA) | bt.contacts.erase() |
| POST /v1/documents | Upload a document for a contact | bt.documents.upload() |
| GET /v1/groups | List contact groups | bt.groups.list(), bt.groups.listAll() |
| POST /v1/groups | Create contact group | bt.groups.create() |
| GET /v1/groups/{id} | Get a contact group | bt.groups.get() |
| PATCH /v1/groups/{id} | Rename a contact group | bt.groups.update() |
| DELETE /v1/groups/{id} | Delete a contact group | bt.groups.delete() |
| PUT /v1/groups/{id}/members | Add contacts to a group | bt.groups.members.add() |
| DELETE /v1/groups/{id}/members | Remove contacts from a group | bt.groups.members.remove() |
| GET /v1/segments | List segments | bt.segments.list(), bt.segments.listAll() |
| GET /v1/conversations | List conversations | bt.conversations.list(), bt.conversations.listAll() |
| PATCH /v1/conversations/{id} | Update conversation | bt.conversations.update() |
| GET /v1/conversations/{id} | Get a conversation | bt.conversations.get() |
| GET /v1/conversations/{id}/messages | List conversation messages | bt.conversations.messages(), bt.conversations.messagesAll() |
| GET /v1/conversations/{id}/messages/{messageId}/media | Get a message's file | bt.conversations.messageMedia() |
| GET /v1/tickets | List tickets | bt.tickets.list(), bt.tickets.listAll() |
| POST /v1/tickets | Raise a ticket | bt.tickets.create() |
| GET /v1/tickets/{id} | Get a ticket | bt.tickets.get() |
| GET /v1/tickets/{id}/comments | List ticket comments | bt.tickets.comments() |
| PATCH /v1/tickets/{id} | Update a ticket | bt.tickets.update() |
| GET /v1/tables | List tables | bt.tables.list() |
| GET /v1/tables/{id} | Get a table | bt.tables.get() |
| GET /v1/tables/{id}/rows | List rows | bt.tables.rows.list(), bt.tables.rows.listAll() |
| POST /v1/tables/{id}/rows | Add a row | bt.tables.rows.create() |
| GET /v1/tables/{id}/rows/{rowId} | Get a row | bt.tables.rows.get() |
| PATCH /v1/tables/{id}/rows/{rowId} | Update a row | bt.tables.rows.update() |
| DELETE /v1/tables/{id}/rows/{rowId} | Delete a row | bt.tables.rows.delete() |
| POST /v1/campaigns | Create campaign | bt.campaigns.create() |
| GET /v1/campaigns | List campaigns | bt.campaigns.list(), bt.campaigns.listAll() |
| GET /v1/campaigns/{id} | Get a campaign | bt.campaigns.get() |
| POST /v1/templates | Submit template | bt.templates.create() |
| GET /v1/templates | List templates | bt.templates.list(), bt.templates.listAll() |
| GET /v1/templates/{id} | Get a template | bt.templates.get() |
| POST /v1/templates/{id}/submit | Submit a draft template | bt.templates.submit() |
| POST /v1/flows | Create Flow draft | bt.flows.create() |
| POST /v1/flows/{id}/publish | Publish Flow | bt.flows.publish() |
| GET /v1/flows | List Flows | bt.flows.list(), bt.flows.listAll() |
| GET /v1/flows/{id} | Get a Flow | bt.flows.get() |
| PATCH /v1/flows/{id} | Update a Flow | bt.flows.update() |
| DELETE /v1/flows/{id} | Delete a Flow | bt.flows.delete() |
| GET /v1/journeys | List journeys | bt.journeys.list() |
| GET /v1/journeys/{id} | Get one journey | bt.journeys.get() |
| POST /v1/journeys/{id}/enroll | Enroll contacts in a journey | bt.journeys.enroll() |
| GET /v1/ai-agents | List AI agents | bt.aiAgents.list() |
| GET /v1/ai-agents/{id} | Get one AI agent | bt.aiAgents.get() |
| POST /v1/ai-agents/{id}/chat | Chat with an AI agent | bt.aiAgents.chat() |
| POST /v1/ai-agents/{id}/chat/handoff | Hand a chat to a human | bt.aiAgents.handoff() |
| PATCH /v1/ai-agents/{id} | Pause an AI agent | bt.aiAgents.pause() |
| GET /v1/sites | List sites | bt.sites.list() |
| POST /v1/sites | Create a site | bt.sites.create() |
| POST /v1/sites/{id}/publish | Publish a site | bt.sites.publish() |
| POST /v1/sites/{id}/unpublish | Take a site offline | bt.sites.unpublish() |
| GET /v1/form-submissions | List form submissions | bt.formSubmissions.list() |
| PATCH /v1/form-submissions/{id} | Mark a submission read | bt.formSubmissions.markRead() |
| GET /v1/channels/whatsapp/readiness | WhatsApp readiness | bt.channels.whatsapp.readiness() |
| GET /v1/channels | List channels | bt.channels.list() |
| POST /v1/channels/connect-sessions | Create a WhatsApp connect link | bt.channels.createConnectSession() |
| GET /v1/channels/connect-sessions | List WhatsApp connect links | bt.channels.listConnectSessions() |
| DELETE /v1/channels/connect-sessions/{id} | Revoke a WhatsApp connect link | bt.channels.revokeConnectSession() |
| POST /v1/webhooks | Create webhook subscription | bt.webhooks.create() |
| POST /v1/webhooks/{id}/test | Send test webhook | bt.webhooks.test() |
| GET /v1/webhook-deliveries | List webhook deliveries | bt.webhooks.deliveries(), bt.webhooks.deliveriesAll() |
| POST /v1/webhook-deliveries/{id}/replay | Replay webhook delivery | bt.webhooks.replayDelivery() |
| POST /v1/webhooks/{id}/replay-failed | Replay failed deliveries since a time | bt.webhooks.replayFailed() |
| GET /v1/webhooks | List webhook subscriptions | bt.webhooks.list() |
| PATCH /v1/webhooks/{id} | Update a webhook subscription | bt.webhooks.rotateSecret(), bt.webhooks.update() |
| DELETE /v1/webhooks/{id} | Delete a webhook subscription | bt.webhooks.delete() |
| GET /v1/integrations | List integrations | bt.integrations.list(), bt.integrations.listAll() |
| POST /v1/integrations | Create an integration | bt.integrations.create() |
| GET /v1/integrations/{id} | Get an integration | bt.integrations.get() |
| PATCH /v1/integrations/{id} | Update an integration | bt.integrations.update() |
| DELETE /v1/integrations/{id} | Delete an integration | bt.integrations.delete() |
| GET /v1/integrations/{id}/deliveries | List integration deliveries | bt.integrations.deliveries(), bt.integrations.deliveriesAll() |
| POST /v1/integrations/{id}/test | Test-fire an integration | bt.integrations.test() |
| GET /v1/suppression | List suppression entries | bt.suppression.list(), bt.suppression.listAll() |
| POST /v1/suppression | Add suppression entry | bt.suppression.add() |
| DELETE /v1/suppression/{id} | Remove a suppression entry | bt.suppression.remove() |
| GET /v1/email/sending-domain | Get email sending identity | bt.email.sendingDomain.get() |
| POST /v1/email/sending-domain | Add an email sending identity | bt.email.sendingDomain.register() |
| POST /v1/email/sending-domain/verify | Verify the email sending identity | bt.email.sendingDomain.verify() |
| DELETE /v1/email/sending-domain | Remove the email sending identity | bt.email.sendingDomain.remove() |
| GET /v1/wallet | Read the prepaid wallet balance | bt.wallet.get() |
| GET /v1/wallet/ledger | List wallet ledger entries | bt.wallet.ledger(), bt.wallet.ledgerAll() |
| GET /v1/wallet/invoices | List tax invoices | bt.wallet.invoices() |
| GET /v1/wallet/invoices/{id} | Get a tax invoice | bt.wallet.invoice() |
| POST /v1/conversions | Record a conversion | bt.conversions.record() |
| GET /v1/commerce/products/{id}/sales | Product sales | bt.commerce.productSales() |
| GET /v1/commerce/products/{id}/stock-movements | Product stock movements | bt.commerce.productStockMovements() |
| GET /v1/commerce/products | List products | bt.commerce.listProducts() |
| POST /v1/commerce/products | Create a product | bt.commerce.createProduct() |
| POST /v1/commerce/products/media | Upload a product image | bt.commerce.uploadProductImage() |
| PATCH /v1/commerce/products/{id} | Update a product or adjust stock | bt.commerce.updateProduct() |
| DELETE /v1/commerce/products/{id} | Remove a product | bt.commerce.deleteProduct() |
| GET /v1/commerce/orders | List orders | bt.commerce.listOrders() |
| GET /v1/commerce/orders/overview | Order totals | bt.commerce.ordersOverview() |
| GET /v1/commerce/orders/{id} | Get one order | bt.commerce.getOrder() |
| PATCH /v1/commerce/orders/{id}/fulfilment | Mark an order fulfilled | bt.commerce.setOrderFulfilment() |
| GET /v1/commerce/payment-links | List payment links | bt.commerce.listPaymentLinks() |
| POST /v1/commerce/payment-links | Create a payment link | bt.commerce.createPaymentLink() |
| GET /v1/commerce/fulfilment | Get delivery & collection settings | bt.commerce.getFulfilment() |
| PUT /v1/commerce/fulfilment | Set delivery & collection settings | bt.commerce.setFulfilment() |
| GET /v1/commerce/discounts | List discount codes | bt.commerce.listDiscounts() |
| POST /v1/commerce/discounts | Create a discount code | bt.commerce.createDiscount() |
| PATCH /v1/commerce/discounts/{id} | Pause or resume a discount code | bt.commerce.updateDiscount() |
| DELETE /v1/commerce/discounts/{id} | Delete a discount code | bt.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.
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.
npx openapi-typescript https://app.blueticked.com/api/v1/openapi.json \
-o ./src/blueticked.d.tsPrefer to let an agent write the integration? Hand it the whole API in one fetch.