Webhooks
Instead of polling, register a webhook URL and fluxa will POST a signed JSON event to it whenever something happens to your orders, subscriptions or settlements. Verify the signature on every delivery, return 2xx quickly, and you have a reliable, real-time integration.
Configure your endpoint
In the merchant portal, set your webhook URL and (optionally) narrow the event subscription to the event types you care about. An empty subscription means all events. The portal also shows your webhook signing secret, which you use to verify deliveries — keep it server-side.
Delivery format
Each delivery is an HTTP POST with a JSON body and these headers:
- Name
X-Fluxa-Event- Type
- string
- Description
The event type, e.g.
payment.succeeded. Also present as theeventfield in the body.
- Name
X-Fluxa-Timestamp- Type
- string
- Description
Unix seconds when the delivery was signed.
- Name
X-Fluxa-Signature- Type
- string
- Description
hex(HMAC_SHA256(secret, "<timestamp>.<body>"))— see below.
- Name
X-Fluxa-Encryption- Type
- string
- Description
Present and set to
A256GCMonly when payload encryption is enabled for your account. See Encrypted payloads.
Verifying the signature
The signature is the lowercase hex HMAC-SHA256 of the string "<timestamp>.<body>" — the X-Fluxa-Timestamp value, a literal dot, then the raw request body — keyed by your webhook signing secret.
Compute the same value over the bytes you received and compare in constant time. Reject the delivery if it doesn't match. Use the raw body exactly as received — don't re-serialize the parsed JSON, or the bytes (and the hash) may differ.
Verify a webhook
import crypto from 'node:crypto'
const SECRET = 'your_webhook_signing_secret'
// Capture the raw body, e.g. express.raw({ type: 'application/json' }),
// then verify before parsing — the signature covers the exact bytes received.
export function verify(rawBody, headers) {
const ts = headers['x-fluxa-timestamp']
const sig = headers['x-fluxa-signature']
const signed = `${ts}.${rawBody}`
const expected = crypto.createHmac('sha256', SECRET).update(signed).digest('hex')
return (
sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
)
}
Event types
Every payload includes an event field. fluxa delivers these event types:
- Name
payment.succeeded- Type
- Description
An order was fully paid and settled.
- Name
payment.failed- Type
- Description
A payment attempt failed terminally.
- Name
payment.refunded- Type
- Description
An order was refunded, in part or in full.
- Name
settlement.completed- Type
- Description
A non-custodial on-chain order's net amount was swept to your settlement address (proof of receipt).
- Name
subscription.charged- Type
- Description
A subscription generated a new cycle's charge.
- Name
subscription.completed- Type
- Description
A subscription reached its end condition.
- Name
subscription.payment_failed- Type
- Description
A subscription's billing failed repeatedly and moved to
past_due.
Payloads
Payment events carry the order summary. paid_at is present once the order is
paid. settlement.completed adds the on-chain transfer details, and
subscription events carry the subscription id and cycle.
payment.succeeded
{
"event": "payment.succeeded",
"order_id": "ord_2Zx9Qw8sUaM2",
"merchant_order_id": "order-1001",
"amount": "49.90",
"currency": "USDT",
"status": "paid",
"channel": "usdt_erc20",
"paid_at": "2026-06-11T08:30:00Z"
}
settlement.completed
{
"event": "settlement.completed",
"order_id": "ord_2Zx9Qw8sUaM2",
"merchant_order_id": "order-1001",
"chain": "usdt_erc20",
"asset": "USDT",
"settlement_tx": "0xabc...",
"settlement_address": "0xYourAddress",
"amount": "48.40",
"fee": "1.50",
"currency": "USDT"
}
subscription.charged
{
"event": "subscription.charged",
"subscription_id": "sub_9Qw8",
"subscription_cycle": 3,
"order_id": "ord_5K2m",
"amount": "19.00",
"currency": "USDT",
"next_charge_at": "2026-07-11T00:00:00Z"
}
Retries
Your endpoint should return a 2xx status quickly (do heavy work asynchronously). Any non-2xx response or connection error is retried with exponential backoff — up to 8 attempts, with the delay capped at 6 hours. After the final attempt the delivery is marked failed. You can inspect recent deliveries — including attempt counts and the last error — via Webhook deliveries or the portal.
Deliveries can arrive more than once (a retry after your endpoint succeeded but the response was
lost). Make your handler idempotent by keying on order_id + event.
Encrypted payloads
When payload encryption is enabled for your account, the body is an AES-256-GCM envelope instead of plaintext JSON, and X-Fluxa-Encryption: A256GCM is set. The signature still covers the envelope you receive, so verify it first, then decrypt. The envelope is:
Encrypted envelope
{ "alg": "A256GCM", "data": "base64(nonce || ciphertext || tag)" }
The key is SHA-256(webhook signing secret). Decrypt data with AES-256-GCM using the first 12 bytes as the nonce to recover the plaintext JSON event.