Payment Links
A payment link is a shareable URL that collects a fixed amount. Each link can be paid in two ways:
- An agent, backend or script pays it in code over x402.
- A person opens it in a browser and pays on a hosted checkout page.
All management endpoints require Authorization: Bearer <agent-jwt>. Funds go to the wallet of the agent that created the link. If the agent isn't linked to a FluxA Wallet yet, these endpoints return 404 Agent not found in wallet or 403 Agent not authorized.
FluxA does not send webhooks for payment links. To confirm a payment, poll Get payments for a link.
Create payment link
POST/api/payment-linksBearer agent-jwtcurl -X POST https://walletapi.fluxapay.xyz/api/payment-links \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $JWT_TOKEN" \
-d '{
"amount": "1000000",
"currency": "USDC",
"network": "base",
"description": "Order #1042",
"resourceContent": "{\"downloadUrl\": \"https://example.com/d/abc\"}",
"expiresAt": "2026-12-31T23:59:59Z",
"maxUses": 1,
"externalId": "order_1042",
"idempotencyKey": "create-order_1042"
}'Request fields
| Field | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Price in atomic units of currency (USDC 1000000 = 1.0; credits 100 = 1.0; XRP in drops). |
currency | string | No | USDC (default), FLUXA_MONETIZE_CREDITS, or XRP. XRP is available only where it's enabled; otherwise you get ripple_not_enabled. |
network | string | No | USDC: base (default) or base-sepolia (also eip155:8453 / eip155:84532). For credits and XRP the network is fixed. |
description | string | No | Label shown to the payer. |
resourceContent | string | No | Text or JSON returned to the payer after a successful x402 payment. |
expiresAt | string | No | ISO 8601 time, must be in the future. |
maxUses | integer | No | Maximum number of successful payments. Leave it out for unlimited. Use 1 for a one-off order. |
paymentMode | string | No | dual (browser checkout + x402), x402 (agents only), or hosted (browser only). The default is dual if the currency supports browser checkout (USDC, credits), and x402 otherwise (XRP). |
allowedCheckoutMethods | string[] | No | Limits browser checkout methods: fluxa (pay with a FluxA wallet) and/or external (connect your own crypto wallet; USDC only). |
returnUrl | string | No | Where to send the payer after a browser payment. The origin must be on the allow-list. |
externalId | string | No | Your reference, such as an order ID (max 200 chars). Creating again with the same externalId returns the existing link. |
idempotencyKey | string | No | Retry key (max 200 chars). Creating again with the same key returns the existing link. |
metadata | object | No | Your own JSON metadata. |
Response
201 Created for a new link. 200 OK with "idempotent": true if a link with the same idempotencyKey or externalId already exists.
{
"success": true,
"paymentLink": {
"linkId": "pl_abc123xyz456",
"amount": "1000000",
"currency": "USDC",
"network": "base",
"payTo": "0xYourAgentWalletAddress",
"assetAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"scheme": "exact",
"description": "Order #1042",
"resourceContent": "{\"downloadUrl\": \"https://example.com/d/abc\"}",
"status": "active",
"expiresAt": "2026-12-31T23:59:59.000Z",
"maxUses": 1,
"useCount": 0,
"paymentMode": "dual",
"externalId": "order_1042",
"metadata": {},
"returnUrl": null,
"url": "https://walletapi.fluxapay.xyz/paymentlink/pl_abc123xyz456",
"checkoutUrl": "https://agentwallet.fluxapay.xyz/pay/paymentlink/pl_abc123xyz456",
"createdAt": "2026-10-25T12:00:00.000Z",
"updatedAt": "2026-10-25T12:00:00.000Z"
}
}| Field | Description |
|---|---|
linkId | Link ID (pl_…). |
url | Public URL. Agents pay it over x402. A browser that opens it is redirected to checkoutUrl. |
checkoutUrl | Hosted checkout page for people. |
status | active or disabled. It stays active after a payment, so don't use it to detect payment. |
useCount | Number of completed payments. |
payTo / assetAddress / network / scheme | What payers pay to. For credits links, payTo is monetize.fluxapay.xyz. |
The response also includes other internal fields. They aren't part of the public API, so don't depend on them.
Create errors ({ "error", "code"? }, status 400): amount is required, invalid amount, unsupported_currency, unsupported_network, ripple_not_enabled, missing_pay_to, invalid expiresAt / maxUses, invalid_payment_mode, invalid_checkout_methods, no_hosted_checkout_method, invalid_return_url / return_url_not_allowed, invalid_metadata.
List payment links
GET/api/payment-linksBearer agent-jwtcurl "https://walletapi.fluxapay.xyz/api/payment-links?limit=50" \
-H "Authorization: Bearer $JWT_TOKEN"| Param | Type | Default | Description |
|---|---|---|---|
limit | number | 100 | Maximum number of results (up to 500). |
Response (200): { "paymentLinks": [ /* same shape as paymentLink above */ ] }. The list contains only links created by this agent.
Get payment link
GET/api/payment-links/:linkIdBearer agent-jwtcurl https://walletapi.fluxapay.xyz/api/payment-links/pl_abc123xyz456 \
-H "Authorization: Bearer $JWT_TOKEN"Response (200): { "paymentLink": { ... } }. You get 404 if the link doesn't exist and 403 if another agent created it.
Update payment link
PATCH/api/payment-links/:linkIdBearer agent-jwtcurl -X PATCH https://walletapi.fluxapay.xyz/api/payment-links/pl_abc123xyz456 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $JWT_TOKEN" \
-d '{ "status": "disabled" }'| Field | Type | Description |
|---|---|---|
description | string | New description. |
resourceContent | string | New content returned after payment. |
status | string | active or disabled. |
expiresAt | string | null | ISO 8601 time, or null to remove the expiry. |
maxUses | number | null | Positive integer, or null for unlimited. |
Response (200): { "success": true, "paymentLink": { ... } }.
Delete payment link
DELETE/api/payment-links/:linkIdBearer agent-jwtcurl -X DELETE https://walletapi.fluxapay.xyz/api/payment-links/pl_abc123xyz456 \
-H "Authorization: Bearer $JWT_TOKEN"Response (200): { "success": true, "message": "Payment link deleted" }. After deletion the link can't be paid.
Get payments for a link
GET/api/payment-links/:linkId/paymentsBearer agent-jwtLists the payments made through one link. Use this to confirm a payment before you deliver anything.
curl https://walletapi.fluxapay.xyz/api/payment-links/pl_abc123xyz456/payments \
-H "Authorization: Bearer $JWT_TOKEN"| Param | Type | Default | Description |
|---|---|---|---|
limit | number | 100 | Maximum number of results (up to 500). |
Response (200):
{
"payments": [
{
"id": 551373,
"payerAddress": "0xPayerWalletAddress",
"amount": "1000000",
"currency": "USDC",
"settlementStatus": "settled",
"settlementTxHash": "0xabc123...",
"acceptedAt": "2026-10-25T13:00:00.000Z",
"createdAt": "2026-10-25T13:00:00.000Z"
}
]
}| Field | Description |
|---|---|
id | Payment ID. Pass it as paymentId when you refund. |
settlementStatus | settled means the funds arrived (completed). failed means the payment didn't go through. Treat any other value as processing: don't deliver yet, and poll again. |
settlementTxHash | On-chain transaction hash once settled. |
Deliver only when a payment for the expected amount has settlementStatus: "settled". Payment records have no status field.
Reconcile payments across links
GET/api/payment-links/merchant/paymentsBearer agent-jwtReturns completed payments across all of this agent's links, oldest first. It's built for incremental sync: store the last paymentId you processed and pass it as afterPaymentId on the next call.
curl "https://walletapi.fluxapay.xyz/api/payment-links/merchant/payments?afterPaymentId=551373&limit=100" \
-H "Authorization: Bearer $JWT_TOKEN"| Param | Description |
|---|---|
afterPaymentId | Return only payments with a larger paymentId. |
linkId | Only payments for this link. |
externalId | Only payments for links created with this externalId. |
status | settled (default; completed payments only), failed, or all. |
limit | Default 100, maximum 500. |
Response (200):
{
"payments": [
{
"paymentId": 551374,
"linkId": "pl_abc123xyz456",
"externalId": "order_1042",
"payerAddress": "0xPayerWalletAddress",
"amount": "1000000",
"currency": "USDC",
"network": "base",
"payTo": "0xYourAgentWalletAddress",
"settlementStatus": "settled",
"settlementTxHash": "0xabc123...",
"createdAt": "2026-10-25T13:00:00.000Z"
}
]
}settlementStatus follows the same rules as above. The response also includes other internal fields. They aren't part of the public API, so don't depend on them.
Hosted checkout (browser)
When a person opens a link that allows hosted checkout (paymentMode dual or hosted) in a browser, FluxA shows a checkout page. They can pay with their FluxA wallet, or with their own crypto wallet (USDC only).
- Share the link. Send the payer
urlorcheckoutUrl. A browser request tourl(Accept: text/htmlwith no payment header) is redirected (302) tocheckoutUrl. - Payer pays. The payer confirms on the hosted page. If you set
returnUrl, they're sent back to your site afterwards. - Verify on your server.
returnUrlisn't proof of payment. Confirm with Get payments for a link and requiresettlementStatus: "settled".
useCount goes up once per completed payment, whichever way the payer paid.
Public payment endpoint (x402)
GET/paymentlink/:linkIdThis is a public endpoint and needs no authentication. Any client can pay the link with x402 against this URL.
- The link must allow x402 (
paymentModex402ordual). - Send
Accept: application/json, or leave theAcceptheader out. WithAccept: text/htmland no payment header, the request counts as a browser and gets redirected.
Step 1: Get the payment requirements
curl -i https://walletapi.fluxapay.xyz/paymentlink/pl_abc123xyz456Response 402 Payment Required. The same requirements are also sent Base64-encoded in the X-Payment-Required and Payment-Required headers.
{
"error": "Payment Required",
"x402Version": 1,
"accepts": [
{
"scheme": "exact",
"network": "eip155:8453",
"maxAmountRequired": "1000000",
"resource": "/paymentlink/pl_abc123xyz456",
"description": "Order #1042",
"mimeType": "application/json",
"outputSchema": {},
"payTo": "0xRecipientAddress",
"maxTimeoutSeconds": 60,
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"extra": { "name": "USD Coin", "version": "2" }
}
]
}What changes by currency:
| Currency | network | asset | payTo | extra |
|---|---|---|---|---|
| USDC (Base) | eip155:8453 (eip155:84532 on Sepolia) | USDC contract | Link owner's wallet | { "name": "USD Coin", "version": "2" } |
| Credits | fluxa-monetize-credits | fluxa-monetize-credit | monetize.fluxapay.xyz | { "name": "FluxA Monetize Credits", "version": "1" } |
| XRP | xrpl-mainnet | XRP | Owner's XRP address | { "name": "XRP", "version": "1" } |
If the link only allows hosted checkout, you get 402 { "error": "Hosted checkout required", "checkoutUrl": "..." } instead.
Step 2: Submit payment
If you have a FluxA agent JWT, pass the Step 1 fields to POST /api/payment/x402V3Payment (with a mandate) or x402V1Payment. Either returns xPaymentB64. Any standard x402 client can build the same payload, for example an EIP-3009 TransferWithAuthorization for USDC.
Retry the same GET with the payment in the X-Payment header (Payment-Signature is also accepted):
curl -i https://walletapi.fluxapay.xyz/paymentlink/pl_abc123xyz456 \
-H "Accept: application/json" \
-H "X-Payment: $X_PAYMENT_B64"Response 200 on success. The receipt is also sent Base64-encoded in the X-Payment-Response header (as {"success":true,"transaction":"0x..."}), mirrored as Payment-Response.
{
"status": "success",
"resource": "{\"downloadUrl\": \"https://example.com/d/abc\"}",
"receipt": {
"txHash": "0xabc123def456...",
"payer": "0xPayerAddress",
"amount": "1000000",
"currency": "USDC"
}
}Step 3: Handle the outcome
| Status | Body | What to do |
|---|---|---|
200 | status: "success", resource, receipt | Done. |
202 | { "status": "processing", "paymentId": 551373 } | The payment was accepted and is still being confirmed. Don't pay again. Poll Get payments for a link (if you own the link), or repeat the same request with the same X-Payment later. |
402 | { "error": "Payment failed", "reason": "..." } | The payment was rejected or didn't settle. Sign a new payment (new nonce) and retry. Use reason for logs only. |
404 | { "error": "Payment link not found" | "...not active" | "...has expired" | "...has reached maximum uses" } | The link can't be paid. |
409 | code: "payment_attempt_nonce_conflict" | The nonce was already used for a different payment. Sign again with a new nonce. |
409 | code: "hosted_checkout_required" | This link is browser-only. Use checkoutUrl. |
409 | code: "payment_link_changed", or error with a reason | The link changed or can't be paid right now. Fetch the requirements again (Step 1). |
Idempotency. Replaying the request with the same X-Payment returns the same payment attempt. A completed payment replays 200 with the original receipt. You aren't charged twice and useCount doesn't go up again. If you lost a response, it's safe to retry with the same payload.
