Skip to main content

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

Request headers

Every webhook request includes the following headers:
HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature in the format sha256=<hex>
X-Webhook-EventThe event type, e.g. invoice.settled
X-Webhook-Event-IdUnique event identifier — use this for idempotency
X-Webhook-TimestampISO 8601 timestamp of when the event was generated
Content-TypeAlways 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.
Node.js
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 });
});
Python
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

FieldTypeDescription
eventstringEvent type. See Event types
eventIdstringUnique identifier for this event — use for idempotency
timestampstringISO 8601 timestamp of when the event was generated
data.invoiceobjectAlways present. Core invoice fields
data.paymentobjectPresent when a payment has been detected
data.conversionobjectPresent when a currency conversion occurred
data.settlementobjectPresent on invoice.settled
data.feesobjectAlways present. Fee breakdown

data.invoice

FieldTypeDescription
idstringInvoice ID
amountstringOriginal invoiced amount
currencystringOriginal invoiced currency
settlementCurrencystringCurrency funds are settled in (e.g. USDT)
settlementChainstringChain funds are settled on (e.g. polygon)
statusstringInvoice status at time of event
createdAtstringISO 8601 creation timestamp
metadataobject | nullMetadata you passed at invoice creation

data.payment

FieldTypeDescription
idstringPayment ID
transactionHashstringOn-chain transaction hash
amountstringAmount received
currencystringToken received (e.g. USDT, ETH)
chainstringChain payment was made on
confirmationsnumberNumber of block confirmations
statusstringPayment status

data.conversion

Present only when the received token differs from your settlement currency and a swap was performed.
FieldTypeDescription
idstringConversion ID
providerstringSwap provider used
sourceAmountstringAmount before conversion
sourceCurrencystringToken swapped from
destinationAmountstringAmount after conversion
destinationCurrencystringToken swapped to
exchangeRatestringExchange rate applied
statusstringConversion status

data.settlement

Present on invoice.settled.
FieldTypeDescription
idstringSettlement ID
transactionHashstringOn-chain transfer hash
grossAmountstringAmount before fees
netAmountstringAmount received in your wallet after fees
currencystringSettlement currency
chainstringSettlement chain
statusstringSettlement status

data.fees

FieldTypeDescription
platformFeestringSettlx platform fee amount
platformFeePercentstringPlatform fee as a percentage (e.g. "1.5%")
networkFeestringEstimated on-chain gas fee
providerFeestringSwap provider fee, if conversion occurred
totalFeesstringSum of all fees
currencystringCurrency 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.
AttemptDelay after previous attempt
21 minute
35 minutes
415 minutes
51 hour
66 hours
7–1024 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.