Make Payments

Pay with MPP

Use the InFlow buyer SDK to request a resource protected by MPP. When the seller responds with a payment challenge, your application obtains a payment credential through InFlow and submits it to the seller. The seller processes the payment and returns the requested result.

Start in Sandbox with an endpoint you control and a known price. A payment-aware client can initiate a purchase when it receives a compatible challenge.

This guide covers one-time inflow/charge payments from application code. For a complete first-run walkthrough, use Make a payment. That quickstart provides the local report endpoint used here.

Prepare your buyer account

  • Use an InFlow account and API key from the same environment. A Developer account can buy; an existing Seller account can also buy.
  • Choose a seller endpoint offering a payment method your application supports.
  • Make the required funds or linked instrument available in the buyer’s account.
  • Keep access to the buyer’s dashboard so you can approve the purchase if InFlow requests approval.

Follow Accounts and environments to create the API key. The examples use Sandbox, not production. Supply the key from your application’s secret configuration rather than committing it to source control.

Your InFlow API key authenticates your application to InFlow. It is not a credential for the seller’s endpoint. If the seller requires separate authentication, provide that through the seller’s supported authentication mechanism.

MPP commonly sends its credential in Authorization: Payment …. Do not overwrite another required authorization header. The Go convenience client rejects that conflict; other integrations need an explicit authentication arrangement with the seller. These examples call an endpoint without separate seller authentication.

Understand the request sequence

  1. Request the seller’s resource.
  2. Read the 402 response and its WWW-Authenticate payment challenges.
  3. Select an acceptable offer and request its payment credential through InFlow.
  4. Wait for approval and credential readiness when required.
  5. Submit the credential to the seller and inspect the resulting response.

A ready credential means you can submit the payment. It does not mean the seller has completed the payment or delivered the result. A response that does not require payment can return without this sequence.

The SDK obtains the credential through InFlow’s buyer endpoints. Your application does not need the seller’s secret key or a local implementation of the payment signature.

Make a payment from code

Add the function for your language to an application. Call it with your Sandbox API key and http://127.0.0.1:3000/api/report from the quickstart. That endpoint offers one 0.01 USDC charge; fund a separate Sandbox buyer before calling it.

Each function returns the response text and prints the seller’s receipt reference when supplied. Errors propagate to your application. Catch them at the call site, report the outcome, and check the existing purchase before deciding to retry. These functions are integration snippets, not standalone command-line programs.

Use Go 1.26 or later. Install the dependency in your Go module:

go get github.com/inflowpayai/inflow-go/mpp/buyer@v0.3.2

Client.Do sends the initial request and one paid retry. It selects the first supported challenge in seller order. Use this convenience function with the known charge endpoint; use Prepare and Wait when your application must choose the challenge itself.

package payments

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"

    inflow "github.com/inflowpayai/inflow-go"
    "github.com/inflowpayai/inflow-go/mpp"
    "github.com/inflowpayai/inflow-go/mpp/buyer"
)

func BuyReport(ctx context.Context, apiKey, url string) (string, error) {
    ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
    defer cancel()
    client, err := buyer.New(buyer.Options{Options: inflow.Options{
        Environment: inflow.Sandbox, APIKey: apiKey,
    }})
    if err != nil {
        return "", err
    }
    request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return "", err
    }
    response, err := client.Do(request, buyer.PaymentOptions{})
    if err != nil {
        return "", err
    }
    defer response.Body.Close()
    if response.StatusCode < 200 || response.StatusCode >= 300 {
        return "", fmt.Errorf("HTTP %d; check the payment before retrying", response.StatusCode)
    }
    if encoded := response.Header.Get("Payment-Receipt"); encoded != "" {
        receipt, err := mpp.DecodeReceipt(encoded)
        if err != nil {
            return "", err
        }
        fmt.Println("Seller receipt:", receipt.Reference)
    }
    body, err := io.ReadAll(response.Body)
    return string(body), err
}

Pass your application’s request context to BuyReport. The function limits the operation to 15 minutes. The resource client does not follow redirects.

Use Node.js 22 or later and TypeScript. Install both the buyer package and its mppx peer dependency:

npm install @inflowpayai/mpp-buyer@0.8.1 mppx@0.8.17

Only payments.fetch can initiate payment in this example. polyfill: false leaves global fetch unchanged. Registering inflow accepts charge offers; it does not register the separate subscription method.

