Skip to the page

SDKs and tools

SDKs

The TypeScript and Python SDKs wrap every API operation with typed models, automatic retries, idempotency keys, pagination and webhook verification. How they behave, and how to call the API from Go, Ruby and PHP without one.

BlueBot has two official SDKs. Both cover every operation in the API, one method each, and behave the same way: they send an idempotency key on every POST, retry what is safe to retry, page through lists for you, raise typed errors that carry BlueBot’s error code, and check webhook signatures.

LanguagePackageInstallNeeds
TypeScript and JavaScript@usebluebot/sdk on npmnpm install @usebluebot/sdkNode 18 or newer, Bun, Deno, or an edge runtime
Pythonbluebot on PyPIpip install bluebotPython 3.9 or newer

The npm package is under the @usebluebot scope. The plain name bluebot on npm belongs to somebody else and has nothing to do with BlueBot, so check the scope before you install.

For the command line, there’s the CLI. For another language, every example in these docs also shows Go, Ruby, PHP and plain fetch, and the OpenAPI file can generate a client.

The same call in each

Every request example in the docs has a tab for each language, and your choice follows you from page to page. Sending a text:

POST/v1/messagesSend a text →
curl -X POST "https://usebluebot.com/api/v1/messages" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-48213-confirmation" \
  -d '{
    "to": "+15555550123",
    "text": "Hi Maria, the showing at 412 Oak St is confirmed for 3 PM.",
    "metadata": {
      "crm_id": "48213"
    }
  }'

How the SDKs are shaped

  • One client, made with your key. new BlueBot({ apiKey }) in TypeScript, BlueBot(api_key=...) in Python. Both read BLUEBOT_API_KEY from the environment when you don’t pass a key.
  • Resources mirror the API. client.messages.send, client.messages.list, client.webhooks.deliveries.replay. A few single calls sit on the client itself: client.lookup(number), client.status() and client.me(). Every method is listed by resource.
  • Path parameters come first, positionally. client.messages.cancel("msg_…"), client.webhooks.update("wh_…", { paused: true }).
  • The body or query comes next. One object in TypeScript, in camelCase (sendAt, chatId). Keyword arguments in Python, in snake_case (send_at=, chat_id=). Keys inside metadata are yours and are never renamed.
  • Request options come last. { idempotencyKey, timeout, maxRetries, signal, headers } in TypeScript; idempotency_key=, timeout=, max_retries=, extra_headers= in Python.
  • Answers are typed records. TypeScript answers in camelCase (message.sendAt, page.nextCursor). Python answers pydantic models in snake_case, with message.from_ for the API’s from, which is a reserved word in Python.

What every call does for you

Idempotency keys

Every POST carries an Idempotency-Key. If you don’t pass one, the SDK makes a random one and reuses it for each retry of that call, so a retried send never sends twice. Pass your own when the same logical send could be attempted again later, such as a job that might run twice: crm-48213-confirmation makes BlueBot answer the first text again for 24 hours instead of sending another. Idempotency explains the rules.

Retries

A call is tried up to three times in all (maxRetries: 2) when:

  • the connection failed or timed out;
  • BlueBot answered rate_limited (429), or any 5xx error;
  • BlueBot answered conflict (409) with a Retry-After, which means an earlier request with the same idempotency key is still running.

Between tries the SDK waits what BlueBot asked (Retry-After, or retry_after in the error), or else half a second, then one second, then two, with a little randomness, never more than 8 seconds. When BlueBot asks for longer than 60 seconds, the SDK raises the error at once rather than hold your program up. Nothing else is retried: a 400 or a 404 will answer the same way every time.

Timeouts

Each try waits 60 seconds for an answer by default. Most calls answer in well under a second; lookup can take up to 15, because it asks your Mac.

Pagination

List calls answer one page, newest first. Loop over the call itself and the SDK fetches the following pages as you go:

TypeScript
for await (const message of client.messages.list({ state: 'failed' })) {
  console.log(message.id, message.failure?.reason);
}
Python
for message in client.messages.list(state="failed"):
    print(message.id, message.failure.reason if message.failure else None)

Errors

A refusal raises an error whose class says the kind and whose fields carry BlueBot’s answer:

