paykit
A payment-provider abstraction for Rust, with a Mollie implementation.
paykit is built around one object-safe trait, PaymentProvider, so an application can
hold an Arc<dyn PaymentProvider> and swap providers — or substitute a fake in tests —
without the call sites changing. The core types (Money, Currency, Payment,
PaymentStatus) carry no HTTP dependency; the transport arrives with a provider feature.
- Object-safe trait, injectable as
Arc<dyn PaymentProvider> - Integer-cent
Money— no floats anywhere near an amount - Transport-agnostic core:
--no-default-featuresbuilds withoutreqwest #![forbid(unsafe_code)],#![deny(missing_docs)]
Install
[]
= "0.1"
Default features are mollie and rustls-tls. For the core traits and types only:
[]
= { = "0.1", = false }
Usage
use Arc;
use Duration;
use ;
use MollieProvider;
# async
Feature flags
| Feature | Default | Description |
|---|---|---|
mollie |
yes | The Mollie provider implementation. Pulls in reqwest and serde_json. |
rustls-tls |
yes | TLS via rustls. No system OpenSSL needed; the usual choice. |
native-tls |
no | TLS via the platform's native stack (OpenSSL / Schannel / Secure Transport). |
serde |
no | Serialize/Deserialize on Payment, Money, PaymentStatus, Refund, RefundStatus, for persisting or forwarding them. |
A provider feature needs a TLS backend. Enabling mollie with neither rustls-tls nor
native-tls is a compile error rather than a runtime surprise — without one, the crate
still builds but every HTTPS request fails once you are in production.
Cargo features are additive across the whole dependency graph, and TLS backend is no
exception. If your application depends on paykit with only rustls-tls, but some other
crate in your dependency tree enables paykit/native-tls (directly or transitively), your
build silently gets both backends compiled in and, depending on the underlying HTTP client,
may end up using native-tls instead of the one you configured. Cargo has no notion of "my
crate's choice wins" — the union of every enabled feature in the graph is what gets built.
If this matters to you, audit cargo tree -e features -i paykit rather than assuming your
own Cargo.toml is the last word.
Turning off default features gives you PaymentProvider, Money, Payment and friends
with no HTTP stack at all, which is what you want when implementing the trait against your
own transport or a test double.
reqwest is a public dependency
This is deliberate and it has a versioning consequence, so it is stated up front rather than buried in the API docs.
Provider constructors accept a caller-supplied reqwest::Client (MollieProvider::with_client)
so that connection pooling, timeouts, proxy configuration and TLS settings are shared with
the rest of your application instead of paykit quietly opening a second pool. reqwest
types therefore appear in the public API, and the crate re-exports paykit::reqwest so you
can construct a client from the exact version this crate links against.
A breaking reqwest release is a breaking release of paykit. When reqwest goes to
0.13, paykit will need a major (pre-1.0: minor) version bump, because a consumer passing a
reqwest 0.12 Client into a paykit built against 0.13 gets a type error, not a
deprecation warning. Pin accordingly, and expect this crate's version to track reqwest's
breaking changes as well as its own.
If you want to avoid that coupling entirely, depend on paykit with
default-features = false and implement PaymentProvider over your own HTTP client.
Security
Read this section before shipping. The failure modes below are the ones that lose money or cancel real orders, and none of them are things this crate can prevent for you.
Verifying a payment is not the same as verifying an order. paykit reports what the
provider believes about a payment. It cannot know what you intended to charge. Before you
treat a payment as settled, compare the amount and the currency against your own stored
order total. A Paid status on a 1.00 EUR payment against a 100.00 EUR order is still
Paid. If they do not match, fail closed: acknowledge the webhook, do not confirm the
order, and raise it for manual review.
Look orders up by the payment id you stored yourself. Never by a reference, description or metadata value echoed back by the provider. Those fields round-trip through a system you do not control and, in some flows, through input a customer can influence. The payment id you persisted when you created the payment is the only safe join key.
Treat an unrecognised payment status as "defer, take no action." Not as a failure, and
not as a cancel path. Providers add statuses; a match arm with a catch-all that releases
stock or refunds will happily cancel real, paid orders the day Mollie ships a new state.
The correct catch-all does nothing and leaves the order for the next webhook or for your
reconciliation sweep to resolve.
Further integrator responsibilities — forward-only status transitions under a row lock, webhook response discipline, reconciliation, rate limiting — are listed in SECURITY.md.
To report a vulnerability in paykit itself, email stefan@hofman-consulting.nl. Please do
not open a public issue.
Minimum supported Rust version
Rust 1.88. The MSRV is verified in CI against the committed Cargo.lock.
Raising the MSRV is treated as a semver-visible change and will come with at least a minor version bump while this crate is pre-1.0.
Contributing
Issues and pull requests are welcome. Before opening a PR, please run:
The last one matters: the core is meant to be transport-agnostic, and it is easy to leak a
reqwest reference into it without noticing.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.