Quick Start

Accept a payment

Run a paid endpoint, purchase its response, and confirm the payment in your InFlow dashboard. This quickstart uses Sandbox and charges 0.01 USDC from the buyer’s InFlow balance.

The example uses MPP. For the other supported payment protocol, see Accept x402.

Before you start

  • A Sandbox Seller account and its API key. Follow Accounts and environments to create the key.
  • A separate Sandbox buyer account with at least 0.01 USDC available in its InFlow balance.
  • The runtime for your chosen language: Go, Node.js, Python, or Rust.
  • The installed InFlow CLI binary to make the test purchase.

Log in as the buyer and open Sandbox deposits if you need funds. Select USDC and follow the displayed network and deposit instructions. Use the appropriate test assets; do not send mainnet funds to a test-network address. An API key does not fund the account.

The seller also needs a private signing secret. This secret authenticates the payment challenges issued by your application; it is separate from the InFlow API key. Generate a value once:

openssl rand -hex 32

Keep that value in private configuration. Reuse it across requests, restarts, and instances of this application rather than generating a new secret for each request.

In your seller terminal, replace both placeholders below. Use the Seller API key’s Private Key and your generated signing secret. Do not commit either value.

export INFLOW_API_KEY='YOUR_SANDBOX_SELLER_PRIVATE_KEY'
export MPP_SECRET_KEY='YOUR_PRIVATE_CHALLENGE_SIGNING_SECRET'

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

Create the paid endpoint

Choose one language. Each application listens at http://127.0.0.1:3000 and exposes two GET routes:

  • /free returns {"ok":true} without payment.
  • /api/report costs 0.01 USDC and returns a sample report after successful payment.

Use Go 1.26 or later. The Go example uses the standard HTTP server. The SDK’s Protect handler processes payment before calling the report handler.

Create a project and install the published dependencies:

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

Save the following application as main.go:

package main

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

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

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() error {
	key, secret := os.Getenv("INFLOW_API_KEY"), os.Getenv("MPP_SECRET_KEY")
	if key == "" || secret == "" {
		return fmt.Errorf("set INFLOW_API_KEY and MPP_SECRET_KEY")
	}
	client, err := seller.New(inflow.Options{
		Environment: inflow.Sandbox,
		APIKey:      key,
	})
	if err != nil {
		return err
	}
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	if err := client.Load(ctx); err != nil {
		return err
	}
	offer := seller.Offer{Charge: &mpp.ChargeRequest{
		Amount: "0.01", Currency: "USDC",
	}}
	if _, err := client.Prepare(ctx, offer); err != nil {
		return err
	}
	opaque := "R0VUIC9hcGkvcmVwb3J0"
	paid, err := client.Protect(seller.Route{
		Realm:     "report-service",
		SecretKey: secret,
		Offers:    []seller.Offer{offer},
		Opaque:    &opaque,
	}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintln(w, `{"report":"Your sample report is ready."}`)
	}))
	if err != nil {
		return err
	}
	mux := http.NewServeMux()
	mux.Handle("GET /api/report", paid)
	mux.HandleFunc("GET /free", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintln(w, `{"ok":true}`)
	})
	server := &http.Server{
		Addr: "127.0.0.1:3000", Handler: mux,
		ReadHeaderTimeout: 5 * time.Second,
	}
	log.Println("Listening on http://127.0.0.1:3000")
	return server.ListenAndServe()
}

The Opaque value identifies this report route in the signed challenge. Use a different value for another paid route.

In the terminal where you configured the Seller key and signing secret, start the server:

go run .

Use Node.js 22 or later. The Node.js example uses Express and mppx. The payments.charge middleware processes payment before the report handler runs.

Create a project and install the published dependencies:

mkdir inflow-report
cd inflow-report
npm init -y
npm install @inflowpayai/mpp-seller@0.11.0 express@5.2.1 mppx@0.8.17

Save the following application as server.mjs:

import express from 'express';
import { Mppx } from 'mppx/express';
import { inflow, paymentHttpTransport } from '@inflowpayai/mpp-seller';

const apiKey = process.env.INFLOW_API_KEY;
const secretKey = process.env.MPP_SECRET_KEY;
if (!apiKey || !secretKey) {
  throw new Error('Set INFLOW_API_KEY and MPP_SECRET_KEY.');
}

const payments = Mppx.create({
  methods: [inflow({ apiKey, environment: 'sandbox' })],
  secretKey,
  transport: paymentHttpTransport(),
});

