snippe
Async Rust client for Snippe — payments for Tanzania.
The SDK targets the 2026-01-25 API version and covers the full surface:
- Collections — mobile money (M-Pesa, Airtel, Mixx, Halotel), card payments via the hosted Selcom checkout, dynamic QR.
- Hosted sessions — pre-built mobile-optimised checkout pages and short payment links for SMS / WhatsApp distribution.
- Disbursements — payouts to mobile wallets and 25+ Tanzanian banks (CRDB, NMB, NBC, ABSA, Equity, KCB, Stanbic, …).
- Webhooks — HMAC-SHA256 signature verification with replay protection and typed event dispatch.
Install
[]
= "0.1"
= { = "1", = ["full"] }
Or with cargo add snippe.
The crate compiles with rustls-tls by default. Switch to OpenSSL / native-tls with:
= { = "0.1", = false, = ["native-tls"] }
Quick start
use Customer;
use ;
use ;
async
The customer's phone receives a USSD push. They enter their mobile-money PIN to authorise. Snippe then POSTs payment.completed (or payment.failed) to your webhook_url.
Hosted checkout sessions
For a pre-built checkout page (great for SMS / WhatsApp distribution):
use ;
use Client;
# async
Disbursements
Payouts always need a balance preflight: calculate the fee, confirm available balance, then send.
use ;
use ;
# async
Bank transfers swap [MobilePayout] for BankPayout and add a BankCode:
use BankCode;
use ;
let request = Bank;
Webhook verification
Snippe signs webhooks with HMAC-SHA256. Always verify the signature against the raw request bytes — parsing-then-re-serialising the JSON breaks the HMAC. The verifier rejects timestamps older than 5 minutes by default to prevent replays.
use ;
#
The verifier returns [EventData::Unknown] for event types this SDK version doesn't enumerate, so new server-side events don't break your handler.
For framework integration, read the request body once as raw bytes (axum::body::Bytes, actix_web::web::Bytes, hyper::body::to_bytes, etc.) and pass that slice straight to verify_typed — never round-trip through serde_json::Value.
Critical rules
- Currency is TZS only. Amounts are integers in the smallest currency unit;
500means 500 TZS, not 5.00. - Minimum amounts: 500 TZS for payments, 5,000 TZS for payouts.
- Phone numbers must be
255XXXXXXXXXor+255XXXXXXXXX. Local formats like0781000000are rejected. - Idempotency keys must be ≤ 30 bytes. The SDK enforces this at construction time via [
IdempotencyKey] — over-long keys can never reach the wire and trigger the crypticPAY_001error. - Webhook payloads have
data.amountas{value, currency}, not a plain integer like request bodies. TheMoneytype models this correctly. - Webhook URLs must be HTTPS and ≤ 500 characters.
Error handling
All API errors come through [Error::Api] carrying a structured [ApiError]:
use ;
# async
[ApiError::is_retryable] returns true for 5xx, 429, and PAY_001 — the cases where retrying with backoff (and the same idempotency key) is safe and likely to recover.
Examples
The examples/ directory has runnable end-to-end snippets:
create_mobile_payment.rs— collect a mobile-money payment.create_session.rs— build a hosted checkout session and print the share link.send_payout.rs— full payout preflight (fee → balance → send).verify_webhook.rs— webhook signature verification outside an HTTP framework.balance.rs— fetch the merchant balance.
Run any of them with SNIPPE_API_KEY=snp_xxx cargo run --example <name>.
Configuration
use Duration;
use ;
let client = builder
.api_key
.environment // or Environment::Production (default)
.timeout // default 30s
.api_version // pinned via Snippe-Version header
.user_agent_suffix // for support correlation
.build
.unwrap;
The base URL can be overridden via base_url(...) for staging environments or wiremock-based tests.
Testing
The crate's own tests use wiremock to stand up a local mock server. Build wiremock-based tests for your application by overriding the base URL:
use MockServer;
use Client;
# async
Status
This SDK is at 0.1 — the public surface may evolve in minor releases until 1.0. The wire types use #[non_exhaustive] so new server-side fields and enum variants don't force a major bump.
License
MIT — see LICENSE.