Quick Start

Make a payment

Call a paid endpoint from your application, approve the payment when requested, and receive the response. This example buys a report for 0.01 USDC in Sandbox.

The example uses MPP and pays from the buyer’s InFlow balance. For endpoints using the other supported payment protocol, see Pay with x402.

Before you start

Start the report endpoint from Accept a payment and leave it running. You can use any of that page’s four server examples; the buyer and seller do not need to use the same language.

  • Use a separate Sandbox buyer account with at least 0.01 USDC available in its InFlow balance.
  • Create an API key for that buyer. A Developer account can buy; a Seller account can also buy.
  • Install the runtime for your chosen language: Go, Node.js, Python, or Rust.

Follow Accounts and environments for API key setup. If the buyer needs funds, open Sandbox deposits, select USDC, and follow the displayed network instructions. Use test assets, not mainnet funds.

Open a separate terminal for the buyer. Set INFLOW_API_KEY to the Private Key from the buyer’s Sandbox dashboard, replacing the placeholder below. Keep the key private and out of source control.

export INFLOW_API_KEY='your-buyer-sandbox-private-key'

The buyer does not need the seller’s API key or MPP signing secret. Logging into the InFlow CLI does not configure this program; the SDK reads the API key supplied here.

The shell commands use macOS or Linux syntax. On Windows, set the same environment variable in PowerShell and use .venv/Scripts/python.exe for the Python environment.

Create the buyer

Choose one language. Each program calls http://127.0.0.1:3000/api/report in Sandbox and prints the response and seller’s receipt reference. Use the report endpoint you started above, which offers one 0.01 USDC charge.

Before adapting the buyer to other sellers, decide which prices and payment options your application may accept. See Choose payment options for that next step.

Use Go 1.26 or later. The Go buyer’s Do method sends the first request, handles the payment challenge, waits when needed, and sends one paid retry.

Create a separate buyer project and install the published dependencies:

mkdir inflow-buyer
cd inflow-buyer
go mod init example.com/inflow-buyer
go get github.com/inflowpayai/inflow-go/mpp/buyer@v0.3.2

Save this program as main.go:

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

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

func main() {
	if err := run(); err != nil {
		log.Printf("%v; check Sandbox transactions before trying again", err)
		os.Exit(1)
	}
}

func run() error {
	key := os.Getenv("INFLOW_API_KEY")
	if key == "" {
		return fmt.Errorf("set INFLOW_API_KEY to the buyer's Sandbox key")
	}
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()
	ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
	defer cancel()

	client, err := buyer.New(buyer.Options{Options: inflow.Options{
		Environment: inflow.Sandbox, APIKey: key,
	}})
	if err != nil {
		return err
	}
	request, err := http.NewRequestWithContext(
		ctx, http.MethodGet, "http://127.0.0.1:3000/api/report", nil,
	)
	if err != nil {
		return err
	}
	fmt.Println("Requesting report. Approve in Sandbox if requested.")
	response, err := client.Do(request, buyer.PaymentOptions{})
	if err != nil {
		return err
	}
	defer response.Body.Close()
	fmt.Printf("HTTP %d\n", response.StatusCode)
	if _, err := io.Copy(os.Stdout, response.Body); err != nil {
		return err
	}
	fmt.Println()
	if response.StatusCode != http.StatusOK {
		return fmt.Errorf("report request did not succeed")
	}
	receipt, err := mpp.DecodeReceipt(response.Header.Get("Payment-Receipt"))
	if err != nil {
		return fmt.Errorf("report response has no readable payment receipt: %w", err)
	}
	fmt.Printf("Seller receipt: %s / %s\n", receipt.Method, receipt.Reference)
	return nil
}

The Go client does not follow redirects. The request context limits waiting to 15 minutes and responds to Control-C.

Use Node.js 22 or later. The Node.js buyer uses mppx to handle the payment challenge and paid request. Only payments.fetch is payment-aware; the program leaves global fetch unchanged.

Create a separate buyer project and install the published dependencies:

mkdir inflow-buyer
cd inflow-buyer
npm init -y
npm install @inflowpayai/mpp-buyer@0.8.1 mppx@0.8.17

Save this program as buyer.mjs:

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

