Skip to main content

Crate inflow_mpp_seller

Crate inflow_mpp_seller 

Source
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, specify methodDetails.rail. Provide instrumentId when the advertised instrument rail requires it.
  • Tempo amounts are base-unit strings, not decimal currency amounts. Supply the token address in currency and the receiving address in recipient. methodDetails defaults to feePayer: false and supportedModes: ["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 externalId of up to 255 characters, including an empty string.
  • Stripe accepts top-level metadata, placed inside methodDetails on 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 names externalId, inflowMppTransactionId, mppChallengeId, mppIntent, mppMethod, or stripeNetworkProfile. A supplied challenge reference must match the credential’s externalId exactly.
  • CARD accepts top-level billingRequired; omission and explicit false remain 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§

CancellationToken
A token which can be used to signal a cancellation request to one or more tasks.
ChallengeOptions
ClientOptions
Credential
A credential retaining the complete challenge, including its display description. Encoding and decoding do not verify payment or establish settlement.
Error
Offer
PaymentChallenge
Payment challenge from server (parsed from WWW-Authenticate header).
Receipt
Payment receipt from server (parsed from Payment-Receipt header).
Seller
SellerOptions
Validation
Non-mutating acceptance result. It is not a receipt and does not authorize resource delivery.

Enums§

Authentication
Environment
Method

Functions§

decode_credential
render_challenge