Invoance Rust SDK
Official Rust SDK for the Invoance compliance API — cryptographic proof, document anchoring, and AI attestation.
Async, built on tokio and reqwest (rustls — no OpenSSL).
Install
[dependencies]
invoance = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
MSRV: 1.86 (driven by transitive dependencies of reqwest).
Quick start
Set your API key:
export INVOANCE_API_KEY=inv_live_...
use invoance::InvoanceClient;
use invoance::models::IngestEventParams;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = InvoanceClient::new()?;
let event = client
.events()
.ingest(IngestEventParams {
event_type: "policy.approval".into(),
payload: json!({ "policy_id": "pol_001", "decision": "approved" })
.as_object()
.unwrap()
.clone(),
..Default::default()
})
.await?;
println!("{}", event.event_id);
use invoance::models::{AnchorFileParams, FileSource};
let anchored = client
.documents()
.anchor_file(AnchorFileParams {
file: FileSource::Path("./invoice.pdf".into()),
document_ref: Some("Invoice #1042".into()),
event_type: None,
metadata: None,
idempotency_key: None,
skip_original: false,
trace_id: None,
})
.await?;
println!("{}", anchored.event_id);
use invoance::models::IngestAttestationParams;
let att = client
.attestations()
.ingest(IngestAttestationParams {
r#type: "output".into(),
input: "Summarize this contract".into(),
output: "The contract states...".into(),
model_provider: "openai".into(),
model_name: "gpt-4o".into(),
model_version: "2025-01-01".into(),
subject: json!({ "user_id": "u_42", "session_id": "sess_4f9a" })
.as_object()
.cloned(),
..Default::default()
})
.await?;
println!("{}", att.attestation_id);
Ok(())
}
Configuration
The client reads from environment variables automatically:
| Variable |
Required |
Default |
INVOANCE_API_KEY |
Yes |
— |
INVOANCE_BASE_URL |
No |
https://api.invoance.com |
Or configure explicitly with the builder:
use std::time::Duration;
use invoance::InvoanceClient;
let client = InvoanceClient::builder()
.api_key("inv_live_...")
.timeout(Duration::from_secs(60))
.build()?;
# Ok::<(), invoance::Error>(())
Quick validation
Sanity-check that your API key works before wiring the SDK into a larger app:
# async fn run(client: invoance::InvoanceClient) {
let result = client.validate().await;
if !result.valid {
panic!("Invoance: {:?} (base: {})", result.reason, result.base_url);
}
# }
validate() probes GET /v1/events?limit=1, never returns Err for the probe itself, and yields { valid, reason, base_url } — use it in health checks, startup scripts, or CI guards.
Error handling
Every fallible call returns Result<T, invoance::Error>. Classify with the boolean predicates or by matching the variants:
# async fn run(client: invoance::InvoanceClient, params: invoance::models::IngestEventParams) {
match client.events().ingest(params).await {
Ok(event) => println!("{}", event.event_id),
Err(e) if e.is_authentication() => eprintln!("bad API key"),
Err(e) if e.is_quota_exceeded() => {
eprintln!("rate limited, retry in {:?}s", e.retry_after_seconds());
}
Err(e) if e.is_timeout() => eprintln!("timed out"),
Err(e) => eprintln!("invoance error: {e}"),
}
# }
Accessors: status_code(), error_code(), retry_after_seconds(), body(), request(), kind(). Predicates: is_authentication(), is_forbidden(), is_not_found(), is_validation(), is_conflict(), is_quota_exceeded(), is_server(), is_network(), is_timeout().
Offline verification
The SDK reproduces the server's cryptography so you can verify proofs without trusting the API:
use invoance::verify_audit_event;
# fn run(event_json: serde_json::Value) {
let result = verify_audit_event(&event_json, None);
println!("valid = {} ({:?})", result.valid, result.reason);
# }
verify_audit_event — Ed25519 signature check over the invoance.audit/1 canonical bytes.
canonical_audit_bytes, payload_hash_hex, normalize_ts, AUDIT_SCHEMA_ID — the canonicalizer primitives.
content_idempotency_key(body) — derive a content-stable Idempotency-Key.
client.attestations().verify_signature(id) — verify an attestation's Ed25519 signature fully client-side.
Resources
Every resource is reached through an accessor on the client: client.events(),
client.documents(), client.attestations(), client.traces(),
client.audit(). Each method takes a typed param struct (most derive
Default, so pass only the fields you need and finish with
..Default::default()) and returns Result<T, invoance::Error>.
Import param structs from invoance::models. Payloads and metadata are
serde_json::Map<String, Value> — build them with serde_json::json! and
.as_object().unwrap().clone().
Events
client.events() — the compliance event ledger.
use invoance::models::{IngestEventParams, ListEventsParams, VerifyEventParams};
use serde_json::json;
# async fn run(client: invoance::InvoanceClient) -> Result<(), invoance::Error> {
let payload = json!({ "user_id": "u_42", "ip": "203.0.113.4" });
let event = client
.events()
.ingest(IngestEventParams {
event_type: "user.login".into(),
payload: payload.as_object().unwrap().clone(),
event_time: None, trace_id: None, idempotency_key: None, ..Default::default()
})
.await?;
println!("{} at {}", event.event_id, event.ingested_at);
let listed = client
.events()
.list(ListEventsParams {
limit: Some(50),
event_type: Some("user.login".into()),
..Default::default()
})
.await?;
println!("{} of {} (has_more={})", listed.events.len(), listed.total, listed.has_more);
let fetched = client.events().get(&event.event_id).await?;
println!("payload_hash = {}", fetched.payload_hash);
let verify = client
.events()
.verify(
&event.event_id,
VerifyEventParams {
payload: payload.as_object().cloned(),
..Default::default()
},
)
.await?;
println!("match_result = {}", verify.match_result);
# Ok(()) }
Documents
client.documents() — anchor document hashes (with optional original bytes).
use invoance::models::{
AnchorDocumentParams, AnchorFileParams, FileSource, VerifyDocumentParams,
};
use sha2::{Digest, Sha256};
# async fn run(client: invoance::InvoanceClient) -> Result<(), invoance::Error> {
let bytes = b"...your document bytes...";
let document_hash = hex::encode(Sha256::digest(bytes));
let doc = client
.documents()
.anchor(AnchorDocumentParams {
document_hash: document_hash.clone(),
document_ref: Some("Invoice #1042".into()),
event_type: Some("invoice".into()),
original_bytes_b64: None, metadata: None,
trace_id: None,
idempotency_key: None,
..Default::default()
})
.await?;
println!("anchored {} ({})", doc.event_id, doc.status);
let anchored = client
.documents()
.anchor_file(AnchorFileParams {
file: FileSource::Path("./invoice.pdf".into()), document_ref: None, event_type: Some("invoice".into()),
metadata: None,
idempotency_key: None,
skip_original: false, trace_id: None,
})
.await?;
println!("anchored via file helper {}", anchored.event_id);
let listed = client
.documents()
.list(invoance::models::ListDocumentsParams {
limit: Some(50),
document_ref: Some("Invoice".into()),
..Default::default()
})
.await?;
println!("{} documents", listed.total);
let detail = client.documents().get(&doc.event_id).await?;
println!("has_original = {}", detail.has_original);
let original: Vec<u8> = client.documents().get_original(&doc.event_id).await?;
std::fs::write("recovered.pdf", &original).ok();
let verify = client
.documents()
.verify(&doc.event_id, VerifyDocumentParams { document_hash })
.await?;
println!("match_result = {}", verify.match_result);
# Ok(()) }
AI Attestations
client.attestations() — anchor AI inputs/outputs with model context.
use invoance::models::{
IngestAttestationParams, ListAttestationsParams, VerifyAttestationParams,
};
use serde_json::json;
# async fn run(client: invoance::InvoanceClient) -> Result<(), Box<dyn std::error::Error>> {
let att = client
.attestations()
.ingest(IngestAttestationParams {
r#type: "output".into(),
input: "Summarize this contract".into(),
output: "The contract states...".into(),
model_provider: "openai".into(),
model_name: "gpt-4o".into(),
model_version: "2025-01-01".into(),
subject: json!({ "user_id": "u_42", "session_id": "sess_4f9a" })
.as_object()
.cloned(), trace_id: None,
idempotency_key: None,
..Default::default()
})
.await?;
println!("{} ({})", att.attestation_id, att.status);
let listed = client
.attestations()
.list(ListAttestationsParams {
limit: Some(50),
model_provider: Some("openai".into()),
attestation_type: Some("output".into()),
..Default::default()
})
.await?;
println!("{} attestations", listed.total);
let one = client.attestations().get(&att.attestation_id).await?;
println!("signature_alg = {}", one.signature_alg);
let verify = client
.attestations()
.verify(
&att.attestation_id,
VerifyAttestationParams { content_hash: "<64-hex sha256>".into() },
)
.await?;
println!("match_result = {}", verify.match_result);
let raw = client.attestations().get_raw(&att.attestation_id).await?;
let raw_string = serde_json::to_string(&raw)?;
let vp = client
.attestations()
.verify_payload(&att.attestation_id, raw_string.as_str())
.await?;
println!("payload match_result = {}", vp.match_result);
let sig = client.attestations().verify_signature(&att.attestation_id).await?;
println!("signature valid = {} ({:?})", sig.valid, sig.reason);
# Ok(()) }
Traces
client.traces() — group items into sealed bundles with a composite hash.
use invoance::models::{CreateTraceParams, GetTraceParams, ListTracesParams};
# async fn run(client: invoance::InvoanceClient) -> Result<(), invoance::Error> {
let trace = client
.traces()
.create(CreateTraceParams {
label: "Onboarding run 2026-07".into(),
metadata: None,
})
.await?;
println!("trace_id = {} ({})", trace.trace_id, trace.status);
let listed = client
.traces()
.list(ListTracesParams { status: Some("open".into()), ..Default::default() })
.await?;
println!("{} traces", listed.total);
let detail = client
.traces()
.get(&trace.trace_id, GetTraceParams { event_page: Some(1), event_limit: Some(50) })
.await?;
println!("status={} events={}", detail.status, detail.events.len());
let sealed = client.traces().seal(&trace.trace_id).await?;
println!("seal: {}", sealed.message);
let bundle = client.traces().proof(&trace.trace_id).await?;
println!(
"composite_hash_valid = {}",
bundle.verification.composite_hash_valid
);
let pdf: Vec<u8> = client.traces().proof_pdf(&trace.trace_id).await?;
std::fs::write("proof.pdf", &pdf).ok();
let deleted = client.traces().delete(&trace.trace_id).await?;
println!("deleted = {}", deleted.deleted);
# Ok(()) }
Audit logs
client.audit() is a namespace of five sub-resources — access them as fields:
.events, .orgs, .streams, .portal_sessions, .exports. Most methods
return the server's raw serde_json::Value.
client.audit().events — the signed event ledger
use invoance::models::{IngestAuditEventParams, ListAuditEventsParams};
use serde_json::json;
# async fn run(client: invoance::InvoanceClient) -> Result<(), invoance::Error> {
let audit = client.audit();
let appended = audit
.events
.ingest(IngestAuditEventParams {
organization_id: "org_customer_1".into(),
action: "user.role.granted".into(),
actor: json!({ "type": "user", "id": "admin_1" }).as_object().unwrap().clone(),
targets: Some(vec![json!({ "type": "user", "id": "u_9" })]),
occurred_at: None, context: None,
metadata: None,
idempotency_key: None,
..Default::default()
})
.await?;
let listed = audit
.events
.list(ListAuditEventsParams {
organization_id: Some("org_customer_1".into()),
limit: Some(100),
..Default::default()
})
.await?;
println!("next_cursor = {:?}", listed.next_cursor);
let one = audit.events.get("evt_123").await?;
let checked = audit.events.verify("evt_123").await?;
# let _ = (appended, one, checked);
# Ok(()) }
client.audit().orgs — end-customer organizations
use invoance::models::CreateAuditOrgParams;
# async fn run(client: invoance::InvoanceClient) -> Result<(), invoance::Error> {
let audit = client.audit();
audit
.orgs
.create(CreateAuditOrgParams {
organization_id: "org_customer_1".into(),
name: Some("Acme Corp".into()),
})
.await?;
audit.orgs.list().await?; audit.orgs.integrity("org_customer_1").await?; audit.orgs.set_retention("org_customer_1", 365).await?; # Ok(()) }
client.audit().streams — SIEM / webhook delivery
use invoance::models::CreateAuditStreamParams;
# async fn run(client: invoance::InvoanceClient) -> Result<(), invoance::Error> {
let audit = client.audit();
let stream = audit
.streams
.create(
"org_customer_1",
CreateAuditStreamParams {
url: "https://siem.example/hook".into(),
r#type: None, },
)
.await?;
audit.streams.list("org_customer_1").await?; audit.streams.test("org_customer_1", "stream_1").await?; audit.streams.delete("org_customer_1", "stream_1").await?; # let _ = stream;
# Ok(()) }
client.audit().portal_sessions — hosted-viewer links
use invoance::models::CreatePortalSessionParams;
# async fn run(client: invoance::InvoanceClient) -> Result<(), invoance::Error> {
let session = client
.audit()
.portal_sessions
.create(CreatePortalSessionParams {
organization_id: "org_customer_1".into(),
intent: "audit_logs".into(), session_duration_seconds: None, link_duration_seconds: None, })
.await?;
# let _ = session;
# Ok(()) }
client.audit().exports — async CSV / NDJSON jobs
use invoance::models::CreateAuditExportParams;
# async fn run(client: invoance::InvoanceClient) -> Result<(), invoance::Error> {
let audit = client.audit();
let job = audit
.exports
.create(CreateAuditExportParams {
organization_id: "org_customer_1".into(),
format: "csv".into(), filters: None,
})
.await?;
let status = audit.exports.get("export_1").await?;
# let _ = (job, status);
# Ok(()) }
Offline-verify an audit event returned by the API (no server trust):
use invoance::verify_audit_event;
# async fn run(client: invoance::InvoanceClient) -> Result<(), Box<dyn std::error::Error>> {
let event = client.audit().events.get("evt_123").await?;
let event_json = serde_json::to_value(&event)?;
let result = verify_audit_event(&event_json, None);
println!("valid={} reason={:?} key_source={:?}",
result.valid, result.reason, result.key_source);
# Ok(()) }
See examples/ for a runnable example per resource area
(events, documents, attestations, traces, audit, and quickstart).
License
MIT — see LICENSE.