const apiKey = process.env.INFLOW_API_KEY;
if (!apiKey) throw new Error("Set INFLOW_API_KEY to the buyer's Sandbox key.");

const method = inflow({ apiKey, environment: 'sandbox', timeoutMs: 900_000 });
const payments = Mppx.create({
  methods: [method],
  polyfill: false,
  maxPaymentRetries: 1,
});
const controller = new AbortController();
const stop = () => {
  controller.abort();
  method.cleanup();
};
const timer = setTimeout(stop, 900_000);
process.once('SIGINT', stop);

try {
  console.log('Requesting report. Approve in Sandbox if requested.');
  const response = await payments.fetch('http://127.0.0.1:3000/api/report', {
    redirect: 'error',
    signal: controller.signal,
  });
  console.log('HTTP', response.status);
  console.log(await response.text());
  if (response.status !== 200) throw new Error('Report request did not succeed.');
  if (!response.headers.has('Payment-Receipt')) {
    throw new Error('Report response has no payment receipt.');
  }
  const receipt = Receipt.fromResponse(response);
  console.log('Seller receipt:', receipt.method, '/', receipt.reference);
} catch (error) {
  console.error(error instanceof Error ? error.message : 'Request failed.');
  console.error('Check Sandbox transactions before trying again.');
  process.exitCode = 1;
} finally {
  clearTimeout(timer);
  process.removeListener('SIGINT', stop);
  method.cleanup();
}

The program allows one paid attempt, refuses redirects, and stops waiting after 15 minutes or Control-C.

Use Python 3.11 or later. The Python buyer supplies a payment transport to httpx. The transport handles the challenge and sends one paid retry.

Create a separate buyer project and install the published dependencies:

mkdir inflow-buyer
cd inflow-buyer
python3 -m venv .venv
.venv/bin/python -m pip install 'inflowpay[mpp]==0.3.2'

Save this program as buyer.py:

import asyncio
import os
import sys

import httpx
from inflowpay import ClientOptions, InflowApiError
from inflowpay.mpp import decode_receipt
from inflowpay.mpp.buyer import (
    BuyerMethod,
    MppPaymentExpiredError,
    MppPaymentFailedError,
    payment_transport,
)


async def main() -> None:
    key = os.environ["INFLOW_API_KEY"]
    options = ClientOptions(environment="sandbox", api_key=key)
    async with (
        BuyerMethod(options, pending_timeout=900) as method,
        httpx.AsyncClient(
            transport=payment_transport([method]),
            follow_redirects=False,
            timeout=30,
        ) as client,
    ):
        print("Requesting report. Approve in Sandbox if requested.", flush=True)
        async with asyncio.timeout(900):
            response = await client.get("http://127.0.0.1:3000/api/report")
        print(f"HTTP {response.status_code}\n{response.text}")
        if response.status_code != 200:
            raise RuntimeError("Report request did not succeed.")
        encoded = response.headers.get("Payment-Receipt")
        if encoded is None:
            raise RuntimeError("Report response has no payment receipt.")
        receipt = decode_receipt(encoded)
        print(f"Seller receipt: {receipt['method']} / {receipt['reference']}")


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except (
        InflowApiError,
        MppPaymentExpiredError,
        MppPaymentFailedError,
        httpx.HTTPError,
        KeyError,
        OSError,
        RuntimeError,
        ValueError,
        KeyboardInterrupt,
    ) as error:
        print(f"Request stopped: {error}", file=sys.stderr)
        print("Check Sandbox transactions before trying again.", file=sys.stderr)
        raise SystemExit(1) from None

Keep both clients open until the request finishes. Redirects are disabled, and the program allows up to 15 minutes for the complete request.

Use Rust 1.93 or later. The Rust buyer handles the HTTP steps explicitly: read the challenge, select an InFlow charge, prepare the payment, wait for a credential, and send the paid request.

Create a separate buyer project and install the published dependencies:

cargo new inflow-buyer
cd inflow-buyer
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 macros,rt-multi-thread,signal

Save this program as src/main.rs:

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

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

#[tokio::main]
async fn main() -> ExitCode {
    let token = CancellationToken::new();
    let operation = buy(&token);
    tokio::pin!(operation);
    let result = tokio::select! {
        result = &mut operation => result,
        signal = tokio::signal::ctrl_c() => {
            token.cancel();
            match signal {
                Ok(()) => operation.await,
                Err(error) => Err(error.into()),
            }
        }
    };
    match result {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("{error}; check Sandbox transactions before trying again");
            ExitCode::FAILURE
        }
    }
}

