On this page
Rust
Last updated
Send vTilt events from Rust code (Axum, Actix, Rocket, plain Tokio) by POSTing to the ingest API.
There's no native Rust SDK yet, but vTilt's wire format is a plain JSON POST so any Rust code can emit events. The pattern below uses reqwest + serde_json; the same shape works inside Axum, Actix, Rocket, Tower, and Tokio binaries.
#1. Add dependencies
# Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tracing = "0.1"
once_cell = "1"toml
#2. Capture client
A OnceCell singleton built on reqwest::Client. Captures are spawned onto the runtime as fire-and-forget tasks so handlers never block on the HTTP call.
// src/vtilt.rs
use once_cell::sync::OnceCell;
use serde::Serialize;
use serde_json::Value;
use std::env;
use std::time::Duration;
#[derive(Serialize)]
struct Event<'a> {
api_key: &'a str,
event: &'a str,
distinct_id: &'a str,
properties: Value,
}
struct Vtilt {
client: reqwest::Client,
token: String,
host: String,
}
static VTILT: OnceCell<Vtilt> = OnceCell::new();
fn vtilt() -> &'static Vtilt {
VTILT.get_or_init(|| Vtilt {
client: reqwest::Client::builder()
.timeout(Duration::from_secs(2))
.build()
.expect("vtilt client"),
token: env::var("VTILT_TOKEN").expect("VTILT_TOKEN missing"),
host: env::var("VTILT_HOST").unwrap_or_else(|_| "https://www.vtilt.com".to_owned()),
})
}
pub fn capture(distinct_id: impl Into<String>, event: impl Into<String>, properties: Value) {
let distinct_id = distinct_id.into();
let event = event.into();
tokio::spawn(async move {
let v = vtilt();
let body = Event {
api_key: &v.token,
event: &event,
distinct_id: &distinct_id,
properties,
};
if let Err(e) = v.client.post(format!("{}/api/e", v.host)).json(&body).send().await {
tracing::warn!(?e, "vtilt capture failed");
}
});
}
pub fn identify(distinct_id: impl Into<String>, properties: Value) {
capture(distinct_id, "$identify", serde_json::json!({ "$set": properties }));
}rust
#3. Capture events
use serde_json::json;
vtilt::identify("user_42", json!({ "email": "alice@example.com", "plan": "pro" }));
vtilt::capture("user_42", "purchase_completed", json!({
"amount": 99.99,
"currency": "USD",
}));rust
#4. Axum handler
use axum::{Json, response::Json as JsonResponse};
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct Checkout { user_id: String, amount: f64 }
async fn checkout(Json(req): Json<Checkout>) -> JsonResponse<serde_json::Value> {
vtilt::capture(req.user_id, "purchase_completed", json!({ "amount": req.amount }));
JsonResponse(json!({ "ok": true }))
}rust
#Next steps
- Identify & alias — full identification model.
- Event forwarding — fan events out to GA4, Meta CAPI, PostHog.
- Reverse proxy — block-resistant ingestion.
- Common mistakes — integration pitfalls to avoid before you ship.