# clicksend-rs
Unofficial [ClickSend](https://clicksend.com) SDK for Rust.
- async-first; optional `blocking` feature uses `reqwest::blocking` (no tokio leak)
- namespaced API: `client.sms().send()`, `client.account().get()`
- redacted `Debug` (api key never logged)
- timeouts + user-agent + retry built into the builder
- `tracing` spans on every request
- webhook parsers for inbound SMS / delivery receipts
- escape hatch: `client.raw_request(method, path)` for endpoints not yet wrapped
## Install
```toml
[dependencies]
clicksend-rs = "0.1"
# or with blocking:
clicksend-rs = { version = "0.1", features = ["blocking"] }
```
## Auth
ClickSend uses HTTP basic with **username + api_key**. Grab both from
[dashboard → API credentials](https://dashboard.clicksend.com/#/account/subaccount).
## Async
```rust
use clicksend_rs::{Client, SmsMessage, SmsMessageCollection};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let c = Client::builder("user", "key")
.timeout(Duration::from_secs(15))
.retry(clicksend_rs::RetryConfig::enabled(3))
.build()?;
let coll = SmsMessageCollection::new(vec![
SmsMessage::new("+1234567890", "hello from rust"),
]);
// free — just prices
let est = c.sms().price(&coll).await?;
println!("{:?}", est.data);
// costs money
// c.sms().send(&coll).await?;
let acct = c.account().get().await?;
println!("balance: {:?}", acct.data.unwrap_or_default().balance);
Ok(())
}
```
## Blocking
```rust
use clicksend_rs::{BlockingClient, SmsMessage, SmsMessageCollection};
let c = BlockingClient::new("user", "key");
let coll = SmsMessageCollection::new(vec![SmsMessage::new("+1234567890", "hi")]);
c.sms().price(&coll)?;
```
## Webhook parsing
```rust
use clicksend_rs::webhook::{parse_inbound_sms, parse_delivery_receipt};
// in your HTTP handler:
let inbound = parse_inbound_sms(&body)?;
let receipt = parse_delivery_receipt(&body)?;
```
## Endpoints implemented
| `account().get()` | GET | `/account` |
| `sms().send()` | POST | `/sms/send` *(costs money)* |
| `sms().price()` | POST | `/sms/price` *(free)* |
| `sms().history()` | GET | `/sms/history` |
| `sms().receipts()` | GET | `/sms/receipts` |
| `sms().inbound()` | GET | `/sms/inbound` |
| `sms().cancel()` | PUT | `/sms/{id}/cancel` |
| `sms().cancel_all()` | PUT | `/sms/cancel-all` |
| `mms().send()` | POST | `/mms/send` |
| `voice().send()` | POST | `/voice/send` |
| `email().send()` | POST | `/email/send` |
ClickSend has ~30 more (campaigns, contacts, fax, post mail, subaccounts).
For those, use the escape hatch:
```rust
let resp: ApiEnvelope<serde_json::Value> = client
.raw_request(reqwest::Method::GET, "/contacts/lists")
.send().await?
.json().await?;
```
## Error handling
```rust
match c.sms().send(&coll).await {
Ok(env) => { /* env.data has results */ }
Err(ClickSendError::Unauthorized) => { /* bad creds */ }
Err(ClickSendError::RateLimited { retry_after_secs }) => { /* back off */ }
Err(ClickSendError::Api { code, message, .. }) => {
// HTTP 200 but logical error (e.g. bad sender id, blocked country)
}
Err(e) => { /* http / decode / other */ }
}
```
## Live tests
```sh
cp .env.example .env # then fill in
cargo test --test live -- --ignored sms_price_async --nocapture
```
All live tests are `#[ignore]` so they don't run by accident. `sms_send` costs money — gated.
## License
0BSD.