How webhooks work
When an event occurs on an invoice — payment confirmed, funds settled, invoice expired — Settlx sends a POST request to the webhookUrl you provided when creating the invoice.
Every request:
- Is signed with HMAC-SHA256 using your webhook secret
- Has a 30-second delivery timeout
- Is retried up to 10 times on failure with exponential backoff
- Returns a unique
eventId you can use for idempotent processing
Every webhook request includes the following headers:
| Header | Description |
|---|
X-Webhook-Signature | HMAC-SHA256 signature in the format sha256=<hex> |
X-Webhook-Event | The event type, e.g. invoice.settled |
X-Webhook-Event-Id | Unique event identifier — use this for idempotency |
X-Webhook-Timestamp | ISO 8601 timestamp of when the event was generated |
Content-Type | Always application/json |
Signature verification
Always verify the signature before processing a webhook. This confirms the request came from Settlx and the body has not been tampered with.
Use express.raw() — not express.json() — for your webhook route. Signature verification requires the raw request body bytes. Parsing it to JSON first will break the HMAC.
const crypto = require('crypto');
app.post('/webhooks/settlx', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.SETTLX_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
handleEvent(event);
res.status(200).json({ received: true });
});
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/settlx', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature', '')
expected = 'sha256=' + hmac.new(
os.environ['SETTLX_WEBHOOK_SECRET'].encode(),
request.data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json(force=True)
handle_event(event)
return {'received': True}, 200
Payload structure
All webhook events share the same top-level structure. The data object contains fields relevant to the specific event.
{
"event": "invoice.settled",
"eventId": "evt_a1b2c3d4_invoice.settled_1744455600000",
"timestamp": "2026-04-12T11:00:00.000Z",
"data": {
"invoice": { ... },
"payment": { ... },
"conversion": { ... },
"settlement": { ... },
"fees": { ... }
}
}
Top-level fields
| Field | Type | Description |
|---|
event | string | Event type. See Event types |
eventId | string | Unique identifier for this event — use for idempotency |
timestamp | string | ISO 8601 timestamp of when the event was generated |
data.invoice | object | Always present. Core invoice fields |
data.payment | object | Present when a payment has been detected |
data.conversion | object | Present when a currency conversion occurred |
data.settlement | object | Present on invoice.settled |
data.fees | object | Always present. Fee breakdown |
data.invoice
| Field | Type | Description |
|---|
id | string | Invoice ID |
amount | string | Original invoiced amount |
currency | string | Original invoiced currency |
settlementCurrency | string | Currency funds are settled in (e.g. USDT) |
settlementChain | string | Chain funds are settled on (e.g. polygon) |
status | string | Invoice status at time of event |
createdAt | string | ISO 8601 creation timestamp |
metadata | object | null | Metadata you passed at invoice creation |
data.payment
| Field | Type | Description |
|---|
id | string | Payment ID |
transactionHash | string | On-chain transaction hash |
amount | string | Amount received |
currency | string | Token received (e.g. USDT, ETH) |
chain | string | Chain payment was made on |
confirmations | number | Number of block confirmations |
status | string | Payment status |
data.conversion
Present only when the received token differs from your settlement currency and a swap was performed.
| Field | Type | Description |
|---|
id | string | Conversion ID |
provider | string | Swap provider used |
sourceAmount | string | Amount before conversion |
sourceCurrency | string | Token swapped from |
destinationAmount | string | Amount after conversion |
destinationCurrency | string | Token swapped to |
exchangeRate | string | Exchange rate applied |
status | string | Conversion status |
data.settlement
Present on invoice.settled.
| Field | Type | Description |
|---|
id | string | Settlement ID |
transactionHash | string | On-chain transfer hash |
grossAmount | string | Amount before fees |
netAmount | string | Amount received in your wallet after fees |
currency | string | Settlement currency |
chain | string | Settlement chain |
status | string | Settlement status |
data.fees
| Field | Type | Description |
|---|
platformFee | string | Settlx platform fee amount |
platformFeePercent | string | Platform fee as a percentage (e.g. "1.5%") |
networkFee | string | Estimated on-chain gas fee |
providerFee | string | Swap provider fee, if conversion occurred |
totalFees | string | Sum of all fees |
currency | string | Currency all fee amounts are denominated in |
Event types
invoice.confirmed
Payment received and confirmed on-chain. Settlement is now in progress.
This is an informational event. Do not fulfill orders here — the funds are not yet in your wallet. Wait for invoice.settled.
Payload includes: invoice, payment, fees
{
"event": "invoice.confirmed",
"eventId": "evt_a1b2c3d4_invoice.confirmed_1744455600000",
"timestamp": "2026-04-12T11:00:00.000Z",
"data": {
"invoice": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"amount": "49.99",
"currency": "USD",
"settlementCurrency": "USDT",
"settlementChain": "polygon",
"status": "confirmed",
"createdAt": "2026-04-12T10:00:00.000Z",
"metadata": { "orderId": "order_123" }
},
"payment": {
"id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"transactionHash": "0xabc123...",
"amount": "49.99",
"currency": "USDT",
"chain": "polygon",
"confirmations": 12,
"status": "confirmed"
},
"fees": {
"platformFee": "0.75",
"platformFeePercent": "1.5%",
"networkFee": "0.50",
"providerFee": "0.00",
"totalFees": "1.25",
"currency": "USDT"
}
}
}
invoice.settled
Funds have been transferred to your wallet. This is the event to act on.
Payload includes: invoice, payment, conversion (if swap occurred), settlement, fees
{
"event": "invoice.settled",
"eventId": "evt_a1b2c3d4_invoice.settled_1744455900000",
"timestamp": "2026-04-12T11:05:00.000Z",
"data": {
"invoice": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"amount": "49.99",
"currency": "USD",
"settlementCurrency": "USDT",
"settlementChain": "polygon",
"status": "settled",
"createdAt": "2026-04-12T10:00:00.000Z",
"metadata": { "orderId": "order_123" }
},
"payment": {
"id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"transactionHash": "0xabc123...",
"amount": "49.99",
"currency": "USDT",
"chain": "polygon",
"confirmations": 12,
"status": "confirmed"
},
"settlement": {
"id": "d4e5f6a7-b8c9-0123-def0-456789012345",
"transactionHash": "0xdef456...",
"grossAmount": "49.99",
"netAmount": "48.74",
"currency": "USDT",
"chain": "polygon",
"status": "completed"
},
"fees": {
"platformFee": "0.75",
"platformFeePercent": "1.5%",
"networkFee": "0.50",
"providerFee": "0.00",
"totalFees": "1.25",
"currency": "USDT"
}
}
}
invoice.expired
The invoice expired before full payment was received. Cancel the associated order.
Payload includes: invoice, fees
invoice.underpaid
A payment was received but the amount is less than the invoice total. The invoice status becomes partial.
Use this event to notify your customer that additional funds are required, or to cancel the order.
Payload includes: invoice, payment, fees
invoice.overpaid
A payment was received that exceeds the invoice amount. The excess is included in the settlement.
Payload includes: invoice, payment, fees
invoice.wrong_token
A payment was received but in a different token or on a different chain than expected. The funds cannot be automatically processed.
Contact Settlx support if this occurs — the funds are not lost.
Payload includes: invoice, payment, fees
invoice.partial_accepted
A partial payment was manually accepted. The invoice proceeds to settlement with the amount that was received.
Payload includes: invoice, payment, fees
Retry policy
If your endpoint returns anything other than a 2xx status, or does not respond within 30 seconds, Settlx retries the delivery.
| Attempt | Delay after previous attempt |
|---|
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 6 hours |
| 7–10 | 24 hours |
After 10 failed attempts the webhook is marked as permanently failed. You can view delivery logs in your dashboard.
Best practices
Return 200 immediately. Do all processing asynchronously. A slow or erroring handler will trigger retries.
Use eventId for idempotency. The same event can be delivered more than once. Store processed event IDs and skip duplicates.
Only fulfill on invoice.settled. This is the only event that guarantees funds are in your wallet.
Always verify the signature. Never process a webhook payload that fails signature verification.