Documentation
PayFlux in one page
PayFlux lets you accept payments in assets that live on different chains, through one API. You price in dollars and name the assets you'll take. PayFlux works out the valid paths, proves the payment happened, and settles you in the asset you asked for.
import { PayFlux } from "payflux-sdk"
const payflux = new PayFlux({ apiKey: process.env.PAYFLUX_SECRET_KEY })
const payment = await payflux.payments.create({
amount: "50.00",
currency: "USD",
acceptedAssets: ["XRP", "FXRP", "C2FLR"],
settlementAsset: "FXRP",
})
redirect(`/checkout/${payment.id}`)That is the whole integration. No XRPL SDK, no attestation client, no lot arithmetic, no transaction watcher — and no wallet connection: as a merchant you never sign anything.
Testnet only
Get started
Quickstart
- 1
Sign in
Go to /sign-in and continue with Google. The first sign-in creates your account — there is no separate sign-up and no password. - 2
Set your settlement addresses
In Settings, add an XRPL Testnet address (where customers send XRP) and a Coston2 address (where your FXRP lands). Both are yours alone — PayFlux never falls back to anyone else's.
Until both are set, every payment route reports itself unavailable. - 3
Create an API key
In API keys, pick the scopes you need and create a key. It is shown once. - 4
Create a payment
bashcurl -X POST https://your-api/v1/payments \ -H "X-API-Key: $PAYFLUX_SECRET_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_1001" \ -d '{ "amount": "50.00", "currency": "USD", "acceptedAssets": ["XRP", "FXRP", "C2FLR"], "settlementAsset": "FXRP", "orderId": "order_1001" }' - 5
Send the customer to checkout
Redirect to/checkout/<paymentId>. They pick an asset, pay, and watch it verify. You get a webhook when it settles.
Check your key works
npm run verify:key -- sk_ctn2_… exercises the key end to end in about ten seconds: authentication, every scope, a real payment, idempotency, and the routing engine.Security
Authentication
There are two credentials and they are deliberately not interchangeable.
| Credential | Who holds it | Can do |
|---|---|---|
X-API-Key: sk_ctn2_… | Your server | Create and read payments, settlements, webhooks |
Authorization: Bearer <id-token> | You, in a browser | Manage keys, settings, audit log |
An API key cannot create another API key. If it could, anyone holding a leaked key could issue themselves a fresh one and survive the rotation meant to lock them out. Equally, a browser session cannot create payments — those come from your server.
Key format
sk_ctn2_a1b2c3d4e5f6a7b8_XmR3nQ7...
└┬┘ └─┬┘ └──────┬───────┘ └──┬──┘
│ │ │ └── 32 random bytes
│ │ └─────────────── key id — public, safe to quote in support
│ └───────────────────────── environment (ctn2 = Coston2 testnet)
└────────────────────────────── secret key markerKeys are stored as SHA-256 digests and compared in constant time. The secret is shown exactly once, when you create it — there is no endpoint that reveals an existing key, because the server does not have it.
Rotation
Rotating issues a successor and puts the old key on a countdown (24 hours by default). Both work during the window, so you can deploy the new key and confirm traffic moved before the old one stops. For a leaked key, rotate with no grace period — it is rejected on the next request.
Security
Scopes
A key carries only the permissions it needs, so a reporting job can hold a key that cannot move money.
| Scope | Grants |
|---|---|
payments:write | Create payments, trigger verification |
payments:read | Read payments, routes and events |
settlements:read | Read settlements and quotes |
settlements:write | Execute settlements |
webhooks:read | Read webhook config and deliveries |
webhooks:write | Send test events |
Omitting scopes grants payments:read + payments:write — the minimum to accept a payment, not full access. A missing scope returns 403 naming what was required:
{
"error": {
"code": "INSUFFICIENT_SCOPE",
"message": "This API key lacks the \"payments:write\" scope. It holds: payments:read. Rotate the key with the scope added.",
"requiredScope": "payments:write"
}
}Every denial is written to your audit log — a key repeatedly hitting an endpoint it cannot use is either a misconfigured deploy or someone probing.
Core
Creating payments
const payment = await payflux.payments.create({
amount: "50.00", // decimal string, max 2 places
currency: "USD", // USD only for now
acceptedAssets: ["XRP", "FXRP", "C2FLR"],
settlementAsset: "FXRP", // what you want to end up holding
orderId: "order_1001", // your reference
metadata: { sku: "hoodie" },
idempotencyKey: "order_1001",
})Idempotency
409 rather than silently handing back the first payment. A retried request can never create a second charge.Statuses
created → awaiting_payment → payment_detected → verifying → verified → settling → settled
│
partially_paid / overpaid
│
failed / expired / refundedYou cannot set a status. There is no field for it on any endpoint. A status is always the consequence of an observed fact — a detected transaction, a finalized attestation, a confirmed settlement.
Underpayment never becomes success. A short payment lands in partially_paid with the outstanding amount recorded. Overpayment settles normally with the excess recorded.
Core
Checkout & routes
Redirect the customer to /checkout/<paymentId>, or build your own using the routes endpoint.
{
"recommended": "route_pay_abc_fxrp",
"data": [
{
"sourceAsset": "XRP",
"status": "available",
"score": 90,
"estimatedInputAmount": "10.025",
"destinationAsset": "FXRP",
"settlementMethod": "fassets-mint",
"priceImpact": "+0.82 XRP rounded up to the FAssets lot boundary",
"reasons": [
"FDC Payment attestation available for testXRP",
"Verified payments are recorded on-chain in PaymentRegistry",
"Settles to FXRP by FAssets minting (1 lot of 10 XRP)"
]
}
]
}Routes are recomputed live against FTSOv2 prices and FAssets agent capacity. A route is only available if it can execute right now — a path PayFlux supports but cannot currently run is reported as degraded or unavailable with the reason, never hidden.
Why show the reasons?
Core
Webhooks
Set your endpoint and signing secret in Settings. Every state change is delivered, signed, with retries at 5s, 30s, 2m, 10m and 1h.
import { verifyWebhookSignature } from "payflux-sdk"
app.post("/webhooks/payflux",
express.raw({ type: "application/json" }),
(req, res) => {
const result = verifyWebhookSignature(
req.header("X-PayFlux-Signature")!,
req.body.toString("utf8"), // RAW body, not a re-serialized object
process.env.PAYFLUX_WEBHOOK_SECRET!,
)
if (!result.valid) return res.status(400).send(result.reason)
const event = JSON.parse(req.body.toString("utf8"))
// event.type, event.paymentId, event.status, event.settlement …
res.json({ received: true })
})Use the raw body
Events:
payment.created payment.detected payment.verifying
payment.verified payment.settling payment.settled
payment.failed payment.expired payment.partially_paid
payment.overpaid settlement.completed settlement.failedThe signature covers the timestamp and the body together, so a captured payload cannot be replayed later.
How it works
How verification works
This is the part worth understanding, because it is what makes a PayFlux payment checkable by someone who does not trust PayFlux.
XRP — proved by Flare
XRPL transaction
│
├─ FDC attestation requested → Coston2 transaction
├─ voting round finalizes → ~2 minutes
├─ Merkle proof retrieved
└─ submitted to PaymentRegistry → the contract calls
FdcVerification.verifyPaymentThe Flare Data Connector is not a bridge. It moves no value. It produces an attestation — a signed, Merkle-committed statement that an external-chain fact is true — which Flare contracts can then verify.
PayFlux commits your expectation to the registry before the customer pays: merchant, destination, reference, minimum amount, expiry. It cannot change that afterwards. PayFlux states the expectation; Flare's attestation providers state the fact. Neither can produce a verified payment alone.
FXRP and C2FLR — read directly
Coston2 is the chain PayFlux runs on, so there is nothing to attest. The transaction and its receipt are read straight off the ledger and checked for destination, amount, status and confirmations.
How a transfer is tied to your order
Not by sender address — that breaks the moment a customer pays from an exchange or pays twice. XRP payments carry a 32-byte memo that FDC itself decodes and reports, so the binding is verified by Flare rather than trusted from our database.
Your transaction hash is a hint
transactionHashHint to /verify, it only narrows which transaction to check. The outcome is decided by the attested data, not by what you sent.How it works
Settlement
FXRP is not a wrapper token that can be minted on demand. FAssets minting is a three-party protocol: collateral is reserved with an agent, the underlying XRP is sent to that agent, and an attestation of that payment mints the FXRP.
So PayFlux does not bolt a conversion onto the side of your payment. It makes the customer's payment be the minting payment — one transfer, one attestation, used twice: once to record the payment, once to mint. The customer's XRP literally becomes the backing for your FXRP.
Lot quantisation
A settlement is only completed when there is a confirmed transaction and your balance actually increased. There is no code path that marks a settlement complete without both.
There is deliberately no XRP → USDT route. PayFlux has no swap infrastructure, so offering one would mean inventing a conversion rate.
Reference
Errors
{
"error": { "code": "ASSET_UNSUPPORTED", "message": "Asset \"DOGE\" is not supported…" },
"requestId": "req_9f2c…"
}| Code | Status | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid key |
INSUFFICIENT_SCOPE | 403 | Key lacks the required scope |
ASSET_UNSUPPORTED | 400 | Asset has no implemented path |
ROUTE_UNAVAILABLE | 409 | Supported, but not executable now |
IDEMPOTENCY_KEY_REUSED | 409 | Same key, different body |
VALIDATION_FAILED | 422 | Body failed validation |
RATE_LIMITED | 429 | 120 requests/min per key |
QUOTE_UNAVAILABLE | 503 | No live price feed — never a guess |
FASSETS_UNAVAILABLE | 503 | No agent capacity right now |
Every response carries X-Request-ID. Quote it in a support thread and the whole request can be traced.
Reference
API reference
/v1/paymentsAPI keyCreate a payment intent. Send Idempotency-Key.
/v1/paymentsAPI keyList your payments.
/v1/payments/:idPublicRead a payment — safe to expose to the customer.
/v1/payments/:id/routesPublicLive routes with scores and reasons.
/v1/payments/:id/eventsPublicThe payment's audit trail.
/v1/payments/:id/streamPublicServer-sent events — status changes as they happen.
/v1/payments/:id/select-assetPublicCustomer chooses how to pay. Locks a quote and commits the intent on-chain.
/v1/payments/:id/verifyPublicAsk PayFlux to re-check the chain.
/v1/payments/:id/settleAPI keySettle a verified payment.
/v1/settlementsAPI keyList settlements.
/v1/assetsPublicSupported assets and their capabilities.
/v1/healthPublicWhat is actually live right now.
/v1/api-keysSigned inList keys. Create, rotate and revoke live here too.
/v1/account/settingsSigned inYour settlement addresses and webhook config.
Be aware
Limits & caveats
| Limit | Value |
|---|---|
| Rate limit | 120 requests / minute, per key |
| API keys per account | 5 live |
| Rotation grace window | 24 hours (configurable, 0 = immediate) |
| Open FAssets reservations | 3 per account |
| Payment window | 15 minutes |
| Quote lifetime | 5 minutes |
| Amount tolerance | 50 bps |
Things worth knowing before you build on this:
- Verification takes 3–4 minutes, dominated by the FDC voting round. That wait is the cost of a trust-minimised proof.
- FXRP payments are matched on amount and time window — ERC-20 transfers have no memo field. When two orders expect the same amount, PayFlux asks for a transaction hash rather than guessing.
- BTC and DOGE are listed as unsupported. FDC can attest them, but PayFlux has no watcher or settlement path yet.
- One account, one human. No team accounts, invites or roles yet.
Testnet
The full blueprint — architecture, design decisions, and an honest list of what is and isn't real — lives in PAYFLUX.md in the repository.