Webhook payload
HelaMesh fires two types of webhook events. Every delivery uses the same signing format and retry behaviour โ only the payload shape differs.
payment.confirmedโ
Fired when an invoice-based payment reaches the required confirmation depth.
{
"event": "payment.confirmed",
"invoiceId": "65f8a1b2c3d4e5f6a7b8c9d0",
"timestamp": "2026-04-10T18:32:14.000Z",
"amount": "25.000000",
"token": "USDT",
"network": "TRON",
"txHash": "abc123โฆ",
"confirmations": 20,
"simulated": false
}
Sent with these headers:
Content-Type: application/json
User-Agent: HelaMesh-Webhooks/1.0
X-HelaMesh-Signature: t=1700000000,v1=<hex>
Field referenceโ
| Field | Type | Notes |
|---|---|---|
event | string | "payment.confirmed" |
invoiceId | string | The invoice ID you stored when creating the invoice |
timestamp | ISO8601 | When HelaMesh enqueued this delivery |
amount | string | Decimal USDT amount |
token | string | "USDT" |
network | string | "TRON" or "BSC" |
txHash | string | On-chain transaction hash. Prefixed with sim_ for simulated payments. |
confirmations | number | Number of confirmations at the moment of webhook enqueue |
simulated | boolean | true when triggered by the sandbox simulate action โ never true for live invoices. Route these to staging. |
transfer.confirmedโ
Fired when a USDT transfer arrives at a derived sub-wallet address (HD wallet flow). There is no invoice involved โ this is the event for platforms that give every user their own address via POST /v1/wallets/derive.
{
"event": "transfer.confirmed",
"timestamp": "2026-04-10T18:32:14.000Z",
"hdIndex": 42,
"toAddress": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"fromAddress": "TGj1Ej1qRzL9feLTLhjwgxXF4Ct6GTWg2U",
"amount": "25.000000",
"token": "USDT",
"network": "TRON",
"txHash": "abc123โฆ",
"confirmations": 20
}
Field referenceโ
| Field | Type | Notes |
|---|---|---|
event | string | "transfer.confirmed" |
timestamp | ISO8601 | When HelaMesh enqueued this delivery |
hdIndex | number | The HD derivation index โ maps to the user in your system |
toAddress | string | The derived address that received the funds |
fromAddress | string | The sender's on-chain address |
amount | string | Decimal USDT amount received |
token | string | "USDT" |
network | string | "TRON" or "BSC" |
txHash | string | On-chain transaction hash |
confirmations | number | Confirmation depth at time of enqueue |
Handling transfer.confirmedโ
Use hdIndex to look up which user in your system owns that address, then credit their balance. Always check for duplicate txHash before crediting โ HelaMesh may deliver the same event twice on network blips.
app.post('/webhooks/helamesh', express.raw({ type: 'application/json' }), async (req, res) => {
// ... verify signature first (see Verifying signatures) ...
const event = JSON.parse(req.body.toString());
if (event.event === 'transfer.confirmed') {
const user = await db.users.findOne({ hdIndex: event.hdIndex });
if (!user) return res.status(200).send('ok'); // unknown index โ ignore
// Idempotency: skip if we already credited this tx
const exists = await db.transactions.findOne({ txHash: event.txHash });
if (exists) return res.status(200).send('ok');
await db.$transaction([
db.transactions.create({
data: {
userId: user.id,
type: 'DEPOSIT',
amount: parseFloat(event.amount),
txHash: event.txHash,
network: event.network,
status: 'COMPLETED',
},
}),
db.users.update({
where: { id: user.id },
data: { balance: { increment: parseFloat(event.amount) } },
}),
]);
}
res.status(200).send('ok');
});
Respect the simulated flagโ
Every webhook triggered by the sandbox simulate endpoint carries "simulated": true at the top level. If your handler moves real balances, updates accounting, or triggers irreversible fulfillment, branch on this flag and route simulated events to a staging path โ never mirror them into production tables.
app.post('/webhooks/helamesh', async (req, res) => {
// ... verify signature ...
const event = JSON.parse(req.body.toString());
if (event.simulated) {
// Test webhook โ update staging tables, don't touch prod
await stagingDb.orders.update(/* โฆ */);
} else {
// Real payment โ full fulfillment flow
await db.orders.update(/* โฆ */);
await sendConfirmationEmail(/* โฆ */);
}
res.status(200).send('ok');
});
Live invoices can never produce simulated: true โ the simulate endpoint is rejected at the service layer for live clients. You only need to care about this flag if you use the sandbox. See Sandbox (test mode) for the full test-mode flow.
Your handler must be idempotentโ
HelaMesh may deliver the same event twice if your endpoint ack'd 2xx but we didn't see it (network blip). Always check your database for an existing record before applying the payment:
await db.orders.update({
where: {
helamesh_invoice_id: event.invoiceId,
payment_status: { not: 'paid' } // skip if already paid
},
data: { payment_status: 'paid', tx_hash: event.txHash }
});