clicksend-rs 0.1.1

Unofficial ClickSend SDK for Rust (async + optional blocking).
Documentation
# 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

| Resource | Method | Path |
|----------|--------|------|
| `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.