const app = express();
app.get('/free', (_request, response) => response.json({ ok: true }));
app.get('/api/report',
  payments.charge({ amount: '0.01', currency: 'USDC' }),
  (_request, response) => {
    response.set('Cache-Control', 'no-store');
    response.json({ report: 'Your sample report is ready.' });
  },
);

app.listen(3000, '127.0.0.1', () => {
  console.log('Listening on http://127.0.0.1:3000');
});

Keep the InFlow method and its transport together in Mppx.create. The transport handles payment responses from the InFlow integration.

In the terminal where you configured the Seller key and signing secret, start the server:

node server.mjs

Use Python 3.11 or later. The Python example uses FastAPI and pympp. The @pay decorator processes payment before calling the report handler.

Create a project and install the published dependencies:

mkdir inflow-report
cd inflow-report
python3 -m venv .venv
.venv/bin/python -m pip install 'inflowpay[mpp,fastapi]==0.3.2' 'uvicorn==0.54.0'

Save the following application as server.py:

import asyncio
import os

import uvicorn
from fastapi import FastAPI, Request
from mpp import Credential, Receipt
from mpp.server.decorator import pay
from starlette.responses import JSONResponse

from inflowpay import ClientOptions
from inflowpay.mpp.seller import Seller


async def main() -> None:
    key, secret = os.environ["INFLOW_API_KEY"], os.environ["MPP_SECRET_KEY"]
    options = ClientOptions(environment="sandbox", api_key=key)
    async with await Seller.create(options) as seller:
        terms = seller.charge_request({"amount": "0.01", "currency": "USDC"})
        app = FastAPI()

        @app.get("/free")
        async def free() -> dict[str, bool]:
            return {"ok": True}

        @app.get("/api/report")
        @pay(
            intent=seller, method=seller.method, request=terms,
            realm="report-service", secret_key=secret,
        )
        async def report(
            request: Request, credential: Credential, receipt: Receipt,
        ) -> JSONResponse:
            return JSONResponse(
                {"report": "Your sample report is ready."},
                headers={
                    "Payment-Receipt": receipt.to_payment_receipt(),
                    "Cache-Control": "no-store",
                },
            )

        await uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=3000)).serve()


if __name__ == "__main__":
    asyncio.run(main())

Keep the Seller client open for the server’s lifetime. The handler includes the payment receipt in the successful response.

In the terminal where you configured the Seller key and signing secret, start the server:

.venv/bin/python server.py

Use Rust 1.93 or later. The Rust example uses Axum. It returns a payment challenge for an unpaid request and calls offer.accept before returning the report.

Create a project and install the published dependencies:

cargo new --bin inflow-report
cd inflow-report
cargo add inflow-mpp-seller@0.4.1 inflow-mpp@0.4.1 serde_json@1 axum@0.8
cargo add tokio@1 --features macros,rt-multi-thread,net,signal

Save the following application as src/main.rs:

use axum::{
    Json, Router,
    extract::State,
    http::{HeaderMap, HeaderValue, StatusCode},
    response::{IntoResponse, Response},
    routing::get,
};
use inflow_mpp_seller::{
    Authentication, CancellationToken, ChallengeOptions, ClientOptions, Environment, Method, Offer,
    Seller, SellerOptions,
};
use serde_json::json;

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

#[tokio::main]
async fn main() -> Result<()> {
    let options = ClientOptions {
        environment: Environment::Sandbox,
        authentication: Authentication::ApiKey(std::env::var("INFLOW_API_KEY")?),
        ..Default::default()
    };
    let seller = Seller::new(
        options,
        SellerOptions {
            realm: "report-service".into(),
            secret_key: std::env::var("MPP_SECRET_KEY")?,
        },
        &CancellationToken::new(),
    )
    .await?;
    let offer = seller.offer(
        Method::Inflow,
        json!({"amount":"0.01", "currency":"USDC", "methodDetails":{"rail":"balance"}}),
        ChallengeOptions::default(),
    )?;
    let app = Router::new()
        .route("/free", get(|| async { Json(json!({"ok":true})) }))
        .route("/api/report", get(report))
        .with_state(offer);
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
    println!("Listening on http://127.0.0.1:3000");
    axum::serve(listener, app)
        .with_graceful_shutdown(async {
            let _ = tokio::signal::ctrl_c().await;
        })
        .await?;
    Ok(())
}

