Webhook payload
HelaMesh fires two types of webhook events. Every delivery uses the same signing format and retry behaviour โ only the payload shape differs.
Sent with these headers on every delivery:
Content-Type: application/json
User-Agent: HelaMesh-Webhooks/1.0
X-HelaMesh-Signature: t=1700000000,v1=<hex>
payment_link.confirmedโ
Fired when a payment link is paid via any rail โ M-Pesa, TRON, or BSC. Check confirmedRail to see which one.
{
"event": "payment_link.confirmed",
"paymentLinkId": "6a20334a21638503775afa39",
"merchantRef": "order_1042",
"timestamp": "2026-06-03T12:32:14.000Z",
"confirmedRail": "MPESA",
"usdtAmount": "100",
"mpesaAmount": 13421,
"externalId": "QHF2ABJK9L",
"payer": "254712345678",
"metadata": { "customerId": "cus_abc123" },
"environment": "test",
"simulated": false
}
M-Pesa payment exampleโ
{
"event": "payment_link.confirmed",
"paymentLinkId": "6a20334a21638503775afa39",
"merchantRef": "order_1042",
"timestamp": "2026-06-03T12:32:14.000Z",
"confirmedRail": "MPESA",
"usdtAmount": "100",
"mpesaAmount": 13421,
"externalId": "QHF2ABJK9L",
"payer": "254712345678",
"metadata": { "customerId": "cus_abc123" },
"environment": "test",
"simulated": false
}
Crypto payment example (TRON)โ
{
"event": "payment_link.confirmed",
"paymentLinkId": "6a20334a21638503775afa39",
"merchantRef": "order_1042",
"timestamp": "2026-06-03T12:32:14.000Z",
"confirmedRail": "USDT_TRON",
"usdtAmount": "100",
"mpesaAmount": null,
"externalId": "a1b2c3d4e5f6โฆ",
"payer": "TGj1Ej1qRzL9feLTLhjwgxXF4Ct6GTWg2U",
"metadata": { "customerId": "cus_abc123" },
"environment": "live",
"simulated": false
}
Field referenceโ
| Field | Type | Notes |
|---|---|---|
event | string | "payment_link.confirmed" |
paymentLinkId | string | The payment link ID โ use this to look up the order in your DB |
merchantRef | string | null | Your reference from link creation, echoed back |
timestamp | ISO8601 | When HelaMesh enqueued this delivery |
confirmedRail | string | "MPESA", "USDT_TRON", or "USDT_BSC" |
usdtAmount | string | USDT amount on the link (the price you set) |
mpesaAmount | number | null | KES amount. Present for all rails โ gives you the KES equivalent. |
externalId | string | M-Pesa receipt code (e.g. QHF2ABJK9L) or on-chain tx hash |
payer | string | null | M-Pesa phone number (254โฆ) or on-chain sender address |
metadata | object | null | Whatever you passed when creating the link |
environment | string | "test" or "live" |
simulated | boolean | true for sandbox test payments โ never true on live keys |
Handling payment_link.confirmedโ
app.post('/webhooks/helamesh', express.raw({ type: 'application/json' }), async (req, res) => {
// Always verify the signature first โ see Verifying signatures
verifySignature(req);
const event = JSON.parse(req.body.toString());
if (event.event === 'payment_link.confirmed') {
// Idempotency โ externalId is the receipt/txHash, unique per payment
const alreadyProcessed = await db.payments.findOne({ externalId: event.externalId });
if (alreadyProcessed) return res.status(200).send('ok');
await db.orders.update({
where: { paymentLinkId: event.paymentLinkId },
data: {
status: 'paid',
paidVia: event.confirmedRail,
receiptId: event.externalId,
paidBy: event.payer,
},
});
await sendConfirmationEmail(event.paymentLinkId);
}
res.status(200).send('ok');
});
transfer.confirmedโ
Fired when a USDT transfer arrives at a derived sub-wallet address โ the HD wallet flow for platforms that give every user their own deposit address via POST /v1/wallets/derive. No payment link is involved.
{
"event": "transfer.confirmed",
"timestamp": "2026-06-03T12:32:14.000Z",
"hdIndex": 42,
"toAddress": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"fromAddress": "TGj1Ej1qRzL9feLTLhjwgxXF4Ct6GTWg2U",
"amount": "25.000000",
"network": "USDT_TRON",
"txHash": "abc123โฆ",
"confirmations": 20
}
Field referenceโ
| Field | Type | Notes |
|---|---|---|
event | string | "transfer.confirmed" |
timestamp | ISO8601 | When HelaMesh enqueued this delivery |
hdIndex | number | HD derivation index โ maps to the user in your system |
toAddress | string | Derived address that received the funds |
fromAddress | string | Sender's on-chain address |
amount | string | Decimal USDT received |
network | string | "USDT_TRON" or "USDT_BSC" |
txHash | string | On-chain transaction hash |
confirmations | number | Confirmation depth at time of enqueue |
Respect the simulated flagโ
Sandbox test payments carry "simulated": true. Never apply these to production balances or trigger real fulfilment.
if (event.simulated) {
await stagingDb.orders.update(/* staging only */);
} else {
await db.orders.update(/* real fulfilment */);
await sendConfirmationEmail();
}
Live keys cannot produce simulated: true โ the flag is only possible on hm_test_* keys.
Your handler must be idempotentโ
HelaMesh may deliver the same event twice on network blips. Always guard with a unique key before applying side effects:
payment_link.confirmedโ deduplicate onexternalId(M-Pesa receipt or tx hash)transfer.confirmedโ deduplicate ontxHash