Skip to main content

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.

POST your-webhook-url
{
"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โ€‹

FieldTypeNotes
eventstring"payment.confirmed"
invoiceIdstringThe invoice ID you stored when creating the invoice
timestampISO8601When HelaMesh enqueued this delivery
amountstringDecimal USDT amount
tokenstring"USDT"
networkstring"TRON" or "BSC"
txHashstringOn-chain transaction hash. Prefixed with sim_ for simulated payments.
confirmationsnumberNumber of confirmations at the moment of webhook enqueue
simulatedbooleantrue 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.

POST your-webhook-url
{
"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โ€‹

FieldTypeNotes
eventstring"transfer.confirmed"
timestampISO8601When HelaMesh enqueued this delivery
hdIndexnumberThe HD derivation index โ€” maps to the user in your system
toAddressstringThe derived address that received the funds
fromAddressstringThe sender's on-chain address
amountstringDecimal USDT amount received
tokenstring"USDT"
networkstring"TRON" or "BSC"
txHashstringOn-chain transaction hash
confirmationsnumberConfirmation 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 }
});