Skip to content

voicetel/rust-sdk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

4 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“ž VoiceTel Rust SDK

The official Rust client for the VoiceTel REST API β€” provision numbers, place orders, validate e911, send messages, and manage your account, all with strongly-typed serde models and an async-first ergonomics on top of [reqwest].

Version Rust License Coverage docs.rs

πŸ“š Table of Contents

✨ Features

πŸ›‘οΈ Strongly Typed End-to-End

  • #[derive(Serialize, Deserialize)] on every model β€” 73 operations across 10 resources, no serde_json::Value smuggling.
  • Option<T> + skip_serializing_if on optional request fields β€” distinguish "not set" from "zero" cleanly when PATCH-ing.
  • Option<T> for nullable response fields β€” forward_to: Option<String> lets you tell apart "no forward configured" from an empty destination.
  • Wire-name aware β€” fromNumber, localRoutingNumber, rateCenterTier, ticketNumber and friends are all surfaced under idiomatic snake_case Rust names via #[serde(rename = ...)].

πŸ” Production-Grade Transport

  • Built on reqwest with rustls-tls, json, and gzip features β€” no OpenSSL surprises.
  • Automatic retry with exponential backoff on 429 / 5xx β€” honours Retry-After headers, capped at 8s. Configurable via Client::builder().max_retries(n).
  • Configurable timeout per client (defaults to 30s).
  • Bearer auth managed for you; the passwordβ†’key exchange is one method call (Client::login).
  • Structured ApiError with typed kind: ErrorKind so you can match err.kind { ErrorKind::RateLimit => ... } without parsing HTTP status codes.
  • Envelope unwrap β€” {"status":"success","data": ...} is stripped before the response hits your code.

πŸ“ž Complete API Coverage

  • Numbers β€” list, get, add, remove, route, translate, CNAM, LIDB, fax, forward, SMS, messaging campaigns, port-out PIN, account moves.
  • Account β€” profile, sub-accounts, CDRs, credits, payments, MRC, registration, password recovery.
  • e911 β€” record provisioning, address validation, lookup, removal.
  • Gateways β€” list, create, update, delete, view bound numbers.
  • Messaging β€” SMS & MMS sending, message history, 10DLC brand and campaign registration, per-number messaging state.
  • Lookups β€” CNAM and LRN dips.
  • iNumbering β€” inventory search, coverage queries, number orders, port-in submissions, port-out availability (with v2.2.10 localRoutingNumber / rateCenterTier).
  • Support β€” ticket create / read / update / delete, threaded messages, replies.
  • ACL β€” IP allowlist management with structured 409 conflict bodies (AclConflictData).
  • Authentication β€” switch between Digest, IP-only, or hybrid modes; rotate passwords.

πŸ§ͺ Battle-Tested

  • 77 unit tests + 2 doc-tests + 2 integration tests via wiremock.
  • 93% line coverage (cargo-tarpaulin, llvm engine).
  • Every method's happy path and error path covered.
  • cargo fmt --check clean.
  • cargo clippy --all-targets -- -D warnings clean.

πŸ“¦ Clean Distribution

  • Single crate (voicetel); install with cargo add voicetel.
  • Minimal dependency surface: reqwest, serde, serde_json, tokio (time only), thiserror, url.

πŸš€ Installation

[dependencies]
voicetel = "2.2.10"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Or with cargo add:

cargo add voicetel
cargo add tokio --features macros,rt-multi-thread

Requires Rust 1.75 or later.

🏁 Quickstart

use std::time::Duration;
use voicetel::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder()
        .timeout(Duration::from_secs(30))
        .max_retries(2)
        .build()?;

    // Exchange username + password for an API key (one-time per session)
    client.login(1_000_000_001, "hunter2").await?;

    // Typed responses β€” your IDE knows what `me` is.
    let me = client.account().get().await?;
    println!("Balance: ${:.2}", me.cash.unwrap_or_default());

    // List your numbers
    let numbers = client.numbers().list().await?;
    for n in numbers.numbers {
        println!("{} route={} cnam={} sms={}", n.number, n.route, n.cnam, n.sms_enabled);
    }
    Ok(())
}

Or, if you already have an API key:

use voicetel::{Client, models::inumbering::CoverageQuery};

# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder().api_key("32hex...").build()?;
let q = CoverageQuery { state: Some("NJ".into()), ..Default::default() };
let coverage = client.i_numbering().coverage(&q).await?;
for bucket in coverage.coverage {
    println!("{:?}-{:?}: {} TNs", bucket.npa, bucket.nxx, bucket.count);
}
# Ok(()) }

πŸ”‘ Authentication

Every endpoint requires Authorization: Bearer <apikey> except POST /v2.2/account/api-key, which exchanges username + password for a fresh 32-hex key. Client::login() handles the exchange and installs the returned key on the transport.

Re-fetch the API key after any password change β€” the old one is invalidated.

