voip-ms 0.1.0

Async client for the voip.ms REST API
Documentation

voip-ms

Crates.io Docs.rs CI License: MIT

Async client for the voip.ms REST API.

A thin, idiomatic Rust wrapper around every method exposed by the voip.ms REST endpoint (https://voip.ms/api/v1/rest.php). Each WSDL operation gets a typed *Params request struct and a method on [Client]; responses come back as serde_json::Value so callers can pick the fields they need or [serde_json::from_value] into a struct of their own.

Installation

[dependencies]
voip-ms = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

By default the crate enables rustls with system root certificates. To use a different TLS backend:

# Embed Mozilla's roots (good for scratch/distroless images):
voip-ms = { version = "0.1", default-features = false, features = ["rustls-tls-webpki-roots"] }

# Use the platform's native TLS stack:
voip-ms = { version = "0.1", default-features = false, features = ["native-tls"] }

Authentication

voip.ms uses two pieces of credential, both of which you control entirely:

  • api_username — your account email.
  • api_password — a distinct password generated on the SOAP and REST/JSON API page in the voip.ms customer portal.

You must also allow-list the source IP address(es) you'll be calling from on that same page. This crate does not load credentials from the environment, files, or any other source — pass them when you construct the [Client].

Usage

use voip_ms::{Client, GetBalanceParams};

#[tokio::main]
async fn main() -> voip_ms::Result<()> {
    let client = Client::new("you@example.com", "your-api-password");

    let balance = client
        .get_balance(&GetBalanceParams { advanced: Some(true) })
        .await?;
    println!("{balance:#}");
    Ok(())
}

Every API method follows the same pattern: construct a *Params struct (every field is Option<T> and omitted from the request when None), then call either:

  • client.some_method(...) for a raw serde_json::Value, or
  • client.some_method_typed::<T>(...) for typed deserialization.
use voip_ms::{Client, SendSmsParams};

# async fn run(client: voip_ms::Client) -> voip_ms::Result<()> {
let resp = client
    .send_sms(&SendSmsParams {
        did: Some("5551234567".into()),
        dst: Some("5557654321".into()),
        message: Some("Hello from Rust".into()),
        ..Default::default()
    })
    .await?;
# Ok(()) }

Customizing the HTTP client

Use [Client::builder] to plug in your own reqwest::Client — for proxies, custom timeouts, retry middleware, or anything else you'd configure on reqwest directly.

use std::time::Duration;
use voip_ms::Client;

let http = reqwest::Client::builder()
    .timeout(Duration::from_secs(30))
    .build()
    .unwrap();

let client = Client::builder("you@example.com", "api-password")
    .http_client(http)
    .build()
    .unwrap();

Typed responses

The WSDL doesn't describe response shapes (all 222 operations declare the same generic arrayResponse), so this crate intentionally hands back serde_json::Value by default. To reduce boilerplate, every generated method also has a typed variant named *_typed:

use voip_ms::{Client, GetBalanceParams, GetBalanceResponse};

# async fn run(client: Client) -> voip_ms::Result<()> {
let resp: GetBalanceResponse = client
    .get_balance_typed(&GetBalanceParams { advanced: Some(true) })
    .await?;
println!("{}", resp.balance.current_balance);
# Ok(()) }

For methods where you only want a nested field, use [Client::call_typed_at] with a JSON pointer:

use serde::Deserialize;
use voip_ms::{Client, GetDidsInfoParams};

#[derive(Debug, Deserialize)]
struct Did {
    did: String,
}

# async fn run(client: Client) -> voip_ms::Result<()> {
let dids: Vec<Did> = client
    .call_typed_at("getDIDsInfo", &GetDidsInfoParams::default(), "/dids")
    .await?;
# Ok(()) }

The crate also includes starter partial response types that keep unknown fields in extra, such as GetBalanceResponse and GetDidsInfoResponse.

Running the examples

The examples/ directory contains small runnable programs that read credentials from VOIP_MS_USERNAME and VOIP_MS_PASSWORD:

VOIP_MS_USERNAME=you@example.com \
VOIP_MS_PASSWORD=your-api-password \
    cargo run --example get_balance

Available examples: get_balance, list_dids, send_sms.

Calling methods this crate hasn't been regenerated for

If voip.ms adds an API method that isn't yet in this crate, use [Client::call] directly with a serde-serializable parameter set:

# async fn run(client: voip_ms::Client) -> voip_ms::Result<()> {
let resp = client
    .call("someBrandNewMethod", &serde_json::json!({ "id": 42 }))
    .await?;
# Ok(()) }

Error model

All errors surface through [voip_ms::Error]. The three variants are:

  • Error::Http — the request failed at the transport or HTTP-status level.
  • Error::Api(ApiStatus) — the response was a well-formed JSON envelope but the status field was something other than success (e.g. invalid_credentials, missing_method, api_not_enabled). The wire string is exposed verbatim — the set of values is per-method and not stable, so consult the voip.ms documentation for the methods you use.
  • Error::InvalidResponse — the response was not the expected JSON envelope (e.g. missing status field).

Regenerating the API surface

The 222 typed request structs and Client methods are generated from tools/server.wsdl by the xtask workspace member (xtask/src/main.rs). To pick up new methods after voip.ms updates the WSDL:

# Download the latest WSDL from voip.ms:
curl -o tools/server.wsdl https://voip.ms/api/v1/server.wsdl

# Then regenerate and verify:
cargo xtask gen
cargo fmt --all
cargo clippy --all -- -D warnings
cargo test

See AGENTS.md for the design notes behind the generator.

Releasing

Publishing is automated via .github/workflows/release.yaml.

  1. Ensure Cargo.toml has the target version (for the first release: 0.1.0).
  2. Move release notes from Unreleased into a versioned section in CHANGELOG.md.
  3. Push a tag in the form vX.Y.Z.
git tag v0.1.0
git push origin v0.1.0

On tag push, the workflow verifies the tag version matches Cargo.toml, runs fmt/clippy/tests, performs cargo publish --dry-run, publishes to crates.io using CRATES_IO_TOKEN, and creates a GitHub release.

License

Licensed under the MIT license.