Field (TypeScript / Python)What it is
statusThe HTTP status, such as 429.
codeBlueBot’s stable code, such as rate_limited. Branch on this.
messageOne sentence saying what went wrong.
hintWhat to do next.
docUrl / doc_urlThe code’s page in these docs.
requestId / request_idQuote it when you write to support.
retryAfter / retry_afterSeconds to wait, on rate_limited.
detailsFor invalid_request, each field that didn’t fit, as { field, message }.
ClassStatusCodes
BadRequestError400invalid_request
AuthenticationError401unauthorized
PlanLapsedError402plan_lapsed
PermissionDeniedError403forbidden, insufficient_scope, account_held
NotFoundError404not_found
ConflictError409conflict, idempotency_conflict, limit_reached
PayloadTooLargeError413payload_too_large
UnsupportedMediaTypeError415unsupported_media_type
RateLimitError429rate_limited
InternalServerError500, 502, 503internal, upstream_failed, unavailable

All of them extend APIError, which extends BlueBotError. When BlueBot couldn’t be reached at all, the SDK raises APIConnectionError, or its subclass APITimeoutError when it waited too long. A bad webhook signature raises WebhookVerificationError. Errors has a page for every code.

A held text is not an error. client.messages.send answers the message with state: "queued" and a hold saying why it waits, and the text goes on its own when the hold ends.

Logging

Set BLUEBOT_LOG=debug to have the SDK log each request’s method, path, status, time and request id. It never logs bodies, so the words of a text, a phone number in a body and your key never reach your logs.

Without an SDK: Go, Ruby and PHP

The API is plain HTTPS and JSON, and every request example in the docs has Go, Ruby and PHP tabs that use only each language’s standard library. Five things make a client solid in any language:

  1. Send Authorization: Bearer <key> and, on a body, Content-Type: application/json.
  2. Send an Idempotency-Key on every POST, and reuse it when you retry that request.
  3. On a refusal, read error.code from the JSON body. Never parse error.message, which may change.
  4. Retry 429 and 5xx answers after the Retry-After header’s seconds, or a short backoff.
  5. Page by sending the answer’s next_cursor as cursor until has_more is false.

Here is that last one in Go, reading every failed text:

Paging in Go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
)

type page struct {
	Data []struct {
		ID      string `json:"id"`
		To      string `json:"to"`
		Failure *struct {
			Reason string `json:"reason"`
		} `json:"failure"`
	} `json:"data"`
	HasMore    bool    `json:"has_more"`
	NextCursor *string `json:"next_cursor"`
}

func main() {
	query := url.Values{"state": {"failed"}, "limit": {"100"}}
	for {
		req, _ := http.NewRequest("GET", "https://usebluebot.com/api/v1/messages?"+query.Encode(), nil)
		req.Header.Set("Authorization", "Bearer "+os.Getenv("BLUEBOT_API_KEY"))
		res, err := http.DefaultClient.Do(req)
		if err != nil {
			panic(err)
		}
		var p page
		json.NewDecoder(res.Body).Decode(&p)
		res.Body.Close()
		for _, m := range p.Data {
			reason := ""
			if m.Failure != nil {
				reason = m.Failure.Reason
			}
			fmt.Println(m.ID, m.To, reason)
		}
		if !p.HasMore || p.NextCursor == nil {
			break
		}
		query.Set("cursor", *p.NextCursor)
	}
}

Reading an error in Ruby:

Errors in Ruby
require "net/http"
require "json"
require "securerandom"

uri = URI("https://usebluebot.com/api/v1/messages")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("BLUEBOT_API_KEY")}"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = SecureRandom.uuid
request.body = { to: "+15555550123", text: "Your order is ready for pickup." }.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
body = JSON.parse(response.body)

if response.is_a?(Net::HTTPSuccess)
  puts "#{body["id"]} is #{body["state"]}"
else
  error = body["error"]
  warn "#{error["code"]}: #{error["message"]} #{error["hint"]} (#{error["request_id"]})"
  sleep(response["Retry-After"].to_i) if error["code"] == "rate_limited"
end

And in PHP:

Errors in PHP
<?php
$ch = curl_init('https://usebluebot.com/api/v1/messages');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('BLUEBOT_API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS => json_encode(['to' => '+15555550123', 'text' => 'Your order is ready for pickup.']),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status >= 400) {
    $error = $body['error'];
    error_log("{$error['code']}: {$error['message']} {$error['hint']} ({$error['request_id']})");
} else {
    echo "{$body['id']} is {$body['state']}\n";
}

Where next