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.

typescript
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

PayFlux runs on Flare Coston2 and XRPL Testnet. No mainnet value moves. The API refuses to start if the network configuration is inconsistent.

Get started

Quickstart

  1. 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. 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. 3

    Create an API key

    In API keys, pick the scopes you need and create a key. It is shown once.
  4. 4

    Create a payment

    bash
    curl -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. 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.

CredentialWho holds itCan do
X-API-Key: sk_ctn2_…Your serverCreate and read payments, settlements, webhooks
Authorization: Bearer <id-token>You, in a browserManage 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 marker

Keys 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.

ScopeGrants
payments:writeCreate payments, trigger verification
payments:readRead payments, routes and events
settlements:readRead settlements and quotes
settlements:writeExecute settlements
webhooks:readRead webhook config and deliveries
webhooks:writeSend 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:

json
{
  "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

typescript
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

Same key + same body returns the same payment. Same key + a different body returns 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 / refunded

You 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.

GET /v1/payments/:id/routes
{
  "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?

A recommendation nobody can interrogate is an arbitrary default. Every route carries its reasons, including its costs — lot rounding appears in the same list as the benefits.

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.

Verifying a delivery
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

Key order and whitespace do not survive a JSON round trip, so verifying a re-serialized object will always fail.

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.failed

The 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.verifyPayment

The 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

If you pass 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

FAssets mints whole lots — currently 10 XRP on Coston2. A $5 payment becomes 10 XRP plus the agent's fee. PayFlux shows this as price impact on the route rather than hiding it.

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

json
{
  "error": { "code": "ASSET_UNSUPPORTED", "message": "Asset \"DOGE\" is not supported…" },
  "requestId": "req_9f2c…"
}
CodeStatusMeaning
UNAUTHORIZED401Missing or invalid key
INSUFFICIENT_SCOPE403Key lacks the required scope
ASSET_UNSUPPORTED400Asset has no implemented path
ROUTE_UNAVAILABLE409Supported, but not executable now
IDEMPOTENCY_KEY_REUSED409Same key, different body
VALIDATION_FAILED422Body failed validation
RATE_LIMITED429120 requests/min per key
QUOTE_UNAVAILABLE503No live price feed — never a guess
FASSETS_UNAVAILABLE503No 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

POST/v1/paymentsAPI key

Create a payment intent. Send Idempotency-Key.

GET/v1/paymentsAPI key

List your payments.

GET/v1/payments/:idPublic

Read a payment — safe to expose to the customer.

GET/v1/payments/:id/routesPublic

Live routes with scores and reasons.

GET/v1/payments/:id/eventsPublic

The payment's audit trail.

GET/v1/payments/:id/streamPublic

Server-sent events — status changes as they happen.

POST/v1/payments/:id/select-assetPublic

Customer chooses how to pay. Locks a quote and commits the intent on-chain.

POST/v1/payments/:id/verifyPublic

Ask PayFlux to re-check the chain.

POST/v1/payments/:id/settleAPI key

Settle a verified payment.

GET/v1/settlementsAPI key

List settlements.

GET/v1/assetsPublic

Supported assets and their capabilities.

GET/v1/healthPublic

What is actually live right now.

GET/v1/api-keysSigned in

List keys. Create, rotate and revoke live here too.

PATCH/v1/account/settingsSigned in

Your settlement addresses and webhook config.

Be aware

Limits & caveats

LimitValue
Rate limit120 requests / minute, per key
API keys per account5 live
Rotation grace window24 hours (configurable, 0 = immediate)
Open FAssets reservations3 per account
Payment window15 minutes
Quote lifetime5 minutes
Amount tolerance50 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

Coston2 and XRPL Testnet only. Nothing here moves real value, and this is not audited software.

The full blueprint — architecture, design decisions, and an honest list of what is and isn't real — lives in PAYFLUX.md in the repository.