PayRail is a provider-neutral Rust payment library for accepting payments through Stripe, PayPal, crypto providers, and Mobile Money providers such as Lipila.
The public API exposes PayRail payment concepts instead of provider-specific SDK types. First-party providers are internal modules behind feature flags so applications only compile the payment rails they need while depending on one public crate.
payrail is the only public crate. It contains provider-neutral domain types, idempotency,
webhook abstractions, route configuration, and first-party providers behind Cargo features.
Planned first-party extension points include additional Mobile Money providers, Mobile Money aggregators, and crypto providers such as Circle, Coinbase, Bridge, and Binance.
[dependencies]
payrail = { version = "0.1", features = ["stripe", "paypal", "lipila"] }The default TLS backend is Rustls. Do not enable all providers unless the application uses them.
Common feature flags:
stripe: Stripe hosted and custom Checkout, status, refunds, and webhooks.paypal: PayPal Orders, OAuth, capture, and webhooks.lipila: Lipila Zambia Mobile Money. This also enablesmobile-money.mobile-money: shared Mobile Money types and helpers.all-providers: all first-party providers.rustls: Rustls TLS backend.native-tls: native TLS backend.
use payrail::{CreatePaymentRequest, Money, PaymentMethod, PayRail};
use payrail::StripeConfig;
use secrecy::SecretString;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = PayRail::builder()
.stripe(StripeConfig::new(SecretString::from(
std::env::var("STRIPE_SECRET_KEY")?,
))?)?
.build()?;
let request = CreatePaymentRequest::builder()
.amount(Money::new_minor(2_500, "USD")?)
.reference("ORDER-1001")?
.payment_method(PaymentMethod::card())
.return_url("https://example.com/success")?
.cancel_url("https://example.com/cancel")?
.idempotency_key("ORDER-1001:create")?
.build()?;
let session = client.create_payment(request).await?;
println!("provider reference: {}", session.provider_reference.as_str());
Ok(())
}Hosted Checkout remains the default. For an on-site Stripe Payment Element flow, request
CheckoutUiMode::Custom and pass the returned client secret to Stripe.js.
Metadata is sent to Stripe and may be visible in the Stripe Dashboard and
reports, so do not put secrets, credentials, card data, bank data, or sensitive
personal data in metadata. PayRail mirrors request metadata to Stripe
PaymentIntent metadata by default for webhook reconciliation; use
.payment_metadata(...) when the payment object needs different metadata.
use payrail::{
CheckoutUiMode, CreatePaymentRequest, Customer, Money, NextAction, PaymentMethod,
};
let request = CreatePaymentRequest::builder()
.amount(Money::new_minor(2_500, "USD")?)
.reference("ORDER-1003")?
.customer(Customer::new().with_email("buyer@example.com"))
.payment_method(PaymentMethod::card())
.checkout_ui_mode(CheckoutUiMode::Custom)
.return_url("https://example.com/stripe/return?session_id={CHECKOUT_SESSION_ID}")?
.idempotency_key("ORDER-1003:create")?
.metadata("tenant_id", "tenant_123")
.metadata("package_id", "package_456")
.build()?;
let session = client.create_payment(request).await?;
if let Some(NextAction::EmbeddedCheckout { client_secret }) = session.next_action() {
let _client_secret_for_stripe_js = client_secret;
}Routes use PayRail's built-in provider identifiers and dispatch through concrete connector fields. There is no trait-object connector registry in the library hot path.
Route configuration and connector availability are separate:
- Implemented connectors today: Stripe, PayPal, and Lipila.
- Reserved crypto route targets: Circle, Coinbase, Bridge, and Binance. These are modeled provider
IDs, but payments routed to them return
ConnectorNotConfigureduntil first-party connectors are implemented and configured. - Reserved Mobile Money and aggregator route targets: MTN MoMo, M-Pesa, Airtel Money, Orange Money,
Flutterwave, and Paystack. These are modeled provider IDs, but payments routed to them return
ConnectorNotConfigureduntil first-party connectors are implemented and configured. PaymentProvider::Other(...)is metadata for normalized provider references and events. It is not a runtime routing extension point.
Lipila is the default Zambia Mobile Money route. Applications can override or add country routes only to modeled built-in providers. Do not route a country to a provider unless that provider connector supports the country.
use payrail::{BuiltinProvider, CountryCode, PayRail};
let client = PayRail::builder()
.mobile_money_route(
CountryCode::new("ZM")?,
BuiltinProvider::Lipila,
)
.build()?;Stripe-hosted USDC Checkout remains the default stablecoin route for compatibility. Other stablecoins, including USDT, require explicit routing so unsupported assets or networks do not accidentally route to a provider that cannot process them.
use payrail::{
BuiltinProvider, CryptoAsset, CryptoNetwork, PayRail, PaymentMethod,
};
let client = PayRail::builder()
.crypto_route(BuiltinProvider::Coinbase)
.crypto_asset_route(CryptoAsset::Usdt, BuiltinProvider::Coinbase)
.crypto_asset_network_route(
CryptoAsset::Usdc,
CryptoNetwork::Base,
BuiltinProvider::Circle,
)
.build()?;
let method = PaymentMethod::usdc_on(CryptoNetwork::Base);
let usdt = PaymentMethod::stablecoin_usdt();Crypto route precedence is asset + network, then asset, then network, then default crypto route.
Future stablecoins can be supported without changing core by using StablecoinAsset::Other(symbol)
and CryptoAsset::Other(symbol) with an explicit asset route. Stablecoins that become broadly
supported can be promoted to first-class enum variants with matching routing and provider tests.
The example above configures route selection only. It is useful for validating routing behavior and for future connector work, but successful payment execution requires the selected provider connector to exist and be configured.
Provider extensions are implemented inside PayRail as feature-gated first-party modules so the
facade can keep static dispatch and avoid runtime trait-object routing. New providers must add a
BuiltinProvider route target when needed, keep secret configuration in secrecy::SecretString,
verify webhooks before parsing, and include mocked backend integration tests for provider
operations.
Use the provider contribution template:
- PayRail never handles raw card numbers; card payments use provider-hosted or provider-secure flows.
- API keys and webhook secrets use
secrecy::SecretString. - Webhook verification uses raw request bodies and constant-time comparison where applicable.
- Public response types do not expose raw provider response bodies or raw webhook payloads by default.
- Errors expose only normalized or redacted provider diagnostics.
- Sandbox/live tests are gated by explicit environment variables and never run by default.
Required before release candidates:
make fmt-check
make check
make lint
make test
make examples
make doc
make coverage
make securityThe workspace release gate is 90% line coverage. Core and security-sensitive modules target 95%+ line coverage, with branch coverage tracked where tooling permits.
- Stripe stablecoin support is delegated to Stripe-supported Checkout flows.
- Stripe card payments support hosted Checkout by default and custom Checkout Sessions with
CheckoutUiMode::Custom.CheckoutUiMode::Elementsremains as a source-compatible alias. - General crypto payments use provider-neutral
PaymentMethod::cryptoand explicit route registration for modeled providers such as Circle, Coinbase, Bridge, or Binance once their first-party connectors exist. - PayRail does not custody private keys, seed phrases, or raw wallet credentials.
- PayPal refunds are not implemented in v1.
- Lipila v1 exposes Zambia Mobile Money collections only.
- MTN MoMo, M-Pesa, Airtel Money, Orange Money, Flutterwave, and Paystack are modeled route targets, but their first-party connectors are not implemented yet.
Licensed under either of:
- Apache License, Version 2.0
- MIT license