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.
| Language | Package | Install | Needs |
|---|---|---|---|
| TypeScript and JavaScript | @usebluebot/sdk on npm | npm install @usebluebot/sdk | Node 18 or newer, Bun, Deno, or an edge runtime |
| Python | bluebot on PyPI | pip install bluebot | Python 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 . For another language, every example in these docs also shows Go, Ruby, PHP and plain fetch, and the 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:
/v1/messagescurl -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 readBLUEBOT_API_KEYfrom 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()andclient.me(). 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 insidemetadataare 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, withmessage.from_for the API’sfrom, 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. 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 aRetry-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:
for await (const message of client.messages.list({ state: 'failed' })) {
console.log(message.id, message.failure?.reason);
}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 |
|---|---|
status | The HTTP status, such as 429. |
code | BlueBot’s stable code, such as rate_limited. Branch on this. |
message | One sentence saying what went wrong. |
hint | What to do next. |
docUrl / doc_url | The code’s page in these docs. |
requestId / request_id | Quote it when you write to support. |
retryAfter / retry_after | Seconds to wait, on rate_limited. |
details | For invalid_request, each field that didn’t fit, as { field, message }. |
| Class | Status | Codes |
|---|---|---|
BadRequestError | 400 | invalid_request |
AuthenticationError | 401 | unauthorized |
PlanLapsedError | 402 | plan_lapsed |
PermissionDeniedError | 403 | forbidden, insufficient_scope, account_held |
NotFoundError | 404 | not_found |
ConflictError | 409 | conflict, idempotency_conflict, limit_reached |
PayloadTooLargeError | 413 | payload_too_large |
UnsupportedMediaTypeError | 415 | unsupported_media_type |
RateLimitError | 429 | rate_limited |
InternalServerError | 500, 502, 503 | internal, 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. 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:
- Send
Authorization: Bearer <key>and, on a body,Content-Type: application/json. - Send an
Idempotency-Keyon every POST, and reuse it when you retry that request. - On a refusal, read
error.codefrom the JSON body. Never parseerror.message, which may change. - Retry 429 and 5xx answers after the
Retry-Afterheader’s seconds, or a short backoff. - Page by sending the answer’s
next_cursorascursoruntilhas_moreis false.
Here is that last one in Go, reading every failed text:
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:
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"
endAnd 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";
}