Async Rust client for the voip.ms REST API.
Every API method has a typed *Params request struct and a method on
Client that
deserializes the response into a typed *Response struct. A *_raw variant
returning serde_json::Value is available on every method as an escape
hatch.
[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"] }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.
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 typed deserialization into aSomeMethodResponsestruct, orclient.some_method_raw(...)for aserde_json::Valueenvelope.
All fields on both *Params and *Response structs are Option<T>, so
you only fill in what you need and unknown omissions never fail
deserialization. Consult the
voip.ms API documentation for which
parameters each method actually requires.
use voip_ms::{Client, SendSmsParams};
#[tokio::main]
async fn main() -> voip_ms::Result<()> {
let client = Client::new("you@example.com", "your-api-password");
let resp = client
.send_sms(&SendSmsParams {
did: Some("5551234567".into()),
dst: Some("5557654321".into()),
message: Some("Hello from Rust".into()),
..Default::default()
})
.await?;
println!("{resp:#?}");
Ok(())
}use voip_ms::{Client, GetBalanceParams};
#[tokio::main]
async fn main() -> voip_ms::Result<()> {
let client = Client::new("you@example.com", "your-api-password");
let resp = client
.get_balance(&GetBalanceParams { advanced: Some(true) })
.await?;
if let Some(balance) = resp.balance.as_ref() {
println!("{}", balance.current_balance.unwrap_or_default());
}
Ok(())
}When you only want one nested field, use
Client::call_at
with a JSON pointer and your own type:
use serde::Deserialize;
use voip_ms::{Client, GetDIDsInfoParams};
#[derive(Debug, Deserialize)]
struct Did {
did: String,
}
#[tokio::main]
async fn main() -> voip_ms::Result<()> {
let client = Client::new("you@example.com", "your-api-password");
let dids: Vec<Did> = client
.call_at("getDIDsInfo", &GetDIDsInfoParams::default(), "/dids")
.await?;
println!("DID count: {}", dids.len());
Ok(())
}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();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_balanceVOIP_MS_USERNAME=you@example.com \
VOIP_MS_PASSWORD=your-api-password \
cargo run --example list_didsVOIP_MS_USERNAME=you@example.com \
VOIP_MS_PASSWORD=your-api-password \
VOIP_MS_FROM_DID=5551234567 \
VOIP_MS_TO=5557654321 \
VOIP_MS_MESSAGE="Hello from Rust" \
cargo run --example send_smssend_sms requires a DID with SMS enabled. You can pass the message body
either through VOIP_MS_MESSAGE or as the first argument after --.
If voip.ms adds an API method that isn't yet exposed as a typed call, use
Client::call_raw
directly with any serde-serializable parameter set:
use voip_ms::Client;
#[tokio::main]
async fn main() -> voip_ms::Result<()> {
let client = Client::new("you@example.com", "your-api-password");
let resp = client
.call_raw("someBrandNewMethod", &serde_json::json!({ "id": 42 }))
.await?;
println!("{resp:#?}");
Ok(())
}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 thestatusfield was something other thansuccess(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. missingstatusfield).
Contributor and maintainer workflows (regeneration, verification, and release) are documented in DEVELOPMENT.md.
See AGENTS.md for design decisions and project-specific guidance.
Licensed under the MIT license.