When it fires
The customer sent a payment to the deposit address, but the amount received (data.payment.amount) is less than the invoice amount (data.invoice.amount).
What to do
Do not fulfill the order. The full payment has not been received.
You have three options:
-
Wait for a top-up. The deposit address remains active. If the customer sends the remaining amount, Settlx will detect it and the invoice will proceed to settlement automatically. Best for situations where the shortfall may be due to a wallet fee deduction.
-
Accept the partial payment. Via the merchant dashboard, you can manually accept the partial amount as full payment. This triggers settlement for the amount received. Best for digital goods where you are willing to accept a slightly lower amount.
-
Contact support for a refund. If you want to return the funds to the customer, contact Settlx support. Do not create a new invoice until the original is resolved.
The recommended action depends on your business context — see the Handling Partial Payments guide for a full decision framework.
The deposit address stays active after invoice.underpaid fires. Any additional payment sent to the same address will be detected and applied to the invoice automatically.
How much the customer still owes
The webhook payload tells you the shortfall directly — the amount of the payment currency (e.g. BNB) the customer still needs to send to the same deposit address to complete the invoice.
| Field | Description |
|---|
data.shortfall_amount | Amount still owed, in data.shortfall_currency |
data.shortfall_currency | The currency of shortfall_amount — always the payment currency, the asset the customer is paying with |
Multiple top-up payments to the same deposit address are supported and added cumulatively. As long as the invoice hasn’t expired, the customer can keep sending until the cumulative total clears the 1% tolerance window — at which point invoice.confirmed fires automatically and settlement begins.
The exchange rate is locked at invoice creation. If the price of the payment currency moves between the partial and the top-up, the customer may end up over- or under-paying relative to the original fiat target. Use shortfall_amount from the latest invoice.underpaid webhook to tell the customer exactly how much more to send right now.
Payload
Includes: invoice, payment, fees (estimated — final fees are confirmed in invoice.settled).
{
"event": "invoice.underpaid",
"eventId": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890_invoice.underpaid_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": "pending",
"createdAt": "2026-04-12T10:00:00.000Z",
"metadata": { "orderId": "order_123", "customerId": "cust_456" }
},
"payment": {
"id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"transactionHash": "0xabc123...",
"amount": "39.99",
"currency": "USDT",
"chain": "polygon",
"confirmations": 12,
"status": "confirmed"
},
"fees": {
"platformFee": "0.60",
"platformFeePercent": "1.5%",
"networkFee": "0.50",
"providerFee": "0.00",
"totalFees": "1.10",
"currency": "USDT"
},
"shortfall_amount": "0.01546288",
"shortfall_currency": "USDT"
}
}
The example above shows a USDT-paid invoice. For BNB → USDC settlements (or any cross-currency case), shortfall_currency will be the payment currency (BNB), not the settlement currency (USDC).
Handler example
app.post('/webhooks/settlx', express.raw({ type: 'application/json' }), async (req, res) => {
verifySignature(req);
const event = JSON.parse(req.body);
if (event.event === 'invoice.underpaid') {
const { invoice, payment, shortfall_amount, shortfall_currency } = event.data;
const orderId = invoice.metadata?.orderId;
const customerId = invoice.metadata?.customerId;
// Mark the order as awaiting top-up — do not cancel yet
await db.orders.update(orderId, { status: 'payment_partial' });
// Use shortfall_amount + shortfall_currency directly — they're computed
// from the locked exchange rate at invoice creation, so this is exactly
// how much MORE the customer needs to send to the same deposit address.
await sendEmail(customerId, {
subject: 'Your payment is incomplete',
body: `We received ${payment.amount} ${payment.currency} for your order. `
+ `Please send ${shortfall_amount} ${shortfall_currency} more to the same deposit address `
+ `before the invoice expires.`,
});
}
res.status(200).json({ received: true });
});
invoice.settled — Fires after the full payment is completed and funds are in your wallet.
invoice.expired — Fires if the invoice expires before the shortfall is resolved.