Integrations
Rust

Rust SDK

Current source release: 0.1.2. The earlier 0.1.1 registry version below remains the verified published version; the new source patch is not yet claimed as published there.

The source patch adds a concrete merchant-site presentation example. Embedded checkout is the default: load only the official Lunixi bundle and mount the existing form; methods and multi-card choices stay in the Lunixi panel. Optional hosted mode uses the backend paymentPageUrl after exact HTTPS pay.lunixi.com/payment/ validation. Its token may differ from the embedded token. Missing URLs keep embedded available and hosted on hold/reconciliation with the original attempt/key. Never derive a URL from the browser token. Authorize the customer/order and native browser CSRF before rendering, send Cache-Control: private, no-store, preserve the merchant CSP nonce and keep tokens/HTML/redirect URLs out of logs. Generic response data preserves paymentPageUrl; no new typed product parity is claimed.

lunixi is an asynchronous server-side SDK for Rust 1.89+ (edition 2024). It uses Tokio and reqwest with verified Rustls TLS, works with Axum and Actix Web, and shares the authentication/transport layer across services.

Install from crates.io

Lunixi 0.1.1 is the last verified crates.io release (opens in a new tab). This older registry option is separate from the current 0.1.2 source/example update; crates.io 0.1.2 is not claimed available here. To pin the earlier release in your application's Cargo.toml:

[dependencies]
lunixi = "=0.1.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

For the current checkout presentation example, use the published v0.1.2 source tag instead of that older registry dependency:

[dependencies]
lunixi = { git = "https://github.com/Lunixi-Payment/lunixi-rust", tag = "v0.1.2" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

v0.1.2 source and examples (opens in a new tab). Keep Cargo.lock to retain the resolved source commit. Presentation helpers live in the tagged source example; adding the library dependency does not install a merchant web application or authorize an order.

Enable features = ["sqlite"] on the lunixi dependency for the optional durable inbox. It uses bundled SQLite and requires a C compiler. The default feature set does not build SQLite.

Create one client per merchant

use lunixi::{Client, Config};
 
let mut config = Config::new(
    "https://api-gateway.lunixi.com",
    std::env::var("LUNIXI_KEY_ID")?,
    std::env::var("LUNIXI_PRIVATE_KEY_PEM")?,
);
config.max_retries = 2;
let client = Client::new(config)?;

Register the public Ed25519 key in the dashboard. The private key stays on your server. PKCS8 PEM, base64 PKCS8 DER, base64 32-byte seed and validated base64 64-byte keypair encodings are accepted. The merchant key selects TEST/LIVE; do not ship private credentials to the browser.

Clone and share the client: token cache and connection pool are shared. Token acquisition is lazy, coalesced between concurrent callers and renewed with an expiry margin. Debug output redacts configuration/client/verifier secrets and response bodies. Explicit data/raw accessors may contain customer information; do not log them.

SettingDefaultLimit / behavior
timeout30 secondsPositive, up to 2 minutes per HTTP request; use an overall Tokio timeout too
max_retries00–5; GET or caller-persisted idempotency key only
max_response_bytes8 MiBUp to 32 MiB
allow_insecure_localhostfalseExplicit loopback/localhost HTTP tests only
additional_root_certificatesemptyTrusted DER CA certificates; verification stays enabled

BaseURL must be a trusted HTTPS origin without path, credentials or query. Redirects, implicit reqwest retries, environment proxies and Referer propagation are disabled. Request paths cannot change origin, traverse directories or enter the authentication namespace.

Start hosted checkout

Read the basket/order from your server-side records. Save the attempt key before the call; reuse the same key/body after an uncertain outcome.

use lunixi::{BasketItem, IntentRequest};
 
let mut intent = IntentRequest::new(
    10050, "TRY", "ORD-1001", "https://merchant.example/payment/return",
);
intent.basket_items.push(BasketItem::new(
    "line-1", "Example product", "PHYSICAL", 10050,
));
let response = client.payments()
    .create_intent(&intent, &persisted_attempt_key).await?;
let token = response.data.as_ref()
    .and_then(|data| data.get("token"))
    .and_then(|value| value.as_str())
    .filter(|value| !value.is_empty())
    .ok_or(lunixi::Error::Response("missing checkout token"))?;

Return the token only to this order's authenticated customer with Cache-Control: no-store, then initialize the existing Lunixi checkout form. Do not put tokens into URLs, logs or analytics. The form handles enabled cards and alternative methods; the SDK does not collect raw cards in its default examples.

Amounts are positive i64 minor units up to 9007199254740991: 10050 = 100.50 TRY. Basket price is the line total; its sum must equal root amount. Use the single root amount field. Installments and fees belong to the form. Optional buyer/address and documented insurance basket fields are supported; method restrictions can only narrow panel-enabled methods. Optional booleans preserve explicit false using Option<bool>.

A checkout token, browser return, HTTP success or AUTHORIZED status is not proof of capture. Fulfill only after authenticating gateway state and matching the stored merchant, order, payment ID, expected amount/currency and final capture/reconciliation state. Handle refunds, voids and disputes separately.

API coverage

FacadeHelpers
payments()create_intent, get, list, methods, capture, refund, void
payments() splitsplit, get_split, refund_split
fraud()evaluate, record_event, decisions, decision
send(Request)Other documented /api/v1/ merchant APIs through shared signing
WebhookVerifierRaw v2 verification, rotation, freshness and ID/type binding
SqliteInboxOptional durable transactional deduplication
let refund = client.payments()
    .refund(&payment_id, Some(2500), Some("Customer return"), &persisted_refund_key)
    .await?;

None amount requests a full capture/refund. void releases an authorization. Split validates safe leg amounts, 1–5 legs and their exact sum; method-specific schema validation stays in the gateway. Pending split states may return HTTP 400 inside Response; keep them unpaid and reconcile aggregate/leg balances.

Responses preserve envelope, data, page_info, binary bytes, content type and safe request ID. decode_data::<T>() supports application-owned schemas. JSON integer values are preserved as integers. Errors omit upstream messages/bodies, credentials and URLs.

Version 0.1.1 has no separate typed clients for wallet, bank-connect, payment links, insurance, marketplace, subscription or identity. Use the documented REST schemas with send; it does not claim typed feature parity for these products.

use lunixi::{Method, Request};
let mut request = Request::new(Method::GET, "/api/v1/payments");
request.query.push(("limit".into(), "20".into()));
let response = client.send(request).await?;

Every merchant call is signed. Canonical JSON matches JavaScript numeric-key order and ECMAScript number formatting. Unsafe JSON integers, duplicate keys and excessive nesting fail locally. The token request has no body or Digest header.

Known payment mutations and financial helpers require a persisted 16–128 character non-space printable ASCII idempotency key. Retries keep that key/body and use fresh nonces. Only GET/keyed operations retry within the configured bound; unkeyed mutations do not. Reconcile an unknown result before starting another payment.

Raw webhook verification

let verifier = lunixi::WebhookVerifier::new(vec![
    std::env::var("LUNIXI_WEBHOOK_SECRET")?,
])?;
let event = verifier.verify(raw_body, headers)?;

Verify bounded raw bytes before JSON middleware. Header type is reqwest::header::HeaderMap (HTTP 1.x); do not hide duplicate identity headers when adapting your framework. HMAC-SHA256 binds event ID, exact timestamp and raw bytes, using constant-time comparison. Freshness defaults to five minutes; with_tolerance accepts 1–900 seconds and cannot disable checks. The signed body requires type; an unsigned type header cannot authorize a financial action. Duplicate JSON/header fields and ID/type mismatches fail closed. Up to three current/previous secrets support rotation.

Verification does not consume the event ID. Enable feature sqlite and use a persistent connection:

let inbox = lunixi::SqliteInbox::new(merchant_scope)?;
lunixi::SqliteInbox::ensure_schema(&connection)?; // startup/migration only
let applied = inbox.process(&mut connection, &event, |transaction| {
    apply_verified_order_transition(transaction, &event)
})?;

connection is your rusqlite::Connection; apply_verified_order_transition is your adapter that validates financial fields and updates order/outbox state in the supplied transaction. Use a blocking worker for synchronous SQLite work in an async server. Marker and business state must commit together. A duplicate skips the callback; a reused ID with different payload fails closed; callback/storage failure rolls back the marker for safe retry.

Use separate merchant scopes and retain markers for your replay policy. External effects belong in an outbox worker. For another database, implement the same unique-event/hash/business-update transaction contract. Never acknowledge a persistence failure with HTTP 2xx. Request a fresh dashboard replay for old events rather than bypassing freshness.

The runnable durable receipt example (opens in a new tab) consumes raw stdin and authentic delivery headers, then records only verified IDs/types. It does not fulfill orders. The TEST checkout example (opens in a new tab) omits tokens from console output.

Checks

cargo fmt --all --check
cargo test --locked --all-targets --all-features
cargo clippy --locked --all-targets --all-features -- -D warnings
cargo audit
cargo deny --all-features check advisories licenses sources
cargo package --locked --allow-dirty --all-features

Tests use real local TLS, independent Node.js JSON/Ed25519/HMAC vectors, concurrent token acquisition and SQLite rollback/restart/replay cases. All test credentials are synthetic; no live merchant calls are needed. Security policy (opens in a new tab).

Merchant-page integration example

The source checkout example includes checkout_presentation.rs (opens in a new tab): render_checkout_html(token, nonce) mounts the official form, while hosted_checkout_url(paymentPageUrl) validates an optional redirect. Adapt them in an existing authorized, CSRF-protected merchant controller. LUNIXI_CHECKOUT_MODE defaults to embedded; hosted is explicit. The CLI never prints the generated HTML/token/URL. These are example helpers, not additional SDK product APIs.