Don't have credentials yet? Get them at voicetel.com/docs/api/v2.2/credentials.

# use voicetel::Client;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder().build()?;
let key = client.login(1_000_000_001, "hunter2").await?;
// `key` is the new 32-hex bearer; the client already has it installed.
# Ok(()) }

πŸ—ΊοΈ Resource Reference

Resource Method on Client Example
Account client.account() client.account().cdr(t1, t2).await?
ACL client.acl() client.acl().add(&body).await?
Authentication client.authentication() client.authentication().update(&body).await?
e911 client.e911() client.e911().validate(&body).await?
Gateways client.gateways() client.gateways().list().await?
iNumbering client.i_numbering() client.i_numbering().search_inventory(&q).await?
Lookups client.lookups() client.lookups().lrn("2015551234", "2012548000").await?
Messaging client.messaging() client.messaging().send(&body).await?
Numbers client.numbers() client.numbers().assign_campaign("2015551234", &body).await?
Support client.support() client.support().create(&body).await?

Optional request fields are Option<T> β€” use Some(...) or the ..Default::default() syntax to leave them unset:

use voicetel::models::account::AccountPutRequest;

let body = AccountPutRequest {
    timezone: Some("America/Chicago".into()),
    notify_threshold: Some(5),
    notify: Some(true),
    ..Default::default()
};

🚨 Error Handling

All HTTP errors return an ApiError. Inspect kind or use the boolean helpers:

ErrorKind HTTP status
BadRequest 400
Authentication 401
PermissionDenied 403
NotFound 404
Conflict 409
RateLimit 429
Server 5xx
Transport transport-level (DNS/TLS/timeout)
Serialization JSON decode failure
Unknown anything else
# use voicetel::{Client, ErrorKind};
# async fn run(client: Client) -> Result<(), Box<dyn std::error::Error>> {
match client.numbers().get("9999999999").await {
    Ok(n) => println!("{:?}", n),
    Err(e) if e.is_not_found() => eprintln!("not on this account"),
    Err(e) if e.kind == ErrorKind::RateLimit => eprintln!("slow down"),
    Err(e) => return Err(e.into()),
}
# Ok(()) }

⏱️ Cancellation and Timeouts

Cancellation works through Tokio's normal mechanisms β€” wrap the call in tokio::time::timeout(...) for a per-call deadline:

use std::time::Duration;
use tokio::time::timeout;

# use voicetel::Client;
# async fn run(client: Client) -> Result<(), Box<dyn std::error::Error>> {
let me = timeout(Duration::from_secs(5), client.account().get()).await??;
# Ok(()) }

For a global per-request timeout, set it on the client:

# use std::time::Duration;
# use voicetel::Client;
# fn run() -> Result<Client, Box<dyn std::error::Error>> {
let client = Client::builder()
    .timeout(Duration::from_secs(45))
    .build()?;
# Ok(client) }

Or supply a fully-configured reqwest::Client (e.g. with a connection pool, proxy, or instrumentation):

# use std::time::Duration;
# use voicetel::Client;
# fn run() -> Result<Client, Box<dyn std::error::Error>> {
let http = reqwest::Client::builder()
    .timeout(Duration::from_secs(45))
    .pool_max_idle_per_host(10)
    .build()?;
let client = Client::builder().http_client(http).build()?;
# Ok(client) }

⏱️ Rate Limits

These endpoints are limited to 6 requests per hour per IP:

  • account/info (AccountService::get)
  • account/cdr (AccountService::cdr)
  • account/recurring-charges (AccountService::recurring_charges)
  • account/payments (AccountService::payments)
  • account/registration (AccountService::registration)
  • account/api-key (Client::login)

The SDK automatically retries 429 responses with Retry-After honoured, up to max_retries(n) (default 2):

# use std::time::Duration;
# use voicetel::Client;
# fn run() -> Result<Client, Box<dyn std::error::Error>> {
let client = Client::builder()
    .api_key("32hex...")
    .max_retries(4)
    .timeout(Duration::from_secs(60))
    .build()?;
# Ok(client) }

πŸ› οΈ Development

git clone https://github.com/voicetel/rust-sdk
cd rust-sdk

# Run unit tests
cargo test

# Lint
cargo fmt --check
cargo clippy --all-targets -- -D warnings

# Build docs
cargo doc --no-deps --open

# Coverage
cargo install cargo-tarpaulin
cargo tarpaulin --out Xml

# Integration tests (live, read-only β€” needs credentials)
export VOICETEL_USERNAME=...
export VOICETEL_PASSWORD=...
cargo test --test integration -- --ignored

πŸ“– API Documentation

πŸ™Œ Contributors

Contributions welcome. Open an issue describing the change, or send a pull request against main.

πŸ’– Sponsors

Sponsor Contribution
VoiceTel Communications Primary development and production hosting

πŸ“„ License

This project is licensed under the MIT License β€” see the LICENSE file for details.

About

Official Rust SDK for the VoiceTel REST API

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages