# Client

HMAC-signed client over `reqwest`. Mirrors `BillingClient` from the JS SDK and the Go SDK's `*Client`. Targets every authenticated endpoint of the billing API plus a small set of public ones (`public_license_keys`, `github_app_info`, `latest_release`, `plans`).

```rust
use akira_billing::{Client, Error};
use akira_billing::types::*;
```

> Server / desktop only. The `product_secret` is the trust root — see [03-architecture](03-architecture.md).

## Construction

```rust
let mut client = Client::new(
    "https://billing.akira.foundation",   // base_url
    "unified-dev",                        // product_slug
    std::env::var("AKIRA_BILLING_SECRET")?,
);
```

`Client::new` creates a fresh `reqwest::Client`. Reuse the value across requests — `Client: Clone` shares the underlying connection pool.

## Token management

| Method | Purpose |
|--------|---------|
| `set_customer_token(impl Into<String>)` | Replace the cached bearer (e.g. after refresh) |
| `clear_customer_token()` | Drop the bearer (logout) |
| `base_url() -> &str` | Normalized base URL |
| `product_slug() -> &str` | Product slug |
| `customer_token() -> Option<&str>` | Current bearer (for inspection) |

These four mutating accessors take `&mut self`; the readers take `&self`. Wrap in `Arc<RwLock<Client>>` for cross-task token rotation, or clone the client per task.

## Authentication

| Method | Endpoint |
|--------|----------|
| `request_otp(payload)` | `POST /api/auth/customer/otp/request` |
| `verify_otp(payload)` | `POST /api/auth/customer/otp/verify` *(stores bearer)* |
| `exchange_oauth_code(payload)` | `POST /api/auth/oauth/exchange` *(stores bearer)* |
| `list_oauth_providers(product)` | `GET /api/v1/products/{product}/auth/providers` |

`verify_otp` and `exchange_oauth_code` mutate `self.customer_token` on success — they require `&mut self`.

## Customer profile

| Method | Endpoint |
|--------|----------|
| `customer_me()` | `GET /api/me` |
| `entitlements()` | `GET /api/me/entitlements` |
| `customer_features(product)` | `GET /api/me/features/{product}` |
| `billing_portal(return_url)` | `GET /api/billing/portal?return_url=...` |

## Licensing

| Method | Endpoint |
|--------|----------|
| `license_check(payload)` | `POST /api/licenses/check` |
| `license_activate(payload)` | `POST /api/licenses/activate` |
| `license_refresh(payload)` | `POST /api/licenses/refresh` |
| `license_sync_usage(payload)` | `POST /api/licenses/sync-usage` |
| `public_license_keys()` | `GET /api/v1/license-keys/public` *(unsigned)* |

## Usage tracking

| Method | Endpoint |
|--------|----------|
| `track_usage(payload)` | `POST /api/me/usage` |
| `track_anonymous_usage(payload)` | `POST /api/v1/usage/anonymous` |

`UsagePayload::action` is `"check"` for pre-flight and `"increment"` after the work completes. `count` is variable.

## Downloads + releases

| Method | Endpoint |
|--------|----------|
| `latest_release(channel)` | `GET /api/v1/downloads/{product}/releases/{channel}/latest` |
| `issue_download(channel, platform)` | `GET /api/v1/downloads/{product}/{channel}/{platform}` *(Accept: application/json)* |
| `complete_download(beacon_url)` | `POST {beacon_url}` *(unsigned, fires the completion beacon)* |
| `plans()` | `GET /api/v1/products/{product}/plans` *(unsigned)* |
| `start_trial(plan_key)` | `POST /api/v1/products/{product}/trial` |

`platform` is `"os-arch"` (`macos-arm64`, `linux-x86_64`, …) matching `AssetPlatform` in the JS SDK.

## GitHub integration

| Method | Endpoint |
|--------|----------|
| `github_installation_token(payload)` | `POST /api/me/github/installation-token` |
| `github_user_token()` | `GET /api/me/github/user-token` |
| `me_github_installations()` | `GET /api/me/github/installations` |
| `github_app_info()` | `GET /api/v1/github/app` *(unsigned)* |

## Signed vs unsigned

Internally each method picks the path:

- **Signed** — adds the four `X-Akira-*` headers plus optional `Authorization: Bearer …` when `customer_token` is set.
- **Unsigned** — only `Accept` + `Content-Type` headers. Used for public endpoints (license public keys, GitHub App info, plans, latest release, completion beacon).

The HTTP method, path, body bytes, and timestamp build the canonical string at signing time (see [11-signature](11-signature.md)).

## Error handling

Every method returns `Result<T, Error>`. Branch on `Error::Api { status, code }` for billing-server-emitted failures:

```rust
match client.license_activate(payload).await {
    Ok(res) => { /* … */ }
    Err(akira_billing::Error::Api { status: 402, code }) if code == "no_active_plan" => {
        redirect_to_upgrade();
    }
    Err(err) => return Err(err.into()),
}
```

See [12-errors](12-errors.md) for the full catalogue.

## Custom HTTP client

The current `Client::new` always constructs a fresh `reqwest::Client`. For custom timeouts, mTLS, or proxies, build the inner client and replace via the (unstable) `with_http` constructor (planned). Until then, wrap calls with a layer (Tower, custom middleware) on the host application.

## Cross-SDK parity

`Client` mirrors the JS `BillingClient` and the Go `Client`. Method names follow Rust convention (`snake_case`) but the endpoints, payload shapes, and signature semantics are identical.

---

Navigation: [← Architecture](03-architecture.md) · **Client** · [Signature →](11-signature.md)