import { Mppx, inflow, Receipt } from '@inflowpayai/mpp-buyer';

export async function buyReport(apiKey: string, url: string): Promise<string> {
  const method = inflow({ apiKey, environment: 'sandbox', timeoutMs: 900_000 });
  const payments = Mppx.create({
    methods: [method], polyfill: false, maxPaymentRetries: 1,
  });
  const controller = new AbortController();
  const timer = setTimeout(() => {
    controller.abort();
    method.cleanup();
  }, 900_000);
  try {
    const response = await payments.fetch(url, {
      redirect: 'error', signal: controller.signal,
    });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}; check the payment before retrying`);
    }
    if (response.headers.has('Payment-Receipt')) {
      const receipt = Receipt.fromResponse(response);
      console.log('Seller receipt:', receipt.reference);
    }
    return await response.text();
  } finally {
    clearTimeout(timer);
    method.cleanup();
  }
}

The function allows one paid retry and refuses redirects. Its deadline aborts the HTTP request and stops the credential wait. Cleanup runs whether the request succeeds or fails.

Use Python 3.11 or later. Install the MPP dependencies in your application’s virtual environment:

python -m pip install 'inflowpay[mpp]==0.3.2'

BuyerMethod accepts inflow/charge by default. payment_transport connects it to httpx and limits automatic payment to one paid retry.

import asyncio

import httpx
from inflowpay import ClientOptions
from inflowpay.mpp import decode_receipt
from inflowpay.mpp.buyer import BuyerMethod, payment_transport


async def buy_report(api_key: str, url: str) -> str:
    async with (
        BuyerMethod(ClientOptions(environment="sandbox", api_key=api_key)) as method,
        httpx.AsyncClient(
            transport=payment_transport([method]),
            follow_redirects=False,
            timeout=30,
        ) as http,
    ):
        async with asyncio.timeout(900):
            response = await http.get(url)
        response.raise_for_status()
        encoded = response.headers.get("Payment-Receipt")
        if encoded is not None:
            receipt = decode_receipt(encoded)
            print(f"Seller receipt: {receipt['reference']}")
        return response.text

The asynchronous context managers close both clients. The overall deadline includes approval waiting; each HTTP operation also has a timeout. Redirects are disabled.

Use Rust 1.93 or later. Add these dependencies to your Cargo project:

cargo add inflow-mpp@=0.4.1 inflow-mpp-buyer@=0.4.1
cargo add reqwest@0.13 --no-default-features --features rustls
cargo add tokio@1 --features rt-multi-thread,macros

Rust makes the HTTP steps explicit. Select an InFlow charge, prepare the payment, wait for its credential, and send one paid request. The complete credential retains the seller’s challenge.

use inflow_mpp_buyer::{
    Authentication, Buyer, CancellationToken, ClientOptions, Environment,
    PaymentOptions, WaitOptions, encode_credential, parse_challenges,
};
use std::time::Duration;

type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

pub async fn buy_report(api_key: &str, url: &str) -> Result<String> {
    let buyer = Buyer::new(ClientOptions {
        environment: Environment::Sandbox,
        authentication: Authentication::ApiKey(api_key.to_owned()),
        ..Default::default()
    })?;
    let http = reqwest::Client::builder()
        .redirect(reqwest::redirect::Policy::none())
        .retry(reqwest::retry::never())
        .timeout(Duration::from_secs(30))
        .build()?;
    let mut response = http.get(url).send().await?;
    if response.status() == 402 {
        let headers = response.headers().get_all("WWW-Authenticate")
            .iter().map(|value| value.to_str())
            .collect::<std::result::Result<Vec<_>, _>>()?;
        let challenges = parse_challenges(&headers)?;
        let challenge = challenges.iter().find(|challenge| {
            challenge.method.as_str() == "inflow" && challenge.intent.as_str() == "charge"
        }).ok_or("No InFlow charge offer; no payment was started")?;
        drop(response);
        let cancellation = CancellationToken::new();
        let payment = buyer.prepare(challenge, PaymentOptions::default(), &cancellation).await?;
        if let Some(id) = payment.approval_id() {
            println!("Pending approval: {id}");
        }
        let credential = payment.wait(WaitOptions::default()).await?;
        let header = credential.challenge.header.as_deref().unwrap_or("Authorization");
        let mut value = reqwest::header::HeaderValue::from_str(&format!(
            "Payment {}", encode_credential(&credential)?
        ))?;
        value.set_sensitive(true);
        response = http.get(url).header(header, value).send().await?;
    }
    if !response.status().is_success() {
        return Err(format!("HTTP {}; check the payment before retrying", response.status()).into());
    }
    if let Some(encoded) = response.headers().get("Payment-Receipt") {
        let receipt = inflow_mpp::decode_receipt(encoded.to_str()?)?;
        println!("Seller receipt: {}", receipt.reference);
    }
    Ok(response.text().await?)
}

Call buy_report from your asynchronous application. Redirects and automatic HTTP retries are disabled. Each resource request has a 30-second timeout, and the SDK’s default credential wait is 15 minutes.

The examples send GET requests. For POST, keep the same method and body when submitting the paid request. A consumed stream might not be replayable. In Go, provide Request.GetBody; http.NewRequestWithContext sets it for supported readers such as bytes.Reader. For an explicit flow, retain the original body and reconstruct the request.

Choose which payments your application accepts

Supporting a payment method does not establish that every offered price is acceptable. Before accepting arbitrary seller URLs or multiple offers, decide which methods, amounts, and currencies your application permits.

These examples rely on a controlled endpoint with one known charge. They do not implement a general spending policy. A seller can change the price, and the first compatible offer need not be the cheapest. Check the current challenge before creating a payment when selection matters.

A normal HTTP client can read a payment challenge without creating a payment. The payment-aware calls above can proceed with a compatible offer. Do not use them as price-inspection calls. See Choose payment options for selection and spending constraints.

The offer determines the funding needed. Read InFlow balance or Linked cards for those prerequisites. Visa CARD and Subscriptions have separate setup and payment flows, outside this one-time-charge walkthrough.

Read the result and receipt

A successful resource response contains the seller’s result. Check its status and parse its body according to the endpoint’s contract. The examples return text so your application can decide how to interpret the content.

A Payment-Receipt reports the seller’s payment outcome. Retain the reference when you need to investigate or recover the purchase. Decoding a receipt does not independently verify settlement, and receiving a receipt does not prove the returned product is correct.

A free response may legitimately have no receipt. After submitting a payment, a missing or unreadable receipt does not prove that no money moved. Preserve the response and available payment references, then check the existing purchase rather than buying again.

Applications that need recovery across requests or restarts should retain the transaction ID and original credential through the SDK’s explicit payment operations. The convenience functions here are not durable recovery storage. Treat stored credentials as secrets and never print them or your InFlow API key.

Handle approval and interrupted requests

InFlow can return a pending transaction while the buyer’s approval is required. The SDK waits for the credential instead of sending an unfinished payment to the seller. Tell the buyer that approval may be needed and let them approve through InFlow. A purchase does not always require a fresh approval.

Rejection, expiration, and a timeout can stop the credential wait. Your application should report the outcome and stop automatic retries. Cancelling a local task does not reverse a completed payment, even when the SDK also attempts to cancel a pending approval.

A second 402 or a lost connection after submission needs investigation. Do not wrap the whole purchase function in a generic retry loop. A fresh call can create another payment; recovery must use the original purchase and available transaction information.

See Approvals and allowances for owner decisions and spending rules. See Status and recovery when the payment outcome is uncertain.

Test the complete purchase

Test the buyer against a running Sandbox seller, not only a simulated challenge. Confirm the purchased result and the recorded transaction. Credential creation alone does not establish that the seller accepted payment.

  • Complete a known-price charge and check the response, seller receipt, and corresponding InFlow transaction.
  • Reject a requested approval. Confirm that the application does not submit a payment credential or silently start another purchase.
  • Offer an unsupported method. Confirm that no payment is created. When you add price-selection rules, also test an offer that exceeds your permitted amount.
  • Return another 402 after submission. Confirm that the application reports the failure instead of creating a second payment.
  • Interrupt the response after payment submission. Confirm that the application checks the existing outcome before retrying.
  • Return a free response without a receipt. Confirm that receipt parsing is optional rather than an unconditional failure.

Keep these checks alongside your project’s unit tests, integration tests, and coverage requirements. Failure-injection tests are useful for lost responses and rejected retries. They complement a real Sandbox purchase; they do not replace it.

Continue your integration

Choose payment options

Limit purchases to the methods, amounts, and currencies your application accepts.

Approvals and allowances

Guide the buyer through required approval and permitted spending.

Status and recovery

Check an existing purchase before retrying an uncertain payment.