wi-api 0.2.0

Official Rust SDK for the wi-api WhatsApp platform
Documentation

wi-api Rust SDK

Crates.io docs.rs license

Rust SDK for wi-api. Async, fully typed, no OpenSSL dependency.

Install

[dependencies]
wi-api = "0.1"
tokio = { version = "1", features = ["full"] }

Or with cargo-add:

cargo add wi-api

Quick start

use wi_api::{Wi, types::SendTextParams};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let wi = Wi::new(std::env::var("WI_API_KEY")?);

    let msg = wi
        .session("my-instance")
        .send_text(SendTextParams {
            to: "5511999999999".into(),
            text: "Hello from wi-api".into(),
            ..Default::default()
        })
        .await?;

    println!("sent: {}", msg.message_id);
    Ok(())
}

Sessions

let session = wi.session("my-instance");

// Start connection flow
session.connect().await?;

// Get QR code (base64 PNG)
let qr = session.qr().await?;
println!("{}", qr.qr.unwrap_or_default());

// Pair by phone number
let result = session.pair_phone("5511999999999").await?;
println!("{}", result.pair_code.unwrap_or_default());

// Check status
let status = session.status().await?;
println!("connected={} phone={:?}", status.connected, status.phone);

session.disconnect().await?;
session.logout().await?;

Sending messages

use wi_api::types::*;

session.send_text(SendTextParams {
    to: "5511999999999".into(),
    text: "Hello!".into(),
    ..Default::default()
}).await?;

session.send_image(SendImageParams {
    to: "5511999999999".into(),
    url: "https://example.com/photo.jpg".into(),
    caption: Some("Look at this".into()),
    ..Default::default()
}).await?;

session.send_audio(SendAudioParams {
    to: "5511999999999".into(),
    url: "https://example.com/audio.ogg".into(),
    ptt: Some(true),
    ..Default::default()
}).await?;

session.send_document(SendDocumentParams {
    to: "5511999999999".into(),
    url: "https://example.com/report.pdf".into(),
    filename: Some("report.pdf".into()),
    ..Default::default()
}).await?;

session.send_location(SendLocationParams {
    to: "5511999999999".into(),
    latitude: -23.5505,
    longitude: -46.6333,
    title: Some("São Paulo".into()),
    ..Default::default()
}).await?;

session.react(SendReactionParams {
    to: "5511999999999".into(),
    message_id: message_id.into(),
    emoji: "".into(),
    ..Default::default()
}).await?;

Webhooks

use wi_api::webhook::{parse_event, verify_webhook, VerifyOptions};

// Axum handler
async fn webhook(
    headers: axum::http::HeaderMap,
    body: axum::body::Bytes,
) -> axum::response::Response {
    use axum::{http::StatusCode, response::IntoResponse};

    // x-wi-signature-v2 (HMAC of "<timestamp>.<body>"), 5 minute replay tolerance.
    let pairs: Vec<(&str, &str)> = headers
        .iter()
        .filter_map(|(k, v)| Some((k.as_str(), v.to_str().ok()?)))
        .collect();

    let secret = std::env::var("WI_WEBHOOK_SECRET").unwrap();

    if !verify_webhook(&body, &pairs, secret.as_bytes(), VerifyOptions::default()) {
        return (StatusCode::UNAUTHORIZED, "invalid signature").into_response();
    }

    let event = parse_event(&body).unwrap();
    println!("event: {} session: {}", event.event, event.session_id);

    StatusCode::NO_CONTENT.into_response()
}

Using WebhookPayload:

use wi_api::webhook::WebhookPayload;

let secret = std::env::var("WI_WEBHOOK_SECRET").unwrap();
match WebhookPayload::from_parts(body, sig, secret.as_bytes()) {
    None => StatusCode::UNAUTHORIZED.into_response(),
    Some(Err(e)) => (StatusCode::BAD_REQUEST, e.to_string()).into_response(),
    Some(Ok(p)) => {
        println!("event: {}", p.event.event);
        StatusCode::NO_CONTENT.into_response()
    }
}

Error handling

use wi_api::error::WiError;

match session.send_text(params).await {
    Ok(msg) => println!("sent: {}", msg.message_id),
    Err(WiError::Api { status, message, .. }) => eprintln!("API {status}: {message}"),
    Err(WiError::Request(e)) => eprintln!("network: {e}"),
    Err(e) => eprintln!("{e}"),
}

Configuration

use std::time::Duration;

let wi = Wi::builder(api_key)
    .base_url("https://endpoint.wi.api.br")
    .timeout(Duration::from_secs(10))
    .build();

Resources