InFlow Payments Integration
InFlow is a stablecoin payment platform. Sellers (merchants, agents) request payments from consumers; consumers approve via mobile/email/SMS; settlements happen on-chain. Consumers can pre-configure policies that auto-approve requests within budget — enabling 0-click flows for autonomous agents.
This skill integrates InFlow into any project — backend service, full-stack web app, CLI, autonomous agent, or worker — without forcing a specific framework or language. Important: the integration itself is always backend work. Browser code is only ever the optional inflow.js popup (UX only); every REST call to InFlow and the INFLOW_API_KEY live exclusively on the server. See the Hard Backend Gate section for the rule and how to handle projects without a backend yet.
What InFlow Supports
| Capability | Endpoint(s) | Use case |
|---|---|---|
| User search | POST /v1/users/search | Find a consumer by email, mobile, or username |
| User registration | POST /v1/requests/register | Onboard a new consumer (out-of-band approval) |
| Login | POST /v1/requests/login | Authenticate a consumer (out-of-band approval) |
| Payment | POST /v1/requests/payment | Request a stablecoin payment from a consumer |
| Policies | POST /v1/policies, POST /v1/requests/policy | Pre-approved spending rules for headless / 0-click flows |
| Webhooks | Inbound POST to your endpoint | Receive transaction/approval status changes |
| Polling fallback | GET /v1/requests/{requestId} | Check status without webhooks |
| Wallet read | GET /v1/balances, GET /v1/transactions | Read balances and transaction history |
| Crypto wallets | GET/POST /v1/deposit-addresses, /v1/withdrawal-addresses | Manage on-chain wallet addresses |
| Agentic users | POST /v1/users/agentic | Create a programmatic account, get an API key |
Currencies: USDC, USDT, EURC, PYUSD (stablecoins only — no fiat)
Blockchains: APTOS, BASE, SOLANA, WORLD
What This Skill Does NOT Do
These features are not exposed by the InFlow public API. If the user asks for any of them, stop and tell them they're not supported — do not attempt workarounds:
- Marketplace splits — No
splits[]field onPaymentRequest. Multi-recipient payments would need separate sequential requests with no atomic guarantee. - Refund creation — Transactions can have status
REFUNDED, but there's no API to initiate one. Refunds are dashboard-only or out-of-band. - Webhook URL registration via API — Webhook URLs are configured via the InFlow dashboard. There's no
POST /v1/webhooksto register them programmatically. - OAuth flow for sellers — Despite some legacy documentation referencing "OAuth", consumer authentication is entirely SDK-mediated. There's no OAuth dance for the integrating seller.
Defaults & Decision Tree
When the user doesn't specify, use these defaults:
| Question | Default | Why |
|---|---|---|
| Currency | USDC | Most widely-held stablecoin |
userDetails field | ["EMAIL"] | Required field; email is the minimum useful identifier |
| Display mode | See decision below | Depends on whether a browser is involved |
| Production vs sandbox | Sandbox SDK URL until user confirms | Production SDK URL is not yet documented |
Display mode: FULL if the consumer is interacting with a browser at the moment of payment (and the page can load inflow.js). HEADLESS for everything else: server-to-server, autonomous agents, scheduled jobs, AI workers.
Webhook vs polling: Webhooks for production. Polling only for local development, scripts, or anything without a public HTTPS endpoint.
User identification: A payment must be addressed to a known userId. The agent must either:
- Get the
userIdfrom the user (if they already know it), OR - Search by email/mobile/username via
POST /v1/users/search, OR - Register a new consumer if the search returns 404
Always do user identification BEFORE creating a payment request.
🛑 Hard Backend Gate (Read Before ANY Code)
InFlow integration is always backend work. The INFLOW_API_KEY is a merchant key that must never reach a browser. There is no exception, demo mode, or "just for now" carve-out.
Mandatory gate
Before writing any InFlow HTTP call, you MUST be able to name the single server-only surface where X-API-Key will be set. Acceptable surfaces:
- An Express / Fastify / Koa / Hono / Hapi route handler
- A Next.js Route Handler (
app/api/.../route.ts) or Server Action — NOT a Client Component - A SvelteKit
+server.ts/+page.server.ts— NOT a+page.sveltecomponent - A Nuxt
server/api/*.ts/server/routes/*.ts— NOT a Vue component or composable - A Remix
loader/action— NOT a route component body - A serverless function: Vercel Function, Netlify Function, Cloudflare Worker, AWS Lambda, etc.
- A Python / Go / Rust / .NET / Java HTTP service
- A standalone backend script or worker (no browser involved)
If the project does NOT have any such surface — for example, a pure Vite + React SPA, a static site, a Storybook setup, a Chrome extension popup — STOP. Tell the user they need a backend surface for the API key, even if it's a single-file serverless function or a 30-line Express server. Offer to scaffold one. Do not proceed until the user agrees and the backend surface exists.
Framework-specific footguns (key-leak mechanics)
These are real, common ways merchant keys end up in browsers. Refuse them all.
Next.js:
process.env.INFLOW_API_KEYreferenced inside a'use client'component isundefinedat runtime — Next.js does NOT bundle non-NEXT_PUBLIC_env vars into client code. The bug surfaces as a broken request with no auth header.- The disaster happens when an agent "fixes" this
undefinedby renaming the var toNEXT_PUBLIC_INFLOW_API_KEY. That prefix tells the bundler to inline the value into the client bundle → the key is now shipped to every visitor. - NEVER prefix
INFLOW_API_KEYwithNEXT_PUBLIC_. The correct fix is to move the call to a Server Component, Route Handler (app/api/.../route.ts), or Server Action — never to make the key public.
Vite (React, Vue, Svelte, Solid, etc.):
import.meta.env.INFLOW_API_KEYisundefinedunless prefixed withVITE_. Some agents "fix" this by renaming toVITE_INFLOW_API_KEY→ Vite then inlines the key into the client bundle and ships it to every visitor.- NEVER prefix
INFLOW_API_KEYwithVITE_. Vite has no server runtime by itself. If you need a backend, the user has to add one (Express, Fastify, or pair with a serverless function).
SvelteKit:
- Components and
+page.sveltefiles run in the browser. Use+server.ts,+page.server.ts, or$lib/server/*(theserverdirectory is enforced as server-only by the bundler).
Nuxt:
- Vue components and composables run in the browser. Use
server/api/*route handlers orserver/utils/*. Never readINFLOW_API_KEYfrom a<script setup>block in a.vuefile.
Astro / Remix / Qwik / Solid Start:
- All have a server/client split. The rule is the same:
INFLOW_API_KEYis read only inside the framework's server boundary (loader, action, server route, etc.), never in component code.
The "just a demo" trap
If the user says "this is just a demo", "just for testing", "we'll add the backend later", "just for now", or anything similar — the rule still applies. Embedded merchant keys leak via:
- Git history (especially after a
git push --force"fix") - Browser devtools (anyone can open them)
- Browser extensions (they read your scripts)
- Error reports / crash dumps shared in tickets
- Screenshots posted in Slack / GitHub / Stack Overflow
- Build artifacts uploaded to CDNs
- Deployed preview URLs that get crawled
A "temporary" key in the browser is a permanent key on the internet. There is no safe short-term embed.
For a quick demo, the right move is still a backend — just a small one. A 20-line Express script, a single Vercel Function, or a Cloudflare Worker. All take less time than the apology email after a key leak.
Backend-Only Fast Path (No Browser, No Frontend)
If the user is integrating InFlow into a pure backend, API, agent, worker, or service — skip the browser/SDK material entirely and follow this short path. This is the most common backend integration shape.
Order of operations:
- Set env vars (Step 1) —
INFLOW_API_KEY, optionallyINFLOW_WEBHOOK_SECRET - Identify the consumer (Step 2) —
POST /v1/users/searchwith email/mobile/username → getuserId - Create the payment with
display: HEADLESS(Step 3) — passpolicyIdif you have one for 0-click; otherwise the consumer gets a mobile/email approval prompt - Persist your correlation key — store
{ yourOrderId, requestId, transactionId? }so the webhook handler can look up the right order. The webhook payload only carries InFlow's IDs; you must map them back to your business object. - Wait for confirmation — either:
- Webhook handler (Step 5) — recommended for production. Verify HMAC, idempotency by
eventId, fulfill onevent.data.status === "PAID"(with the discriminator check) - OR polling (Step 6) — for scripts, batch jobs, local dev. Call
GET /v1/requests/{requestId}until status leavesPENDING
- Webhook handler (Step 5) — recommended for production. Verify HMAC, idempotency by
Skip: Step 4 (frontend SDK), the full-stack happy path diagram below, anything mentioning inflow.js or browsers.
For autonomous agents that need 0-click headless payments without consumer interaction per transaction, also implement Step 7 (Policies) and attach policyId to every payment request.
Order Correlation (Backend Pattern)
A common bug in payment integrations: the webhook arrives but the handler can't figure out which of your orders it belongs to. The InFlow webhook payload contains InFlow's IDs (requestId, transactionId, approvalId) but none of your business IDs. You must store the mapping yourself at payment creation time.
Minimum mapping table (in your DB / store / cache):
| Your column | Source |
|---|---|
your_order_id | Your business object — the thing being purchased |
inflow_request_id | Returned from POST /v1/requests/payment |
inflow_transaction_id | Returned later in webhook payload (event.data.transactionId) — start NULL, fill on first event |
status | Your local state machine: pending / paid / failed |
created_at | Audit trail |
At payment creation: insert a row with your_order_id + inflow_request_id, status pending.
At webhook receipt: look up the row by inflow_request_id (use event.data.approvalId or correlate via requestId if data is an ApprovalResponse; use event.data.transactionId once it appears in a TransactionResponse). Update status. Fulfill the order.
Idempotency: also store processed event.eventIds separately so you don't double-fulfill on retries (InFlow may resend the same event for up to 72 hours).
Happy Path: Add Checkout to a Web App
This is the most common request. End-to-end recipe for "I want to accept InFlow payments at checkout":
┌──────────────────────────────────────────────────────────────────┐
│ Frontend (browser) │
├──────────────────────────────────────────────────────────────────┤
│ 1. Collect consumer email at checkout │
│ 2. Click "Pay with InFlow" → POST to YOUR /api/inflow/checkout │
│ 3. Receive { requestId } from your backend │
│ 4. Render popup: new InFlow.Request(requestId).render({...}) │
│ 5. statusCallback fires when consumer approves │
│ 6. Show "processing..." (DO NOT fulfill yet) │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Backend (your /api/inflow/checkout endpoint) │
├──────────────────────────────────────────────────────────────────┤
│ 1. Receive { email, amount } from frontend │
│ 2. POST /v1/users/search { email } → get userId │
│ (if 404 → POST /v1/requests/register first) │
│ 3. POST /v1/requests/payment { │
│ userId, amount, currency: "USDC", │
│ display: "FULL", userDetails: ["EMAIL"] │
│ } │
│ 4. Return { requestId } to frontend │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Backend (your /api/webhooks/inflow endpoint) │
├──────────────────────────────────────────────────────────────────┤
│ 1. Receive POST from InFlow with x-inflow-signature header │
│ 2. Verify HMAC-SHA256 signature → 401 if invalid │
│ 3. Check eventId against idempotency store → skip if seen │
│ 4. Fulfill ONLY if ALL three are true: │
│ a. event.type === "TRANSACTION_UPDATED" │
│ b. event.data.transactionId is present │
│ (i.e. data is a TransactionResponse, not Approval) │
│ c. event.data.status === "PAID" │
│ (use INNER data.status, NOT outer event.status) │
│ 5. Return HTTP 200 │
└──────────────────────────────────────────────────────────────────┘
Two
statusfields, two meanings. The webhook envelope has an outerevent.status(DELIVERED/FAILED/PENDING/DISABLED— InFlow's delivery state) and the innerevent.data.status(the actual transaction state likePAID/PENDING/INSUFFICIENT_FUNDS). Always checkevent.data.statusfor fulfillment decisions. See Step 5 for details.
The three things the user MUST do manually:
- Get an
INFLOW_API_KEY(from InFlow dashboard, or create an agentic user — see "Agentic User Setup" below) - Register the webhook URL
https://yourdomain.com/api/webhooks/inflowin the InFlow dashboard (no API for this) - Get the
INFLOW_WEBHOOK_SECRETfrom the InFlow dashboard after registering the URL
After implementing the integration, summarize these manual steps prominently.
Reference Material
This skill ships with three reference documents in references/. Read them when you need details beyond what's in this file:
references/overview.md— Platform concepts, approval model, FULL vs HEADLESS display modesreferences/flows.md— 8 worked-out integration flows with HTTP examplesreferences/api-reference.md— Every endpoint, every schema, every enum value
When the user asks something specific (e.g. "how do I list policies?", "what does the webhook payload look like?"), look it up in the references rather than guessing.
Implementation Guide
Prerequisites
Before writing any code:
- Read the project — detect language and framework via
package.json,requirements.txt,go.mod,Cargo.toml, etc. Use whatever HTTP client and web framework already exists. Do not impose Express, Next.js, React, or any specific framework. - Pass the Hard Backend Gate (see the "🛑 Hard Backend Gate" section earlier in this file) — name the server-only surface where the API key will live. If there isn't one, scaffold one (with the user's agreement) before continuing.
- Confirm the user has an API key — if not, walk them through the agentic user setup at the bottom of this file.
- Confirm the project type — pure backend / fullstack web app / CLI / agent / worker. Pure-frontend projects are NOT a valid type — see the Hard Backend Gate.
- Pure backend → Server payment + webhook (or polling)
- Fullstack / web app → Server payment + frontend SDK + webhook
- CLI / agent / worker → Headless payment + webhook (or polling) + maybe policies for 0-click
Step 1: Set Up Environment Variables
Add these to the project's existing env file (.env, .env.local, etc. — match the project's convention). Add to .env.example too.
# Required for any InFlow integration
INFLOW_API_KEY=<your-private-key>
# Required if you handle webhooks
INFLOW_WEBHOOK_SECRET=<your-webhook-secret>
# Optional: defaults to https://api.inflowpay.ai
# INFLOW_API_BASE_URL=https://api.inflowpay.ai
Make sure .env and .env.local are in .gitignore.
Critical: INFLOW_API_KEY is server-side only. Never embed it in frontend code, never expose it via a public route, never log it.
Step 2: Find or Create the Consumer
A payment must be addressed to a userId. If the user doesn't know the consumer's userId, you must look it up first.
Search by email (or mobile / username):
POST https://api.inflowpay.ai/v1/users/search
X-API-Key: <INFLOW_API_KEY>
Content-Type: application/json
{ "email": "alice@example.com" }
On match (200):
{ "userId": "8a5c2948-9e08-439e-a7a6-9f0ee335f566" }
On no match (404): initiate registration:
POST https://api.inflowpay.ai/v1/requests/register
X-API-Key: <INFLOW_API_KEY>
Content-Type: application/json
{
"display": "FULL",
"userDetails": ["EMAIL", "NAME"]
}
⚠️ API quirk:
RegisterRequestlistsuserIdas optional (onlydisplayanduserDetailsare required per the OpenAPI spec), but its semantics for a true new-user registration flow are not documented. Do not invent a UUID. Try the call withoutuserIdfirst; if the API rejects it, the user must obtain a provisionaluserIdfrom InFlow support or check vendor docs. Treat this as vendor-ambiguous and surface the ambiguity to the user — do not silently work around it.
This returns an ApprovalResponse with a requestId. Hand the requestId to the frontend SDK to render the registration popup. After approval, the consumer is registered and can be searched again.
Search field rules:
email— RFC email formatmobile— E.164 format like+15551234567username— 3-16 chars, alphanumeric +-_
userDetails enum values: BIRTHDATE, DEPOSIT_ADDRESSES, EMAIL, MOBILE, NAME, NATIONAL_ID, PHYSICAL_ADDRESS, USERNAME. Always include at least EMAIL.
Step 3: Create a Payment Request
POST https://api.inflowpay.ai/v1/requests/payment
X-API-Key: <INFLOW_API_KEY>
Content-Type: application/json
{
"userId": "<consumer-uuid-from-Step-2>",
"amount": 99.99,
"currency": "USDC",
"display": "FULL",
"userDetails": ["EMAIL"]
}
Response (200):
{
"requestId": "<uuid>",
"type": "PAYMENT",
"status": "PENDING"
}
Required fields: amount (≥ 0.01), currency, display, userDetails.
Optional: userId, policyId (for auto-approval via a pre-existing policy).
Amount format: Plain decimal in major units of the chosen stablecoin. 99.99 means 99.99 USDC (≈ $99.99). No cents/minor units.
Adapt the HTTP call to the user's stack — read the project and use whatever HTTP client is already in use. The exact code shape depends on the language (Node fetch, Python requests, Go net/http, etc.).
Error handling:
- 4xx → user error (missing field, invalid format, user not found). Return the error to the caller, don't retry.
- 5xx / network → transient. Retry with exponential backoff (max 3 attempts) before failing the checkout.
After getting requestId, the next step depends on the display mode:
FULL→ handrequestIdto the frontend SDK (Step 4)HEADLESS→ wait for the webhook (Step 5) or pollGET /v1/requests/{requestId}(Step 6)
Step 4: Frontend SDK (inflow.js) — for display: FULL
For browser-based flows. The frontend loads inflow.js, gets a requestId from the backend, and renders a popup.
Load the SDK:
<script src="https://sandbox.inflowpay.ai/sdk/inflow.js"></script>
⚠️ This is the sandbox URL. The production URL is not yet publicly documented. Before deploying to production, the user should confirm the production SDK URL with InFlow.
Render a request:
// Get the requestId from your backend (which calls POST /v1/requests/{login|register|payment|...})
const requestId = await fetchRequestIdFromYourBackend();
const request = new InFlow.Request(requestId);
request.render({
statusCallback: (status) => {
// status.status is one of: PENDING, APPROVED, DECLINED, EXPIRED, CANCELLED
if (status.status === InFlow.STATUS.APPROVED) {
// SDK indicated user approved. Show "processing..."
// DO NOT fulfill the order here — wait for the webhook.
}
}
});
Same pattern works for login, register, payment, and details requests. Only the backend endpoint that produces the requestId differs.
Adapt the surrounding markup to the user's framework — wrap in a React/Vue/Svelte component if needed. The SDK itself is framework-agnostic.
Step 5: Webhook Handler
InFlow delivers signed events to your registered webhook URL.
🚨 CRITICAL MANUAL STEP: The webhook URL must be registered in the InFlow dashboard by the user — there is no API for this. After implementing the handler, tell the user explicitly:
"Go to the InFlow dashboard, navigate to webhooks, register
https://yourdomain.com/api/webhooks/inflow(or your equivalent URL), and copy the webhook secret into yourINFLOW_WEBHOOK_SECRETenv var. Until you do this, no events will be delivered."
For local testing, suggest ngrok or similar to expose localhost over HTTPS.
Webhook payload shape — note the two status fields:
{
"eventId": "<uuid>",
"webhookId": "<uuid>",
"type": "TRANSACTION_UPDATED",
"status": "DELIVERED", ← OUTER: InFlow's delivery state. Ignore for fulfillment.
"data": {
"transactionId": "<uuid>",
"approvalId": "<uuid>",
"amount": 99.99,
"currency": "USDC",
"blockchain": "SOLANA",
"status": "PAID", ← INNER: actual transaction state. Use this for fulfillment.
"transactionHash": "0x...",
"created": "2026-01-01T00:00:00.000Z"
},
"created": "2026-01-01T00:00:00.000Z",
"delivered": "2026-01-01T00:00:01.000Z"
}
Critical disambiguation: the envelope has TWO
statusfields:
event.status(outer) — InFlow's webhook delivery state:PENDING/DELIVERED/FAILED/DISABLED. This tells you whether InFlow successfully delivered the event. Don't branch on this for business logic.event.data.status(inner) — the actual transaction or approval state:PAID,INITIATED,PENDING,INSUFFICIENT_FUNDS,APPROVED,DECLINED, etc. This is what you check for fulfillment.
Event types: TRANSACTION_CREATED, TRANSACTION_UPDATED
The data field is a discriminated union:
- If it has
requestIdandtype(LOGIN/PAYMENT/etc.) → it's anApprovalResponse - If it has
transactionId,blockchain, andtransactionHash→ it's aTransactionResponse
Only fulfill orders when ALL of these are true: event.type === "TRANSACTION_UPDATED", the data field is shaped as a TransactionResponse (presence of transactionId is the simplest discriminator), and event.data.status === "PAID". Do not generalize to "any TRANSACTION_* event with status PAID" — an ApprovalResponse payload may also have a status field (APPROVED/PENDING/etc.) and using it for fulfillment will crash or misfire.
data.status values that matter (TransactionResponse):
PAID— payment completed (this is your "fulfill the order" signal)INITIATED,PENDING— in progress, do nothingINSUFFICIENT_FUNDS,GENERAL_ERROR— failed, notify userREFUNDED,RETURNED— money was returned
Mandatory: HMAC signature verification. Every webhook is signed with HMAC-SHA256. The signature is in the x-inflow-signature header. Verify it before processing.
// Generic — adapt to whatever crypto library the project uses.
// Node.js example using built-in crypto module:
import crypto from 'node:crypto';
function verifyInflowWebhook(rawBody, signatureHeader, secret) {
// Guard against missing or malformed header — timingSafeEqual throws on length mismatch
if (typeof signatureHeader !== 'string' || signatureHeader.length === 0) return false;
if (!rawBody || !secret) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
if (expected.length !== signatureHeader.length) return false;
// Constant-time compare to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader),
);
}
The same logic applies in any language: HMAC-SHA256 the raw request body using the webhook secret, hex-encode it, compare to the x-inflow-signature header.
Critical: verify against the raw request body bytes, NOT a re-serialized JSON object. If the framework parses JSON automatically, you must capture the raw body before parsing. The exact mechanism depends on the framework.
Webhook handler responsibilities:
- Verify the signature → reject with HTTP 401 if invalid
- Parse the JSON body
- Check
eventIdagainst an idempotency store → skip if already processed (InFlow may retry the same event for up to 72 hours) - Look up your local order by
event.data.approvalIdorevent.data.transactionId(see Order Correlation section above) - Branch on the FULL three-condition rule (do not skip any of these):
- Fulfill when ALL of:
event.type === "TRANSACTION_UPDATED"ANDevent.data.transactionIdis present (it's aTransactionResponse, not anApprovalResponse) ANDevent.data.status === "PAID" - Mark failed when
event.data.transactionIdpresent ANDevent.data.statusisINSUFFICIENT_FUNDS/GENERAL_ERROR/RETURNED - Log only for other event types or non-terminal statuses (
INITIATED,PENDING, etc.) - NEVER fulfill on
event.status(the OUTER delivery status) — that just means InFlow delivered the webhook successfully
- Fulfill when ALL of:
- Return HTTP 200 to acknowledge
If the handler doesn't return 200, InFlow keeps retrying with exponential backoff for up to 72 hours.
Step 6: Polling (Webhook Alternative)
If a webhook listener isn't possible (local dev, batch jobs, simple scripts), poll request status:
GET https://api.inflowpay.ai/v1/requests/{requestId}
X-API-Key: <INFLOW_API_KEY>
Returns the current ApprovalResponse. Poll every few seconds until status is no longer PENDING.
For full transaction details after approval:
GET https://api.inflowpay.ai/v1/transactions/{transactionId}
Where transactionId comes from the approved ApprovalResponse.transactionId.
Step 7: Policies (Enable 0-Click Headless Payments)
Policies are pre-approved spending rules. When a HEADLESS payment request includes a policyId and the request is within the policy's limits, it auto-approves immediately — no consumer interaction needed.
Use policies for:
- Autonomous AI agents that need to make purchases without per-transaction approval
- Recurring payments (subscriptions, automated top-ups)
- High-frequency low-value transactions
Direct creation (seller-initiated):
POST https://api.inflowpay.ai/v1/policies
X-API-Key: <INFLOW_API_KEY>
Content-Type: application/json
{
"type": "PAYMENT",
"action": "APPROVE",
"currency": "USDC",
"budget": 1000,
"threshold": 100,
"period": "MONTHLY",
"expires": "2026-12-31T00:00:00.000Z"
}
Consumer-initiated request (the consumer must approve the policy via their device):
POST https://api.inflowpay.ai/v1/requests/policy
X-API-Key: <INFLOW_API_KEY>
Content-Type: application/json
{
"userId": "<consumer-uuid>",
"display": "FULL",
"userDetails": ["EMAIL"]
}
This returns an ApprovalResponse — same flow as login/payment. Consumer approves via SDK or mobile, and the policy becomes active.
Policy fields:
action—APPROVE,DECLINE,REVIEWperiod—DAILY,MONTHLY,YEARLY,ONCEbudget— total allowance for the periodthreshold— per-transaction maximumspent— current usage (read-only onPolicyResponse)
CRUD:
GET /v1/policies
GET /v1/policies/{policyId}
POST /v1/policies
PUT /v1/policies/{policyId}
DELETE /v1/policies/{policyId}/delete
Attach a policy to a payment:
{
"userId": "<consumer-uuid>",
"amount": 50,
"currency": "USDC",
"display": "HEADLESS",
"userDetails": ["EMAIL"],
"policyId": "<policy-uuid>"
}
If the request is within budget and threshold, the response will already show status: APPROVED.
Step 8: Wallet Read Operations
Read-only access to the seller's wallet state. Useful for dashboards, reconciliation, and balance checks.
GET /v1/balances — all currency balances
GET /v1/balances/{currency} — single currency balance
GET /v1/transactions — paginated transaction history
GET /v1/transactions/{id} — single transaction
GET /v1/users/self — current authenticated user
All return JSON. See references/api-reference.md for full schemas.
Verification
Run a smoke test to confirm everything works. Adapt the commands to whatever the project uses (npm scripts, curl, http file, etc.):
- API key auth — call
GET /v1/users/selfwith the configuredX-API-Key. Should return the authenticated user. If 401, the key is wrong. - User search — call
POST /v1/users/searchwith a known email. Confirm 200 +userId, or 404 if the user doesn't exist. - Payment request — initiate a small test payment to the known
userId. Confirm the response contains arequestIdandstatus: PENDING. - Polling (if implemented) — call
GET /v1/requests/{requestId}and confirm the status updates as the consumer interacts. - Webhook (if implemented) — once a real event is delivered, check the handler's logs to confirm: signature verified, payload parsed, handler logic executed, HTTP 200 returned. (You can also use
GET /v1/eventsto inspect past events andPOST /v1/events/{eventId}/resendto replay one.)
If the user is in production, also recommend:
- A persistent idempotency store for
eventIds (DB or Redis) - Logging all webhook events for audit
- A dead-letter queue for failed webhook processing
Agentic User Setup (No Existing Account)
If the user does NOT have an existing InFlow account, create an agentic (programmatic) user. This is a one-time setup and does NOT require existing authentication.
POST https://api.inflowpay.ai/v1/users/agentic
Content-Type: application/json
{
"locale": "EN_US",
"timezone": "US/Pacific"
}
Returns:
{
"userId": "<uuid>",
"privateKey": "<your-api-key>",
"locale": "EN_US",
"timezone": "US/Pacific",
"created": "...",
"updated": "..."
}
The privateKey is what goes in the INFLOW_API_KEY env var. Store it securely — it cannot be retrieved again.
Idempotency
Before each step, check if the work is already done. Skip steps that are complete:
- If
INFLOW_API_KEYis already in.env/.env.example, skip env setup - If a webhook handler with HMAC verification already exists for InFlow paths, skip the webhook scenario
- If
inflow.jsis already loaded somewhere in the frontend, skip the SDK script tag - If there's an existing helper module that wraps
api.inflowpay.aicalls (look forINFLOW_API_KEYusage orinflowpay.aiin source), reuse it instead of creating a new one - If
.env/.env.localis not in.gitignore, add it
Always read the project before writing — don't duplicate existing logic.
Final Summary (After Implementation)
After implementing, give the user a clear summary in this format:
- Files created or modified — list each one with its purpose
- Env vars to set — name + what each is for + where to get the values
- What flows are now active — payment / login / register / webhook / polling / policies
- Manual steps the user must complete — be explicit about:
- Getting the API key (from dashboard or via
POST /v1/users/agentic) - Registering the webhook URL in the InFlow dashboard (no API for this — flag prominently if a webhook handler was created)
- Copying the webhook secret into
INFLOW_WEBHOOK_SECRET - Confirming the production SDK URL with InFlow before going live (sandbox URL is in code by default)
- Getting the API key (from dashboard or via
- Test commands — exact commands to verify each flow
- Next steps — what to do after the integration is wired up
Keep the summary terse. The user should be able to complete the integration after reading it.