async fn buy(token: &CancellationToken) -> Result<()> {
    let buyer = Buyer::new(ClientOptions {
        environment: Environment::Sandbox,
        authentication: Authentication::ApiKey(std::env::var("INFLOW_API_KEY")?),
        ..Default::default()
    })?;
    let http = reqwest::Client::builder()
        .redirect(reqwest::redirect::Policy::none())
        .retry(reqwest::retry::never())
        .timeout(Duration::from_secs(30))
        .build()?;
    let url = "http://127.0.0.1:3000/api/report";
    println!("Requesting report. Approve in Sandbox if requested.");
    let response = http.get(url).send().await?;
    if response.status() != 402 {
        return Err(format!(
            "Expected a payment challenge, got HTTP {}; no payment was started",
            response.status()
        )
        .into());
    }
    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 payment = buyer
        .prepare(challenge, PaymentOptions::default(), token)
        .await?;
    if let Some(id) = payment.approval_id() {
        println!("Pending approval: {id}");
    }
    let credential = payment.wait(WaitOptions::default()).await?;
    if token.is_cancelled() {
        return Err("Request cancelled; cancellation does not reverse payment".into());
    }
    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);
    let response = http.get(url).header(header, value).send().await?;
    let status = response.status();
    let encoded = response.headers().get("Payment-Receipt").cloned();
    println!("HTTP {status}\n{}", response.text().await?);
    if status != 200 {
        return Err("Report request did not succeed".into());
    }
    let encoded = encoded.ok_or("Report response has no payment receipt")?;
    let receipt = inflow_mpp::decode_receipt(encoded.to_str()?)?;
    println!("Seller receipt: {} / {}", receipt.method, receipt.reference);
    Ok(())
}

The credential must retain the complete challenge. This program sends it once, disables redirects and HTTP retries, and uses the SDK’s 15-minute payment wait.

Keep the API key in the InFlow client configuration. The seller receives a payment credential, not your InFlow API key.

Run and approve the payment

Keep the seller process running. In the buyer terminal where you set the API key, run the command for your chosen language. This starts a purchase; run only one example.

go run .
node buyer.mjs
.venv/bin/python buyer.py
cargo run

The first request receives a payment challenge. The buyer SDK requests payment through InFlow and waits if your approval is needed. After approval, the program sends the payment credential to the seller and receives the report.

While the program is waiting, open Sandbox Approvals as the buyer. If a request is waiting for your approval, check the seller, 0.01 USDC amount, and currency before approving it. Leave the program running; it continues without a second invocation.

The program does not open a browser for you. If no approval is required, it can continue immediately. See Approvals and allowances for how payment permissions affect this step.

Confirm the result

A successful request prints HTTP 200 and the report body:

{"report":"Your sample report is ready."}
  1. Confirm that the response contains the report, rather than another payment challenge or an error.
  2. Keep the seller’s payment receipt reference printed by the program.
  3. Open Sandbox transactions as the buyer. Confirm the seller, 0.01 USDC amount, and successful transaction status.

The receipt reports the seller’s payment result; decoding it is not an independent settlement check. Use the dashboard record alongside the response to confirm this test purchase.

Receiving an unpaid 402 or obtaining a payment credential alone is not the end of this quickstart. The goal is to receive the purchased report and confirm its payment.

If the request does not finish

If InFlow reports that the payment failed or expired, the program stops rather than starting a second purchase. Read the error and check the transaction before deciding what to do next.

A timeout or lost connection does not necessarily mean the payment failed. Check the transaction before running the program again. Stopping the program does not reverse a completed payment.

Keep any transaction identifier or receipt reference that was returned. If none was printed, find the purchase in the buyer’s Sandbox transaction history. Follow Status and recovery before retrying an uncertain payment.

Continue your integration

Pay with MPP

Adapt the payment flow to your application’s requests and supported MPP methods.

Pay with x402

Call endpoints that advertise x402 payment requirements.

Approvals and allowances

Understand when a purchase needs approval and how users authorize payments.