# 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 [CLI](https://usebluebot.com/docs/cli). For another language, every example in these docs also shows Go, Ruby, PHP and plain `fetch`, and the [OpenAPI file](https://usebluebot.com/docs/openapi) 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:

```bash
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"
    }
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const message = await client.messages.send({
  to: '+15555550123',
  text: 'Hi Maria, the showing at 412 Oak St is confirmed for 3 PM.',
  metadata: {
    crm_id: '48213',
  },
}, { idempotencyKey: 'crm-48213-confirmation' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

message = client.messages.send(
    to="+15555550123",
    text="Hi Maria, the showing at 412 Oak St is confirmed for 3 PM.",
    metadata={
        "crm_id": "48213",
    },
    idempotency_key="crm-48213-confirmation",
)
print(message)
```

## 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](https://usebluebot.com/docs/sdks/typescript#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](https://usebluebot.com/docs/concepts/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:

```ts title="TypeScript"
for await (const message of client.messages.list({ state: 'failed' })) {
  console.log(message.id, message.failure?.reason);
}
```

```python title="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                                                                  |
| ---------------------------- | --------------------------------------------------------------------------- |
| `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`. [Errors](https://usebluebot.com/docs/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:

```go title="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:

```ruby title="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:

```php title="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

- [TypeScript SDK](https://usebluebot.com/docs/sdks/typescript): Every method, pagination, errors, webhooks and types.
- [Python SDK](https://usebluebot.com/docs/sdks/python): Every method in snake_case, with sync and async clients.
- [CLI](https://usebluebot.com/docs/cli): Send, read and listen for webhooks from a terminal.
- [OpenAPI, Postman and Insomnia](https://usebluebot.com/docs/openapi): Import the API, or generate a client for another language.
