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.

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.

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

FieldTypeNotes
eventstring"payment_link.confirmed"
paymentLinkIdstringThe payment link ID โ€” use this to look up the order in your DB
merchantRefstring | nullYour reference from link creation, echoed back
timestampISO8601When HelaMesh enqueued this delivery
confirmedRailstring"MPESA", "USDT_TRON", or "USDT_BSC"
usdtAmountstringUSDT amount on the link (the price you set)
mpesaAmountnumber | nullKES amount. Present for all rails โ€” gives you the KES equivalent.
externalIdstringM-Pesa receipt code (e.g. QHF2ABJK9L) or on-chain tx hash
payerstring | nullM-Pesa phone number (254โ€ฆ) or on-chain sender address
metadataobject | nullWhatever you passed when creating the link
environmentstring"test" or "live"
simulatedbooleantrue 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.

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

FieldTypeNotes
eventstring"transfer.confirmed"
timestampISO8601When HelaMesh enqueued this delivery
hdIndexnumberHD derivation index โ€” maps to the user in your system
toAddressstringDerived address that received the funds
fromAddressstringSender's on-chain address
amountstringDecimal USDT received
networkstring"USDT_TRON" or "USDT_BSC"
txHashstringOn-chain transaction hash
confirmationsnumberConfirmation 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 on externalId (M-Pesa receipt or tx hash)
  • transfer.confirmed โ†’ deduplicate on txHash