Idiomatic async-first Rust client for the bKash Payment Gateway API.
bkash-rs covers every documented bKash payment product — tokenized
checkout, classic URL-based checkout, authorization & capture,
subscriptions, and SNS-style webhook signature verification — behind a
single, cheaply-cloneable [Bkash] handle with transparent token
caching, idempotent retries on transient failures, and typed error
mapping for bKash's statusCode envelope.
use bkash_rs::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Configure credentials from your bKash developer portal.
let bkash = Bkash::builder()
.environment(Environment::Sandbox)
.app_key("your_app_key")
.app_secret("your_app_secret")
.username("your_username")
.password("your_password")
.build_and_connect() // validates config, warms the token cache
.await?;
// 2. Drive a payment through Tokenized Checkout.
let resp = bkash
.tokenized()
.query_payment("TR0011") // bKash paymentID, not trxID
.await?;
println!("status = {:?}, trxID = {}", resp.transaction_status, resp.trx_id);
Ok(())
}A runnable version of this flow lives in
examples/quickstart.rs; a staged sandbox
walk-through that prints bkashURLs for wallet approval lives in
examples/from_env.rs.
| Feature flag | Default | What it enables |
|---|---|---|
rustls-tls |
✅ | TLS via rustls — pure Rust, no OpenSSL dependency. |
native-tls |
❌ | TLS via the platform's native stack (Secure Transport, SChannel, OpenSSL). |
tokenized-checkout |
✅ | bkash.tokenized() — Tokenized Checkout product accessor (the modern TLV-based flow). |
checkout |
✅ | bkash.checkout() — classic URL-based Checkout product accessor. |
auth-capture |
❌ | bkash.auth_capture() — Authorization & Capture product accessor (two-step authorize-then-capture). |
subscriptions |
❌ | bkash.subscriptions() — Subscriptions product accessor (recurring billing agreements). |
webhooks |
❌ | bkash_rs::webhooks — verify SNS-style webhook signatures and parse typed WebhookEvents. |
The two TLS features are mutually exclusive — pick exactly one. The product features are independent and additive; enabling more shrinks your binary but gives you more API surface. A minimal build that disables the default checkout products and keeps only Auth & Capture plus webhook verification:
[dependencies]
bkash-rs = { version = "0.2", default-features = false, features = ["rustls-tls", "auth-capture", "webhooks"] }Add to Cargo.toml:
[dependencies]
bkash-rs = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Or with cargo add:
cargo add bkash-rs
cargo add tokio --features macros,rt-multi-thread| Product | Accessor | Feature flag | What it's for |
|---|---|---|---|
| Tokenized Checkout | bkash.tokenized() |
tokenized-checkout |
Modern TLV-based checkout. The recommended path for new integrations. |
| URL-based Checkout | bkash.checkout() |
checkout |
Classic one-shot URL redirect flow (legacy but still fully supported). |
| Auth & Capture | bkash.auth_capture() |
auth-capture |
Two-step authorize-then-capture for delayed settlement. |
| Subscriptions | bkash.subscriptions() |
subscriptions |
Agreement-based recurring billing. Typed façade over Tokenized Checkout. |
| Webhooks | bkash_rs::webhooks::* |
webhooks |
Verify SNS-style signatures on bKash webhook deliveries. |
Each accessor borrows the parent Bkash and reuses its Transport
and token cache — there is no per-product connection state. Calling
two different accessors from the same Bkash is just as cheap as
calling one twice.
use bkash_rs::Environment;
Bkash::builder()
.environment(Environment::Sandbox) // or Environment::Production
.app_key("...").app_secret("...")
.username("...").password("...")
.build_and_connect().await?;Environment::Sandbox points at https://tokenized.sandbox.bka.sh/v1.2.0-beta
(per-product subdomains are selected automatically — see
[Config::base_url]). Environment::Production points at the
equivalent *.pay.bka.sh host. The crate does not gate live calls
or add a dry-run mode — choose the correct environment for your
context.
Every supported bKash endpoint maps to one crate method. All methods
are async, take their request body by value, and return a typed
Result<_, Error>. HTTP method and path are what bKash's API
actually exposes; the crate method sits one level above that.
Requires the tokenized-checkout feature. Endpoints route through the
tokenized.sandbox.bka.sh / tokenized.pay.bka.sh subdomain.
| HTTP | Endpoint path | Crate method | Notes |
|---|---|---|---|
| POST | /tokenized/checkout/token/grant |
bkash.tokenized().grant_token(req) |
Auto-invoked by the transport on first request. |
| POST | /tokenized/checkout/token/refresh |
bkash.tokenized().refresh_token(req) |
Refresh a near-expiry id_token. |
| POST | /tokenized/checkout/create |
bkash.tokenized().create_agreement(req) |
Returns paymentID (a.k.a. agreementCreateID). |
| POST | /tokenized/checkout/execute |
bkash.tokenized().execute_agreement(payment_id) |
Returns the agreementID. |
| POST | /tokenized/checkout/agreement/status |
bkash.tokenized().query_agreement(agreement_id) |
Query current agreement state. |
| POST | /tokenized/checkout/agreement/cancel |
bkash.tokenized().cancel_agreement(agreement_id) |
Cancel a DRAFT or ACTIVE agreement. |
| POST | /tokenized/checkout/create |
bkash.tokenized().create_payment(req) |
Create a payment against an agreement (mode=0001). |
| POST | /tokenized/checkout/execute |
bkash.tokenized().execute_payment(payment_id) |
Returns the trxID. |
| POST | /tokenized/checkout/payment/status |
bkash.tokenized().query_payment(payment_id) |
Query payment state. |
| POST | /tokenized/checkout/general/searchTransaction |
bkash.tokenized().search_transaction(trx_id) |
Look up a transaction by trxID. |
| POST | /tokenized/checkout/payment/refund |
bkash.tokenized().refund(req) |
Refund a captured payment. Up to 10 partials per txn. |
| POST | /tokenized/checkout/payment/refund/status |
bkash.tokenized().refund_status(payment_id, trx_id) |
Look up a previously-issued refund. |
Requires the checkout feature. Wire endpoints are identical to
Tokenized Checkout — only the base URL subdomain (checkout.* vs.
tokenized.*) differs. There is no agreement step; this is the
classic one-shot create → execute flow.
| HTTP | Endpoint path | Crate method |
|---|---|---|
| POST | /tokenized/checkout/token/grant |
bkash.checkout().grant_token(req) |
| POST | /tokenized/checkout/token/refresh |
bkash.checkout().refresh_token(req) |
| POST | /tokenized/checkout/create |
bkash.checkout().create_payment(req) |
| POST | /tokenized/checkout/execute |
bkash.checkout().execute_payment(payment_id) |
| POST | /tokenized/checkout/payment/status |
bkash.checkout().query_payment(payment_id) |
| POST | /tokenized/checkout/general/searchTransaction |
bkash.checkout().search_transaction(trx_id) |
| POST | /tokenized/checkout/payment/refund |
bkash.checkout().refund(req) |
| POST | /tokenized/checkout/payment/refund/status |
bkash.checkout().refund_status(payment_id, trx_id) |
create_payment automatically sets mode = "0011"; on Tokenized
Checkout create_payment automatically sets mode = "0001".
Requires the auth-capture feature. Routes through the same
tokenized.* subdomain as Tokenized Checkout. Use this when you want
to authorize a payment now and capture (settle) it later — e.g. for
marketplace escrow or delayed-charge flows.
| HTTP | Endpoint path | Crate method |
|---|---|---|
| POST | /tokenized/checkout/token/grant |
bkash.auth_capture().grant_token(req) |
| POST | /tokenized/checkout/token/refresh |
bkash.auth_capture().refresh_token(req) |
| POST | /tokenized/checkout/payment/create |
bkash.auth_capture().create_payment(req) |
| POST | /tokenized/checkout/execute/{paymentID} |
bkash.auth_capture().execute_payment(payment_id) |
| POST | /tokenized/checkout/payment/status |
bkash.auth_capture().query_payment(payment_id) |
| POST | /tokenized/checkout/payment/confirm/capture |
bkash.auth_capture().capture(payment_id) |
| POST | /tokenized/checkout/payment/confirm/capture/void |
bkash.auth_capture().void(payment_id) |
| GET | /checkout/payment/search/{trxID} |
bkash.auth_capture().search_transaction(trx_id) |
execute_payment here uses paymentID as a URL path parameter
(other products pass it in the request body).
Requires the subscriptions feature. bKash hosts Subscriptions on the
tokenized-checkout agreement endpoints, so SubscriptionsClient is a
typed façade over the same wire calls — it just routes to the
subscriptions product subdomain and tags requests so they appear under
the merchant's recurring-billing dashboard.
| HTTP | Endpoint path | Crate method |
|---|---|---|
| POST | /tokenized/checkout/create |
bkash.subscriptions().create_subscription(req) |
| POST | /tokenized/checkout/execute |
bkash.subscriptions().execute_subscription(payment_id) |
| POST | /tokenized/checkout/agreement/status |
bkash.subscriptions().query_subscription(agreement_id) |
| POST | /tokenized/checkout/agreement/cancel |
bkash.subscriptions().cancel_subscription(agreement_id) |
Requires the webhooks feature. These are synchronous helpers, not
HTTP calls — they verify and parse inbound bKash SNS envelopes
delivered to your endpoint.
| Helper | Description |
|---|---|
webhooks::verify_sns_signature(envelope) |
Fetch the signing certificate and verify the signature. |
webhooks::verify_signature_with_key(envelope, key) |
Verify against a pre-fetched rsa::RsaPublicKey. |
webhooks::parse_event(envelope) |
Parse a verified envelope into a typed WebhookEvent. |
webhooks::build_string_to_sign(envelope) |
Build the canonical string-to-sign for manual verification. |
webhooks::cert_pem_to_public_key(pem) |
Parse a PEM-encoded X.509 cert into a RsaPublicKey. |
webhooks::confirm_subscription(envelope, url) |
POST to a SubscribeURL to confirm a new subscription. |
WebhookEvent::trx_id() / as_code() / is_*() |
Typed accessors on a parsed event. |
Every crate method returns Result<T, bkash_rs::Error>. The
Error
enum covers:
Error::Auth(String)— invalid app key/secret/username/password (bKasherrorCode2001–2004 etc).Error::Api { code, message, http_status }— typed bKash error envelope;codeis a [ErrorCode] (auto-mapped from bKash'sstatusCode/errorCodestrings).Error::Transport(reqwest::Error)— network / TLS failure.Error::Timeout(Duration)— request exceeded configured timeout.Error::Config(String)— invalidConfig(missing credentials, invalid URL, etc).Error::Json { kind, path }— response body failed to deserialize (the raw payload is preserved).
Error::is_transient
and
Error::is_auth
help with retry / re-auth decisions. Transient errors are retried
automatically by the transport with exponential backoff (configurable
via ConfigBuilder::max_retries).
The examples/ directory ships two runnable smoke tests:
-
quickstart.rs— hard-coded credentials, drives a Tokenized Checkout payment from grant → create → query → (optional) refund.# Edit the constants at the top of the file first. cargo run --example quickstart -
from_env.rs— staged sandbox walk-through that reads credentials from a.env(seeset -a; source .env; set +a). Walks the customer through wallet approval on the phone between steps. Covers Tokenized Checkout, Subscriptions, and the classic URL-based Checkout product.set -a; source .env; set +a cargo run --example from_env -- create-agreement # opens bkashURL # approve on phone with PIN 12121 / OTP 123456 ... cargo run --example from_env -- execute-agreement --payment-id <ID> cargo run --example from_env -- create-payment --agreement-id <AG> cargo run --example from_env -- execute-payment --payment-id <ID> cargo run --example from_env -- query-payment --payment-id <ID> cargo run --example from_env -- refund --payment-id <ID> --trx-id <TRX> --amount 100.00
Subscriptions and classic Checkout stages require their feature flags (
--features subscriptions,--features checkout); the example prints ahelplisting when invoked with no subcommand.
ConfigBuilder
covers everything. All four credentials (app_key, app_secret,
username, password) are required.
use std::time::Duration;
use bkash_rs::Environment;
let config = Bkash::builder()
.environment(Environment::Sandbox)
.app_key("...").app_secret("...")
.username("...").password("...")
.timeout(Duration::from_secs(30)) // default: 30s
.max_retries(3) // default: 3
.build()?; // returns Result<Config, Error>For advanced setups (custom reqwest::Client, proxy, base-URL
override for a local bKash mock) use with_http_client(...) or
with_base_url(...).
src/
├── lib.rs # crate-level docs + module declarations
├── client.rs # `Bkash` handle (cheap-clone Arc<Inner>)
├── config.rs # Config, ConfigBuilder, Environment, Product
├── error.rs # Error, ErrorCode
├── token.rs # TokenCache + CachedToken
├── transport.rs # Transport (reqwest + retry + auth)
├── prelude.rs # `use bkash_rs::prelude::*;`
├── checkout.rs # `bkash.checkout()` accessor
├── tokenized.rs # `bkash.tokenized()` accessor
├── auth_capture.rs # `bkash.auth_capture()` accessor
├── subscriptions.rs # `bkash.subscriptions()` accessor
├── webhooks.rs # `bkash_rs::webhooks::*` helpers
└── models/ # request/response types, one submod per product
tests/ # wiremock integration tests + fixtures + #[ignore]'d live sandbox smoke tests
examples/ # quickstart + from_env staged driver
The minimum supported Rust version is 1.75. Bumping the MSRV is a breaking change and will require a minor version bump.
bkash-rs is on the 0.2.x track. The 0.2.0 release cut the first
crates.io publish and locked the public API surface for the
initial release. Semver is now meaningful: breaking changes will
require a 0.3.0 bump, and additive changes will go into 0.2.x.
Until 1.0, the API is permitted to evolve within the 0.x track.
See CHANGELOG.md for the current state, including
all breaking-change notes between releases.
Contributions welcome — please read CONTRIBUTING.md
first. It covers building from source, the test layout, fixture
recording, and the opt-in live-sandbox smoke tests.
Report security issues privately — see SECURITY.md.
Do not file public GitHub issues for vulnerabilities.
Licensed under either of Apache-2.0 or MIT at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.