Past raw HTTP calls.
The resource methods cover every endpoint, but a few things needed more than a flat method signature. This page covers all of them.
Pagination
Nomba's list endpoints are cursor-paginated server-side — they
return a cursor you feed back in for the next page.
paginate (sync) and apaginate (async) drive
that loop for you with an iterator; no second pagination scheme is introduced.
use nomba_rs::{Nomba, paginate};
use std::collections::HashMap;
let nomba = Nomba::new("id", "secret", "account_id")?;
let accounts = paginate(|limit, cursor| {
nomba.virtual_accounts.filter_virtual_accounts(
limit.map(|n| n.to_string()),
cursor,
None, None, None, None, None, None, None, None,
)
.and_then(|resp| serde_json::to_value(resp.data).map_err(nomba_rs::NombaError::from))
}, Some(50));
for account in accounts {
let account: HashMap = account?;
println!("{:?}", account.get("accountRef"));
}
Async variant (a Stream; add futures::StreamExt for .next()):
use nomba_rs::{AsyncNomba, apaginate};
use futures::StreamExt;
use std::collections::HashMap;
let nomba = AsyncNomba::new("id", "secret", "account_id").await?;
let mut stream = apaginate(|limit, cursor| {
let accounts = nomba.virtual_accounts.clone();
async move {
accounts.filter_virtual_accounts(
limit.map(|n| n.to_string()),
cursor,
None, None, None, None, None, None, None, None,
)
.await
.and_then(|resp| serde_json::to_value(resp.data).map_err(nomba_rs::NombaError::from))
}
}, Some(50));
while let Some(account) = stream.next().await {
let account: HashMap = account?;
println!("{:?}", account.get("accountRef"));
}
Confirmed paginated endpoints: accounts.list_all_accounts, accounts.fetch_terminals_assigned_to_account, accounts.fetch_terminals_assigned_to_parent_account, virtual_accounts.filter_virtual_accounts, and all six transactions.* list/filter methods.
Guided card-payment flow
Card checkout is a multi-step sequence: submit card details, then
maybe an OTP, maybe a 3D Secure redirect, then confirm.
CardPaymentFlow (sync) and AsyncCardPaymentFlow (async)
wrap the sequence and decode Nomba's response codes into plain booleans,
instead of you needing to know what "T0" or "S0" mean.
use nomba_rs::Nomba;
let nomba = Nomba::new("id", "secret", "account_id")?;
let order = nomba.checkout.create_order(
"order-001", "1000", "NGN",
"jane@example.com", "Jane Doe",
"https://example.com/callback", None, None
)?;
let mut flow = nomba.card_payment(order.data.order_reference);
let mut step = flow.submit_card("encrypted_card_details", "rsa_public_key", Some(true), None)?;
if step.requires_otp {
step = flow.submit_otp("123456")?;
} else if step.requires_3ds {
// redirect the user using step.secure_authentication_data
}
if step.completed {
let result = flow.confirm()?;
}
Async variant with AsyncCardPaymentFlow:
use nomba_rs::AsyncNomba;
let nomba = AsyncNomba::new("id", "secret", "account_id").await?;
let order = nomba.checkout.create_order(
"order-001", "1000", "NGN",
"jane@example.com", "Jane Doe",
"https://example.com/callback", None, None
).await?;
let mut flow = nomba.card_payment(order.data.order_reference);
let mut step = flow.submit_card("encrypted_card_details", "rsa_public_key", Some(true), None).await?;
if step.requires_otp {
step = flow.submit_otp("123456").await?;
} else if step.requires_3ds {
// redirect the user using step.secure_authentication_data
}
if step.completed {
let result = flow.confirm().await?;
}
step is a CardPaymentStep with .completed, .requires_otp, .requires_3ds, .transaction_id, .message.
Webhook signature verification
Implements Nomba's documented HMAC-SHA256 scheme
(nomba-signature / nomba-timestamp
headers, base64-encoded signature) plus a replay-window check on the
timestamp — a valid signature alone doesn't prove a webhook wasn't
captured and resent later.
use nomba_rs::{verify_webhook_request, NombaError};
use std::collections::HashMap;
fn handle_webhook(body: &[u8], headers: &HashMap) -> nomba_rs::Result<()> {
// Verifies the nomba-signature header and, with Some(300.0),
// rejects timestamps older/newer than 5 minutes (replay protection).
let payload = verify_webhook_request(
"your_webhook_signature_key",
body,
headers,
Some(300.0),
)?;
println!("Received webhook: {:?}", payload);
Ok(())
}
Lower-level helpers are also available: verify_webhook_signature(key, &payload, signature, timestamp) for a pre-parsed JSON payload, compute_signature(key, &payload, timestamp), and check_timestamp_freshness(timestamp, max_age_seconds).
Local request validation
With the validation feature enabled, every write call (POST/PUT)
is validated against Nomba's bundled OpenAPI spec before any network call.
This catches missing required fields in nested request bodies that Rust's type system can't enforce.
use nomba_rs::{validate_body, NombaError};
use serde_json::json;
let body = json!({
"orderReference": "order-001",
"customerId": "cust-001",
"callbackUrl": "https://example.com/cb",
"customerEmail": "jane@example.com",
"amount": 1000,
"currency": "NGN",
"allowedPaymentMethods": ["CARD", "ACCOUNT_TRANSFER"],
});
match validate_body("post", "/v1/checkout/order", &body) {
Ok(_) => println!("Request is valid"),
Err(NombaError::Validation { missing, .. }) => {
eprintln!("Missing fields: {:?}", missing);
}
Err(e) => eprintln!("Error: {}", e),
}
Reliability: locking + retry/backoff
The HTTP client guards token fetching with a lock, so concurrent
requests never race to re-fetch a token — only one fetch happens,
the rest reuse it. Requests that hit a 429 or transient
5xx retry automatically with exponential backoff,
respecting Retry-After when Nomba sends one.
use nomba_rs::{NombaClientConfig, Nomba};
let config = NombaClientConfig::new("id".into(), "secret".into(), "account_id".into())
.timeout(std::time::Duration::from_secs(60)) // default 30s
.max_retries(3) // default; retries 429/5xx
.backoff_factor(0.5); // default; delay ~= factor * 2^attempt + jitter
let nomba = Nomba::with_config(config)?;
Bounded concurrency for fan-out calls
Firing off many calls at once can trigger a retry storm if several
start failing together, or just trip Nomba's rate limit by bursting
too many requests in one window. gather_limited and
gather_limited_ordered (both async) run the same calls with a cap on in-flight requests.
use nomba_rs::{AsyncNomba, gather_limited};
let nomba = AsyncNomba::new("id", "secret", "account_id").await?;
let refs = vec!["acct-1", "acct-2", "acct-3", "acct-4", "acct-5"];
let calls: Vec<_> = refs.into_iter().map(|r| {
let nomba = nomba.clone();
let r = r.to_string();
move || async move { nomba.virtual_accounts.fetch_virtual_account(r).await }
}).collect();
let results = gather_limited(calls, 5).await?; // limit 5 concurrent
for r in results {
println!("{:?}", r.data.account_ref);
}
Use gather_limited_ordered (same arguments) when results must come back in input order.