alguna 0.1.0

Rust SDK for the Alguna billing API
Documentation
# alguna

Rust SDK for the [Alguna API](https://alguna.com/docs): usage-based billing,
subscriptions, invoicing and payments.

## Install

```bash
cargo add alguna
cargo add tokio --features macros,rt-multi-thread
```

The crate uses `rustls` for TLS. It needs an async runtime such as `tokio`.

## Client

Create an API key in the dashboard under **Settings → Connections → Developers → API
keys**. A key looks like `<key_id>.<key_secret>`. The key is a secret, so use the SDK on
a server only.

```rust
use alguna::prelude::*;
use alguna::ApiClientBuilder;

fn clients() -> Result<(), ApiError> {
    // Reads the key from the ALGUNA_API_KEY environment variable.
    let from_env = ApiClientBuilder::default().build()?;
    let explicit = ApiClientBuilder::default()
        .api_key("<key_id>.<key_secret>")
        .build()?;
    let from_struct = Alguna::new(ClientConfig {
        api_key: Some("<key_id>.<key_secret>".to_string()),
        ..Default::default()
    })?;
    Ok(())
}
```

If there is no key, `build()` and `Alguna::new` return `ApiError::Configuration` and send
nothing.

| Setting     | Builder method   | Default                                                    |
| ----------- | ---------------- | ---------------------------------------------------------- |
| API key     | `api_key`        | the `ALGUNA_API_KEY` environment variable                  |
| API version | `api_version`    | `2026-04-01`, the version this release targets |
| Base URL    | `new(url)`       | `https://api.alguna.io`                                    |
| Timeout     | `timeout`        | 60 seconds for each attempt                                |
| Retries     | `max_retries`    | 2                                                          |
| HTTP client | `reqwest_client` | one built from the timeout and user agent                  |

The SDK sends the key as `Authorization: Bearer <key>`. Every request carries an
`Alguna-Version` header, so the response types match the API. For the sandbox, use
`ApiClientBuilder::new("https://api.sandbox.alguna.io")`.

## Requests

Each resource is a field of the client: `client.customers`, `client.invoices` and so
on. A method takes its request and an optional `RequestOptions`, which overrides the
client settings for that call. Every list filter is optional.

```rust
use alguna::prelude::*;

async fn requests(client: &Alguna) -> Result<(), ApiError> {
    let request = CreateCustomerRequest {
        name: "Acme".to_string(),
        currency: "USD".to_string(),
        ..Default::default()
    };
    let customer = client.customers.create(&request, None).await?;
    println!("{}", customer.id);

    let filter = ProductsListQueryRequest {
        fee_type: Some(vec![ListProductsRequestFeeTypeItem::Metered]),
        ..Default::default()
    };
    let options = RequestOptions::new().api_version("2026-04-01");
    for product in client.products.list(&filter, Some(options)).await?.data {
        println!("{}", product.name);
    }
    Ok(())
}
```

## Pagination

`list` returns one page. `list_paginated` returns a `Stream` of every item. It loads the
next page when the stream needs it, from `offset` (default 0). Add `futures = "0.3"` for
`StreamExt`.

```rust
use alguna::prelude::*;
use futures::StreamExt;

async fn every_customer(client: &Alguna) -> Result<(), ApiError> {
    let mut customers = client
        .customers
        .list_paginated(&CustomersListQueryRequest::default(), None)
        .await?;
    while let Some(customer) = customers.next().await {
        let customer: CustomerResponse = customer?;
        println!("{}", customer.name);
    }
    Ok(())
}
```

## Idempotency and retries

Every request other than a GET carries an `Idempotency-Key` header. The SDK makes a
UUID v4 key for each call and sends the same key on every retry of that call, so the
API acts once. To make a call safe to repeat later, for example after your process
stops, set your own key:

```rust
use alguna::prelude::*;

async fn create_once(client: &Alguna, request: &CreateCustomerRequest) -> Result<(), ApiError> {
    let options = RequestOptions::new().idempotency_key("create-customer-acme");
    client.customers.create(request, Some(options)).await?;
    Ok(())
}
```

`client.events.ingest`, `client.credits.check` and `client.credits.track` are different.
The API identifies their work by the request body, so the SDK makes no key for them and
still retries them. The API removes duplicate events by `unique_id`, and
`credits.track` draws credit once for the same `unique_id`s. A key replaces that
identity on `credits.track`, so a new key on each call would make a redelivered batch
draw credit twice. A key that you set is sent on every attempt. `credits.check` with
`reserve` needs your key, because the key identifies the hold.

By default the SDK retries a call 2 times after:

- a network error: the connection is refused, reset or closed before the response, or
  the attempt times out;
- a 408, 429, 502, 503 or 504 response;
- a 409 response with `Retry-After`. The API sends it while a request with the same
  idempotency key is still running. Any other 409 is a conflict, and the SDK does not
  retry it.

A retry waits 1 second, then 2 seconds, and so on, each ±10%. If the response has
`Retry-After` (seconds or an HTTP date), the retry waits that long, up to 60 seconds.
Set `max_retries` on the builder or on `RequestOptions`. 0 turns retries off.

To stop a call, drop its future, for example with `tokio::time::timeout`. A dropped
call makes no more attempts.

## Timeouts

`timeout` on the builder limits each attempt. The default is 60 seconds.
`timeout_seconds` on `RequestOptions` replaces it for one call. A custom
`reqwest_client` keeps its own timeout.

```rust
use alguna::prelude::*;
use alguna::ApiClientBuilder;
use std::time::Duration;

async fn tuned() -> Result<(), ApiError> {
    let client = ApiClientBuilder::default()
        .timeout(Duration::from_secs(30))
        .max_retries(4)
        .build()?;
    let options = RequestOptions::new().timeout_seconds(10).max_retries(0);
    client.customers.get("cus_123", Some(options)).await?;
    Ok(())
}
```

## Errors

Every call returns `Result<_, ApiError>`. The enum is `#[non_exhaustive]`, so keep a
wildcard arm. An error prints the API's reason: the body's `detail`, else its `title`,
else its `message`, else the HTTP status text. For example:
`NotFoundError: Resource not found - customer cus_1 not found`.

```rust
use alguna::prelude::*;

async fn find(client: &Alguna, id: &str) -> Result<Option<CustomerResponse>, ApiError> {
    match client.customers.get(id, None).await {
        Ok(customer) => Ok(Some(customer)),
        Err(ApiError::NotFoundError { .. }) => Ok(None),
        Err(error) => {
            // status(): the HTTP status, if the API answered. detail(): its reason.
            eprintln!(
                "{:?} {:?} timeout={}",
                error.status(),
                error.detail(),
                error.is_timeout()
            );
            Err(error)
        }
    }
}
```

A network failure is `ApiError::Network`. Its cause is the error's `source()`.

## Webhooks

Alguna sends webhooks through Svix. Verify the signature before you trust a payload:
give the raw request body and its `svix-id`, `svix-timestamp` and `svix-signature`
headers to the [`svix`](https://docs.rs/svix) crate
(`Webhook::new(secret)?.verify(body, &headers)?`). Then read the event. Each event
`type` has a payload type in this crate, such as `WebhookPaymentCreatedPayload` for
`payment.created`. The example also needs `serde_json`.

```rust
use alguna::prelude::*;

// `body` is the raw request body, after its signature is verified.
fn handle(body: &[u8]) -> Result<(), serde_json::Error> {
    let event: Value = serde_json::from_slice(body)?;
    match event["type"].as_str() {
        Some("payment.created") => {
            let event: WebhookPaymentCreatedPayload = serde_json::from_value(event)?;
            println!("payment {}", event.data.amount);
        }
        Some("invoice.paid") => {
            let event: WebhookInvoicePaidPayload = serde_json::from_value(event)?;
            println!("invoice {} paid", event.data.id);
        }
        // Alguna can add event types before you upgrade.
        _ => {}
    }
    Ok(())
}
```

## Reference

Every endpoint and type is in [reference.md](reference.md). The API documentation is at
[alguna.com/docs](https://alguna.com/docs).

## About this repo

Alguna generates this crate from the Alguna API spec in a private repository. Open
issues here. Alguna ports code changes to that repository, so a pull request here is
not merged as it is.