async fn report(State(offer): State<Offer>, headers: HeaderMap) -> Response {
    let mut response = match paid_report(&offer, &headers).await {
        Ok(response) => response,
        Err(_) => (
            StatusCode::BAD_GATEWAY,
            "Payment could not be confirmed. Check its status before retrying.",
        )
            .into_response(),
    };
    response
        .headers_mut()
        .insert("cache-control", HeaderValue::from_static("no-store"));
    response
}

async fn paid_report(offer: &Offer, headers: &HeaderMap) -> Result<Response> {
    let Some(header) = headers.get("authorization") else {
        let challenge = inflow_mpp_seller::render_challenge(&offer.challenge(None)?)?;
        return Ok((
            StatusCode::PAYMENT_REQUIRED,
            [("www-authenticate", challenge)],
        )
            .into_response());
    };
    if headers.get_all("authorization").iter().count() != 1 {
        return Ok((StatusCode::BAD_REQUEST, "Send one Payment credential.").into_response());
    }
    let encoded = header
        .to_str()
        .ok()
        .and_then(|value| value.split_once(' '))
        .filter(|(scheme, _)| scheme.eq_ignore_ascii_case("Payment"));
    let Some((_, encoded)) = encoded else {
        return Ok((StatusCode::BAD_REQUEST, "Expected Payment authorization.").into_response());
    };
    let credential = match inflow_mpp_seller::decode_credential(encoded.trim()) {
        Ok(value) => value,
        Err(_) => {
            return Ok((StatusCode::BAD_REQUEST, "Invalid Payment credential.").into_response());
        }
    };
    let receipt = offer
        .accept(&credential, None, &CancellationToken::new())
        .await?;
    let receipt = inflow_mpp::encode(&serde_json::to_value(receipt)?)?;
    Ok((
        [("payment-receipt", receipt)],
        Json(json!({"report":"Your sample report is ready."})),
    )
        .into_response())
}

The explicit Rust handler also writes the receipt header. A failure to confirm payment returns an error without delivering the report.

In the terminal where you configured the Seller key and signing secret, start the server:

cargo run

Leave the server running while you complete the buyer steps. Replace the sample report response with your application’s result when adapting the example. Keep that work after successful payment processing.

These are local examples bound to your own computer. Press Control-C in the seller terminal when you have finished.

Check the unpaid response

Open a second terminal and call both routes without payment credentials:

curl -i http://127.0.0.1:3000/free
curl -i http://127.0.0.1:3000/api/report

The free route returns 200 OK with {"ok":true}. The report route returns 402 Payment Required and a WWW-Authenticate: Payment header.

The challenge advertises an inflow charge for 0.01 USDC, paid from an InFlow balance. Some challenge fields are encoded; the inspect command below shows the payment terms in readable form.

A 402 response confirms that your endpoint advertises payment requirements. It does not prove that a buyer can pay or that payment reaches your Seller account.

Make a test payment

Use a separate buyer terminal without the Seller’s INFLOW_API_KEY environment variable. Log in with the buyer’s Sandbox account:

inflow auth login --sandbox

Complete the login instructions printed by the CLI before continuing. If the vault is locked, unlock it when prompted. Then check the buyer’s available balance:

inflow balances list --sandbox

Inspect the report endpoint without paying:

inflow mpp inspect http://127.0.0.1:3000/api/report --sandbox

Check that the displayed terms match 0.01 USDC and the inflow method. Then make the purchase:

inflow mpp pay http://127.0.0.1:3000/api/report --sandbox --interval 5

This command makes a payment. The --interval 5 option keeps the command waiting while a payment is pending, including when approval is required.

If InFlow requests approval, open the provided link and approve the expected report purchase for 0.01 USDC. The CLI continues after approval.

Confirm the result

A successful purchase returns HTTP 200 and this report body:

{"report":"Your sample report is ready."}
  1. Confirm that the CLI received the report, not another payment challenge or an error response.
  2. Check the successful payment receipt and keep its reference.
  3. Log in as the Seller and open Sandbox transactions. Confirm the buyer, currency, amount, and successful transaction status.

Together, these checks confirm payment and delivery for this endpoint. They do not test every failure case or prove that your application’s own buyer code works.

If the request times out, do not assume the payment declined or start another purchase. Keep the original identifiers and follow Status and recovery to check the outcome.

Continue your integration

Accept MPP

Adapt payment acceptance to your application’s routes and supported payment options.

Accept x402

Expose a paid endpoint through x402 when your buyers use that protocol.

Test your integration

Test approvals, failures, retries, and delivery before relying on the integration.