Subscriptions
Sell recurring access to your service instead of charging for every request. For example, a buyer can subscribe to a report service for 9.99 USDC per month and request reports throughout each paid period.
This guide covers the inflow method with the subscription intent in MPP. Payments come from the buyer’s InFlow balance. The examples do not use linked cards, Stripe, Visa CARD, or x402 for recurring billing.
Understand recurring access
The buyer approves the subscription terms, and the seller accepts the initial payment before granting access. InFlow records the subscription and manages later billing. Your application still decides what the subscription includes, such as the reports a subscriber can read.
Later requests use the existing subscription. The buyer gets a fresh credential for the seller’s current challenge and submits it to the endpoint. The seller checks that credential through InFlow before returning the report. A request within an already-paid period does not incur another subscription charge.
The recurring price is fixed by the subscription terms. Counting requests and billing their measured usage is not part of this flow. Application quotas, access permissions, and report delivery remain your application’s responsibility.
Check SDK support
| SDK | Buy and use subscriptions | Sell subscriptions |
|---|---|---|
| Go | Supported | Supported |
| Node.js | Supported | Supported |
| Python | Supported | Not supported |
| Rust | Supported | Not supported |
A Python or Rust buyer can subscribe to a Go or Node.js seller. The buyer and seller do not need to use the same language.
Sellers need an InFlow Seller account, its API key, and a private challenge-signing secret. Buyers need an authenticated InFlow account funded in the subscription currency. Keep both sides in the same environment; start in Sandbox. See Accounts and environments and InFlow balance for setup.
Define the subscription offer
Describe the access being sold and set its recurring terms. For the report service, the offer is 9.99 USDC every month. The subscription also has an end date, after which access and renewals stop.
| Field | Meaning |
|---|---|
| amount | The positive price per billing period, expressed in currency units. '9.99' means 9.99 USDC, not token base units. The initial payment also charges this amount. |
| currency | A supported balance currency, such as USDC. The buyer needs that balance for the initial payment and renewals. |
| periodUnit and periodCount | The billing interval. month with 1 means monthly; week with 2 means every two weeks. Other units include hour, day, quarter, and year. |
| subscriptionExpires | The subscription’s end date and time in RFC 3339 format. This is separate from the short-lived payment challenge’s expiry and must be later than that expiry. |
| externalId | An optional seller reference, such as reports-monthly. Use a distinct value to distinguish plans that otherwise share a price and billing interval. It is not the buyer’s subscription ID or a credential. |
InFlow records the billing schedule when the purchase starts. Read the subscription’s next billing date rather than calculating it from your application’s first successful report response. The examples use a fixed end date; choose and maintain an appropriate future date for your own offer.
These seller fragments extend Accept MPP. Keep its account setup, application authorization, input checks, and error handling. The same endpoint handles initial purchase and later subscription access.
Replace the charge offer in Accept MPP with this subscription offer. Keep the existing client.Protect route and handler, passing offer in its Offers list.
plan := "reports-monthly"
offer := seller.Offer{
Subscription: &mpp.SubscriptionRequest{
ChargeRequest: mpp.ChargeRequest{
Amount: "9.99", Currency: "USDC",
},
PeriodUnit: "month",
PeriodCount: 1,
SubscriptionExpires: "2027-12-31T23:59:59Z",
ExternalID: &plan,
},
}The fragment uses mpp and seller from the Go SDK. The protected handler runs only after InFlow accepts the purchase or subscription access, and the middleware adds the receipt.
Register the subscription method on the core Mppx server. Unlike the one-time-charge example, this uses a Fetch API handler, not the mppx/express factory. Supply the Seller API key and stable signing secret from private configuration.
import {
Mppx, inflow, inflowSubscriptions, paymentHttpTransport,
} from '@inflowpayai/mpp-seller';
export function createReportsHandler(apiKey: string, secretKey: string) {
const payments = Mppx.create({
methods: [inflow.subscription({ apiKey, environment: 'sandbox' })],
secretKey,
transport: paymentHttpTransport(),
});
const subscribe = inflowSubscriptions(payments, [{
amount: '9.99', currency: 'USDC',
periodUnit: 'month', periodCount: 1,
subscriptionExpires: '2027-12-31T23:59:59Z',
externalId: 'reports-monthly',
}]);
return async (request: Request): Promise<Response> => {
const result = await subscribe(request);
if (result.status === 402) return result.challenge;
return result.withReceipt(Response.json(
{ report: 'Your subscriber report is ready.' },
{ headers: { 'Cache-Control': 'no-store' } },
));
};
}Mount the returned handler at your report endpoint in a framework that accepts Web Requests and Responses. For Express, use inflowSubscriptionsNodeListener as shown in the Node.js seller subscription guide. Keep server errors as errors; do not convert them into successful report responses.
Existing subscriptions keep their agreed terms. Changing the endpoint’s price, currency, billing interval, or seller reference can prevent those subscribers from using the new offer. Keep an offer matching existing subscriptions while introducing a separately identified plan. Do not silently purchase a replacement when the old subscription no longer matches.
Purchase and use a subscription
- First purchase. Select the seller’s subscription offer and check its recurring price, period, and end date. Start the purchase without an existing subscription ID. Follow the required approval flow, then submit the resulting payment credential to the seller.
- Successful activation. The seller settles the initial payment and returns the report with a Payment-Receipt. Retain its subscriptionId with the buyer, seller, environment, and plan in your application. A prepared credential alone does not prove activation or delivery.
- Later access. Request the report again and supply the saved subscription ID to the buyer SDK. The SDK asks InFlow for a fresh credential tied to the current challenge. Submit that credential to the same seller and check the response and receipt.
The subscription ID identifies the agreement; it is not an access token. InFlow checks the authenticated buyer and the offer’s terms before issuing an access credential. Keep credentials private and do not reuse one as a permanent subscription token.
The fragments below adapt the buyer functions in Pay with MPP for later access. Use a known subscription endpoint and the saved ID. Retain that guide’s timeouts, response checks, receipt parsing, and cleanup. These are changes to that flow, not standalone applications.
Replace the client.Do call inside BuyReport. Add subscriptionID as a string parameter supplied from your stored subscription.
response, err := client.Do(request, buyer.PaymentOptions{
SubscriptionID: subscriptionID,
})For the initial purchase, use buyer.PaymentOptions{} instead. Go recognizes the subscription intent from the selected challenge.
In buyReport, replace the charge method with inflow.subscription. Add a string subscriptionId parameter and pass it in the fetch context. Keep the existing controller, timer, and finally cleanup.
const method = inflow.subscription({
apiKey, environment: 'sandbox', timeoutMs: 900_000,
});
const payments = Mppx.create({
methods: [method], polyfill: false, maxPaymentRetries: 1,
});
const response = await payments.fetch(url, {
context: { subscriptionId },
redirect: 'error', signal: controller.signal,
});For the initial purchase, omit context but keep the subscription method. Registering the ordinary inflow(...) charge method does not enable subscription purchases.
Add a string subscription_id parameter to buy_report. Replace its BuyerMethod construction with this expression, retaining as method and the surrounding asynchronous context managers.
BuyerMethod(
ClientOptions(environment="sandbox", api_key=api_key),
intent="subscription",
subscription_id=subscription_id,
)For the initial purchase, omit subscription_id but keep intent="subscription". The configured method is tied to the supplied subscription; construct it with the appropriate ID for each buyer and plan.
In buy_report, select inflow/subscription rather than inflow/charge. Add a subscription_id: &str parameter and replace the selection and preparation statements with these. Keep credential waiting and submission unchanged.
let challenge = challenges.iter().find(|challenge| {
challenge.method.as_str() == "inflow"
&& challenge.intent.as_str() == "subscription"
}).ok_or("No InFlow subscription offer; no purchase was started")?;
drop(response);
let cancellation = CancellationToken::new();
let payment = buyer.prepare(challenge, PaymentOptions {
subscription_id: Some(subscription_id.to_owned()),
..Default::default()
}, &cancellation).await?;For the initial purchase, use PaymentOptions::default() while still selecting the subscription challenge. Neither prepare nor wait sends the credential to the seller; retain that HTTP step from the complete example.
Supplying no subscription ID requests a new purchase. If the original payment succeeded but your application lost the receipt, recover that subscription before buying again. Buyers can inspect their Sandbox subscriptions or production subscriptions.
Applications can list owned subscriptions with GET /v1/subscriptions and inspect one with GET /v1/subscriptions/{subscriptionId}. Use your InFlow credentials against the matching API environment, not the seller’s report endpoint. The API reference describes these operations and their responses.
Handle renewals and insufficient funds
InFlow schedules recurring charges against the buyer’s balance. Your seller application does not need its own billing timer. Accessing the resource can also trigger settlement when the current billing period has not been paid.
If renewal fails because the balance is insufficient, the subscription becomes past due and InFlow schedules further attempts. Access requiring that unpaid period is denied unless settlement succeeds. A past-due status is not a promise of free access during retries.
Tell the buyer to fund the subscription currency in the same InFlow environment. Check the subscription’s status and next billing date before retrying access. Keep using the existing subscription ID; omitting it would request another purchase. InFlow can mark the subscription failed when its renewal retry window ends.
Preserve the existing payment details when a response is interrupted. Do not treat every error as a reason to subscribe again. Status and recovery explains how to separate an uncertain payment from a confirmed failure.
Cancel or end access
Buyers can cancel a subscription from its InFlow dashboard details. Sellers can revoke a subscription belonging to them. For application-driven cancellation, use the subscription ID with this InFlow API endpoint:
DELETE /v1/subscriptions/{subscriptionId}/deleteAuthenticate as the buyer or seller against the matching API environment. The caller’s role determines cancellation or revocation; a successful call returns HTTP 200.
Cancellation and revocation stop further billing and immediately prevent subscription access. They do not wait for the paid period to end, and they do not refund earlier payments. Explain this behavior to buyers before offering a cancellation control.
Reaching subscriptionExpires also ends access and renewals. Removing a plan from your seller endpoint is not a substitute for cancelling the existing subscriptions; those agreements remain recorded in InFlow.
Cancelling a pending payment approval is different from cancelling an active subscription. Buyer SDK cleanup and approval-cancellation helpers do not end a subscription. Use the subscription dashboard or cancellation endpoint for that operation.
Test the complete flow
Use controlled Sandbox buyer and seller accounts. For renewal testing, periodUnit: 'minute' with periodCount: 5 provides a short billing interval. Keep this testing plan separate from the customer-facing monthly offer, and cancel it when finished.
- Complete the first purchase. Confirm the report, receipt subscription ID, active subscription, and initial transaction in InFlow.
- Request another report within the paid period with the saved subscription ID. Confirm access without a second purchase or another subscription charge.
- Reject the initial approval. Confirm that the seller does not deliver the report and the application does not start another purchase automatically.
- Allow the next billing period to arrive. Confirm the renewal transaction and access with the same subscription ID.
- Test an insufficient balance at renewal. Confirm the failed access, past-due status, and recovery after funding within the retry window.
- Try another buyer’s subscription ID or a plan with different terms. Confirm rejection without delivering the report or creating a replacement subscription.
- Cancel, revoke, and expire separate test subscriptions. Confirm that later requests cannot access the resource through those subscriptions.
- Interrupt delivery after the initial payment. Confirm that recovery finds the existing subscription instead of creating a duplicate.
Keep these running-system checks alongside your project’s unit tests, integration tests, and coverage requirements. A simulated challenge alone does not prove recurring billing, subscription access, or cancellation works end to end.
Continue your integration
Status and recovery
Recover an interrupted purchase without paying twice.
Deliver paid results
Keep payment confirmation, application access, and successful delivery distinct.