# blooio
[](https://crates.io/crates/blooio)
[](https://docs.rs/blooio)
[](#license)
Typed, low-overhead Rust bindings for the [Blooio](https://blooio.com) API
(iMessage / SMS automation), exposing **both** an async and a blocking surface
from a single sans-IO core. Sync users pull no async runtime.
## Features
| `async` | ✅ | The async [`Client`] executor (reqwest). |
| `sync` | | The blocking `BlockingClient` executor (ureq), no tokio. |
| `rustls` | ✅ | TLS via rustls. |
| `native-tls` | | TLS via the system's native stack. |
| `webhooks` | ✅ | Typed webhook payloads + HMAC signature verification. |
| `tracing` | ✅ | Secret-redacted request instrumentation. |
At least one of `async` / `sync` must be enabled (enforced at compile time).
## Install
```toml
[dependencies]
blooio = "0.1"
```
Blocking client only, no async runtime:
```toml
[dependencies]
blooio = { version = "0.1", default-features = false, features = ["sync", "rustls", "webhooks"] }
```
## Quick start (async)
```rust,no_run
use blooio::Client;
#[tokio::main]
async fn main() -> blooio::Result<()> {
let client = Client::new(std::env::var("BLOOIO_API_KEY").unwrap())?;
// Who am I?
let me = client.account().get().await?;
// Send a message.
let chat = client.chat("chat-id");
chat.send_text("hello from rust").await?;
Ok(())
}
```
## Quick start (blocking)
```rust,no_run
use blooio::BlockingClient;
fn main() -> blooio::Result<()> {
let client = BlockingClient::new(std::env::var("BLOOIO_API_KEY").unwrap())?;
client.chat("chat-id").send_text("hello from rust")?;
Ok(())
}
```
The async and blocking surfaces are mirror images: the same resource handles and
method names, differing only by `.await`.
## Resources
Resource handles hang off the client and group the endpoints:
| `account()` | `get` |
| `chats()` / `chat(id)` | `list`, `send`/`send_text`, messages, reactions, polls, typing, read receipts, backgrounds |
| `contacts()` | `list`, `create`, `get`, `update`, `delete`, `capabilities`, tags |
| `groups()` | `list`, `create`, `get`, `update`, `delete`, icons, `members(id)` |
| `contact_card()` | `get`, `update` |
| `facetime()` | `call` |
| `location()` | `list`, `get`, `refresh` |
| `numbers()` | `list` |
| `phone_numbers()` | `lookup`, `lookup_post`, `batch` |
| `webhooks()` | `list`, `create`, `get`, `update`, `delete`, `rotate_secret`, `logs(id)` |
### Builders
Endpoints with many optional fields use a fluent builder. For example, sending
a message:
```rust,no_run
# async fn demo(client: blooio::Client) -> blooio::Result<()> {
let message = client
.chat("chat-id")
.message()
.text("hi")
.effect("slam")
.use_typing_indicator(true)
.idempotency_key("abc-123");
client.chat("chat-id").send(message).await?;
# Ok(()) }
```
### Pagination
List endpoints expose a `*_all` paginator that fetches successive pages lazily:
```rust,no_run
# async fn demo(client: blooio::Client) -> blooio::Result<()> {
let mut pages = client.chats().list_all();
while let Some(page) = pages.next_page().await {
for chat in page? {
// ...
}
}
// or drain everything:
let all = client.contacts().list_all().collect_all().await?;
# Ok(()) }
```
In the blocking client, the paginator also implements `Iterator`.
### Escape hatch
Every endpoint is described once as a public [`Operation`]. Anything not covered
by a convenience method can be sent directly:
```rust,no_run
# async fn demo(client: blooio::Client, op: impl blooio::Operation<Output = ()>) -> blooio::Result<()> {
let out = client.send(op).await?;
# Ok(()) }
```
## Configuration
`Client::new(key)` uses production defaults. For more control, build a
`ClientConfig`:
```rust,no_run
use blooio::{Client, ClientConfig};
use std::time::Duration;
# fn demo() -> blooio::Result<()> {
let config = ClientConfig::new("my-api-key")
.with_base_url("https://backend.blooio.com/v2/api")
.with_timeout(Duration::from_secs(10))
.with_user_agent("my-app/1.0");
let client = Client::from_config(config)?;
# Ok(()) }
```
The API key is wrapped in a `Secret`, which zeroizes on drop and redacts itself
in `Debug` output (`api_key: [REDACTED]`) — it is never logged or serialized in
cleartext.
## Errors
All fallible calls return `blooio::Result<T>`. The `Error` enum distinguishes
`Api` (non-2xx, with a machine-readable `code`), `Transport`, `Encode`,
`Decode`, and (with `webhooks`) `Webhook`. Match on the stable code for
programmatic handling:
```rust,no_run
# fn handle(err: blooio::Error) {
if err.code() == Some("outbound_limit_reached") {
// back off and retry later
}
# }
```
## Webhooks
With the `webhooks` feature, verify and parse incoming events. The module is
framework-agnostic — it does not run a server:
```rust,no_run
use blooio::webhook::{self, WebhookEvent};
# fn handle(secret: &[u8], sig_header: &str, raw_body: &[u8]) -> blooio::Result<()> {
// Verify the signature (constant-time, with replay protection).
webhook::verify_default(secret, sig_header, raw_body)?;
// Parse the typed payload.
let event = WebhookEvent::parse(raw_body)?;
if let Some(kind) = event.kind() {
// dispatch on the message event kind
}
# Ok(()) }
```
## Tracing
With the `tracing` feature, each request emits a `blooio.request` span carrying
the method, path, status, and elapsed time. The API key is never recorded.
## License
Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.
[`Client`]: https://docs.rs/blooio/latest/blooio/struct.Client.html
[`Operation`]: https://docs.rs/blooio/latest/blooio/trait.Operation.html