Expand description
§inflow-mpp-seller
Accept MPP payments for a Service through InFlow. Create a signed challenge for an endpoint, validate the buyer’s credential, and settle before delivering the paid resource. Supported methods are InFlow, Tempo, Stripe, and CARD charge.
Source · MPP · Core codecs
Add inflow-mpp-seller, inflow-mpp, and serde_json to use the examples below.
The handler can run inside any HTTP framework using the application’s Tokio runtime.
§Account and setup
For a runnable HTTP server and matching Buyer, follow the Sandbox example walkthrough.
Use an InFlow Seller account and its API key. A Developer account does not
authorize Seller configuration, validation, or settlement.
Sandbox and production
have separate accounts and credentials. Select Environment::Sandbox for testing;
production is the default.
The challenge-signing secret belongs to your Service and is separate from the InFlow API key. Use a randomly generated secret of at least 32 bytes, store it securely, and share it across instances that accept the same challenges. Changing it invalidates outstanding challenges.
use inflow_mpp_seller::{
Authentication, CancellationToken, ChallengeOptions, ClientOptions,
Environment, Method, Offer, Seller, SellerOptions,
};
use serde_json::json;
async fn configure() -> Result<Offer, Box<dyn std::error::Error>> {
let seller = Seller::new(
ClientOptions {
environment: Environment::Sandbox,
authentication: Authentication::ApiKey(std::env::var("INFLOW_API_KEY")?),
..Default::default()
},
SellerOptions {
realm: "api.example.com".into(),
secret_key: std::env::var("MPP_SECRET_KEY")?,
},
&CancellationToken::new(),
).await?;
Ok(seller.offer(
Method::Inflow,
json!({"amount": "0.10", "currency": "USDC"}),
ChallengeOptions {
description: Some("One search request".into()),
requires_auth: true,
..Default::default()
},
)?)
}Seller::new loads authenticated Seller configuration before returning. If it
fails, handle the error and retry construction when appropriate. Clone the
constructed Seller for concurrent routes; clones reuse its configuration.
Use your application’s Tokio runtime; the SDK does not start another runtime.
§Set the price and payment method
An Offer holds the endpoint’s expected terms. Construct it from trusted Service
configuration, not from the buyer’s submitted credential.
- InFlow amounts are decimal strings, such as
"0.10". Configuration supplies the Seller recipient and supported currency rails. If a currency supports multiple rails, specifymethodDetails.rail. ProvideinstrumentIdwhen the advertised instrument rail requires it. - Tempo amounts are base-unit strings, not decimal currency amounts. Supply the
token address in
currencyand the receiving address inrecipient.methodDetailsdefaults tofeePayer: falseandsupportedModes: ["pull"].
The SDK checks the credential against the Offer, including rail and method-specific transfer terms, before calling InFlow. Upstream MPP verifies the signed challenge, expiration, and any request-body digest.
§Stripe tokens and Visa CARD
Use Method::Stripe to accept an external payer’s Stripe Shared Payment Token.
Use Method::Card to accept an encrypted Visa network-token credential. Neither
method decrypts tokens or contacts Stripe from your application: InFlow performs
credential verification and processing through the existing validate/broadcast flow.
Both methods accept decimal USD strings, from "0.50" through "999999.99".
For example, seller.offer(Method::Card, json!({"amount":"1.25"}), options)
creates a challenge containing "amount":"125" in cents. Extra fractional digits
are rejected, not rounded. This differs from Tempo’s base-unit input.
Authenticated Seller configuration supplies Stripe’s network profile and allowed payment methods, or CARD’s merchant recipient, name, Visa network and public encryption key. Route values cannot override these fields. An unavailable capability prevents offer construction. Your application needs an InFlow Seller key, not a Stripe secret key.
- Both methods accept
externalIdof up to 255 characters, including an empty string. - Stripe accepts top-level
metadata, placed insidemethodDetailson the wire: up to 45 string entries, keys up to 40 characters and values up to 500 characters. Keys cannot be blank, contain brackets, or use the reserved namesexternalId,inflowMppTransactionId,mppChallengeId,mppIntent,mppMethod, orstripeNetworkProfile. A supplied challenge reference must match the credential’sexternalIdexactly. - CARD accepts top-level
billingRequired; omission and explicitfalseremain distinct.
Both methods require a successful receipt with the same method and challenge ID.
An absent payer source is represented as an empty string in the InFlow request;
external payers do not need an InFlow payer identity. Token contents and optional
billing fields are retained. See the runnable examples.
§Handle an HTTP request directly
requires_auth: true puts payment in Payment-Authorization, leaving
Authorization for your Service’s own authentication. It does not implement
or enforce that authentication: authenticate and authorize the caller before
accepting payment.
This framework-independent function shows the payment portion of a handler. Its return value contains the response status, payment header, and body for your framework to write. Read the incoming body with your framework’s size limit and pass the exact bytes, without parsing and re-serializing them.
use inflow_mpp_seller::{
CancellationToken, Error, Offer, decode_credential, render_challenge,
};
async fn payment_response(
offer: &Offer,
payment_authorization: Option<&str>,
body: &[u8],
cancellation: &CancellationToken,
) -> Result<(u16, &'static str, String, &'static str), Error> {
let Some(header) = payment_authorization else {
let challenge = offer.challenge(Some(body))?;
return Ok((402, "WWW-Authenticate", render_challenge(&challenge)?, ""));
};
let (_, encoded) = header.split_once(' ')
.filter(|(scheme, _)| scheme.eq_ignore_ascii_case("Payment"))
.ok_or_else(|| Error::new("INVALID_MPP_DATA", "expected Payment credential"))?;
let credential = decode_credential(encoded.trim())?;
let receipt = offer.accept(&credential, Some(body), cancellation).await?;
let header = inflow_mpp::encode(&serde_json::to_value(receipt)
.map_err(|_| Error::new("INVALID_MPP_DATA", "invalid receipt"))?)?;
// Run the paid operation only after accept returns a receipt.
Ok((200, "Payment-Receipt", header, "paid result"))
}Read Payment-Authorization for the configuration above, or Authorization
when requires_auth is false. On the first request, return 402 with the rendered
challenge. On the retried request, decode the payment credential and call
accept. Attach its encoded receipt to the successful resource response.
Use Cache-Control: no-store for payment challenges and payment responses.
Map errors deliberately: rejected payment is not a successful resource response,
and a network or platform failure must not invite the buyer to make another
payment automatically. Error retains the platform’s HTTP metadata and problem
body. Never log the payment credential, API key, or challenge-signing secret.
§Validation, settlement, and cancellation
validate is non-mutating and returns payment details; it is not proof of
settlement and does not authorize resource delivery. accept performs validation
again and then calls the terminal settlement operation. Do not deliver the paid
resource based only on a prior validate result.
For an InFlow instrument payment, accept also requires the receipt’s method and
challenge identifier to match the submitted credential. A missing or mismatched
identifier is an error, not permission to deliver the resource or charge again.
When the platform enables idempotency keys, each acceptance call generates one key and retains it across that call’s transport retries. A separate call gets a separate key. The platform remains authoritative for credential replay. Both configurations use the shared client’s bounded retry policy.
Cancellation stops local requests; it is not payment reversal. A lost settlement response, a cancelled request, or a handler failure after settlement can leave a payment completed. Do not automatically charge again to recover resource delivery. Your application owns delivery recovery.
Challenges expire five minutes after creation unless expires is supplied.
Keep the same body bytes for challenge creation and acceptance when using body
binding. Pass None only when the route deliberately does not bind payment to
request-body contents.
§Upstream differences and limitations
Malformed validation responses and missing or mismatched receipts return
MPP_PAYMENT_FAILED with an internal-payment-error problem and status 500 in
Error.body. Explicit platform problems retain their original fields. Do not map
every MPP_PAYMENT_FAILED to HTTP 402: inspect the problem status. An internal
failure during settlement can leave the payment outcome unknown; it is not an
instruction to create another payment.
- Expected-offer checks include InFlow and Tempo method-specific terms. Upstream issue #555 tracks the corresponding upstream binding support.
- Seller subscriptions are not exposed because upstream lacks the Seller intent implementation. Issue #554 tracks it. Buyer subscription support is separate.
- Framework integration: use the direct handler integration shown above. The optional Tower adapter is deferred because upstream Tower middleware returns empty error bodies. Applications need a useful explanation when payment fails, including when the server cannot confirm the result. Upstream’s Axum extractors provide problem bodies, but switching to extractors would change the integration design; it is not a prerequisite for using this SDK. We retain the Tower design and track response-body support in upstream issue #561, following PR #497. Direct Seller integrations and SDK releases do not depend on that optional adapter.
Structs§
- Cancellation
Token - A token which can be used to signal a cancellation request to one or more tasks.
- Challenge
Options - Client
Options - Credential
- A credential retaining the complete challenge, including its display description. Encoding and decoding do not verify payment or establish settlement.
- Error
- Offer
- Payment
Challenge - Payment challenge from server (parsed from WWW-Authenticate header).
- Receipt
- Payment receipt from server (parsed from Payment-Receipt header).
- Seller
- Seller
Options - Validation
- Non-mutating acceptance result. It is not a receipt and does not authorize resource delivery.