voip-ms
Async Rust client for the VoIP.ms REST API.
The goal is an idiomatic, ergonomic Rust surface over an API that is itself
inconsistent: fields that are really booleans, durations, enums, or routing
targets arrive on the wire as strings (1/0, yes/no, none,
account:100001_VoIP, …), and this crate evens that out into real Rust types
so callers don't have to decode the wire encoding by hand.
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.
Installation
[]
= "0.3"
= { = "1", = ["macros", "rt-multi-thread"] }
By default the crate enables rustls verifying against the OS trust store. To
use the platform's native TLS stack instead:
= { = "0.3", = false, = ["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 ;
async
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 ;
async
Reading typed responses
use ;
async
Picking a nested field with a JSON pointer
When you only want one nested field, use
Client::call_at
with a JSON pointer and your own type:
use Deserialize;
use ;
async
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 Duration;
use Client;
let http = builder
.timeout
.build
.unwrap;
let client = builder
.http_client
.build
.unwrap;
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 \
VOIP_MS_USERNAME=you@example.com \
VOIP_MS_PASSWORD=your-api-password \
VOIP_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" \
send_sms requires a DID with SMS enabled. You can pass the message body
either through VOIP_MS_MESSAGE or as the first argument after --.
Calling a method that isn't in this crate yet
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 Client;
async
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 thestatusfield was something other thansuccess.ApiStatusis an enum with a variant per documented code (ApiStatus::InvalidCredentials,ApiStatus::APINotEnabled, …) for ergonomic match arms, plus anApiStatus::Unknown(String)catch-all that preserves any code VoIP.ms hasn't documented.ApiStatus::description()returns the documented human-readable meaning (orNoneforUnknown),as_str()gives the verbatim wire string, andis_documented()reports whether it's a known variant.One exception: VoIP.ms returns a distinct
no_*status per list method when the collection is empty (no_sms,no_cdr,no_messages, …). The typed methods treat such a status (ApiStatus::is_empty()) as a successful empty response -- the collection field comes backNonerather thanErr-- so you don't pattern-match a "no SMS" code where an empty list is the natural answer. The*_rawmethods keep the verbatim contract and still surface it asError::Api.match client.get_balance.await -
Error::InvalidResponse— the response was not the expected JSON envelope (e.g. missingstatusfield).
Development and release
Contributor and maintainer workflows (regeneration, verification, and release) are documented in DEVELOPMENT.md.
See AGENTS.md for design decisions and project-specific guidance.
License
Licensed under the MIT license.