Skip to main content

Webhook Security & Reliability


Security

To run a public endpoint for receiving Webhook messages reliably, consider the following security threats and apply the best practices recommended by Nodit to minimize risk.


Security threats

Replay attacks

A replay attack is when an attacker intercepts a request and resends it multiple times, causing unintended duplicate processing or malicious behavior. For example, if the same Webhook message is replayed by an attacker, payment or data-update logic may run repeatedly, leading to extra load or broken consistency. To mitigate this, implement your business logic so that handling of received Webhook messages is idempotent.

Man-in-the-middle (MITM) attacks

In a MITM attack, the attacker intercepts communication between client and server to eavesdrop or modify payloads. Use encrypted protocols such as HTTPS/TLS to protect the channel and, where possible, verify signatures to authenticate message integrity and origin.

Best Practices

Follow the security guidelines below to reduce threats like replay and MITM attacks and provide a safer experience. These are optional but recommended for stronger security.

Verify message integrity with signatures

Nodit includes a signature for each Webhook message in the x-signature header. Your endpoint can verify this signature using the Signing Key shown when the Webhook was created, to confirm integrity and origin of each message.

What is a signature?

A signature is a message authentication code (MAC) produced by hashing the payload (e.g. with HMAC-SHA256) using a shared secret. If sender and receiver share a key, they can:

  1. The sender sends the payload and the signature.
  2. The receiver uses the shared key to verify the signature and obtain the digest, confirming the sender holds the key.
  3. The receiver hashes the received payload to get a digest.
  4. The receiver compares this digest with the one from the signature; if they match, the payload was not altered.

Retrieving the Signing Key

The Signing Key is created when the Webhook is created/registered and is the same for all messages of that Webhook (by subscriptionId). You can obtain it as follows.

(1) From the console

In the Nodit console Webhook menu, click the [Signing Key] button to view the Signing Key for each Webhook.

signing-key

(2) From the Webhook create API response

When you create a Webhook via the API, the response includes a signingKey field. This is only returned at creation time; it cannot be retrieved later via list/get APIs, so store it when you create the Webhook if you need it.

Verifying Nodit Webhook message signatures

When you receive a Webhook over HTTP, read the signature from the x-signature header and validate it using the steps above. Below are example implementations in several languages.

const crypto = require('crypto');

/**
* Validate the signature using the request body, header signature, and signing key.
*
* @param {Object} body - The request body (JSON object)
* @param {string} signature - The signature string from the header
* @param {string} signingKey - The message signing key used for signing
* @returns {boolean} Returns true if the signature is valid, otherwise false
*/
function isValidSignature(body, signature, signingKey) {
const hmac = crypto.createHmac('sha256', signingKey);
hmac.update(JSON.stringify(body), 'utf8');
return signature === hmac.digest('hex');
}

// --- Sample Data and Execution Example ---

// Example request body
const sampleBody = {
"subscriptionId": "1",
"sequenceNumber": "1",
"description": "Signature Test",
"protocol": "APTOS",
"network": "TESTNET",
"subscriptionType": "WEBHOOK",
"notification": {
"webhookUrl": "YOUR_WEBHOOK_ENDPOINT"
},
"eventType": "EVENT",
"event": {
"eventType": "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f::just::RandomIndexEvent",
"eventAccountAddress": "0x0",
"messages": [
{
"guid": {
"creation_number": "0",
"account_address": "0x0"
},
"sequence_number": "0",
"type": "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f::just::RandomIndexEvent",
"data": {
"price": "44289819"
},
"event_index": 2,
"version": "6627015772"
}
]
},
"createdAt": "2025-02-18T05:52:53.559342615Z"
};
const signature = "da5eedb3f1fa386e095dc4f66a8f21155d22964633e0e6f844c331296ef1abaa";
// Example signing key (should be securely managed in production)
const signingKey = '7b8664b96de828e3b3bacf538c51e0ddcfa4fa6c686e738d8c0aeff5c8545ae7';

// Print the computed hash for debugging purposes
const hmac = crypto.createHmac('sha256', signingKey);
hmac.update(JSON.stringify(sampleBody), 'utf8');
console.log(hmac.digest('hex'));

// Execute the validation logic
if (isValidSignature(sampleBody, signature, signingKey)) {
console.log('Signature is valid.');
} else {
console.log('Signature is invalid.');
}

import json
import hmac
import hashlib

def is_valid_signature(body, signature, signing_key):
"""
Validate the signature using the request body, header signature, and signing key.

:param body: The request body (JSON object)
:param signature: The signature string from the header
:param signing_key: The message signing key used for signing
:return: Returns True if the signature is valid, otherwise False
"""
# Generate JSON string without unnecessary whitespace
message = json.dumps(body, separators=(',', ':')).encode('utf-8')
computed_signature = hmac.new(signing_key.encode('utf-8'), message, hashlib.sha256).hexdigest()
return signature == computed_signature

if __name__ == '__main__':
sample_body = {
"subscriptionId": "1",
"sequenceNumber": "1",
"description": "Signature Test",
"protocol": "APTOS",
"network": "TESTNET",
"subscriptionType": "WEBHOOK",
"notification": {
"webhookUrl": "YOUR_WEBHOOK_ENDPOINT"
},
"eventType": "EVENT",
"event": {
"eventType": "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f::just::RandomIndexEvent",
"eventAccountAddress": "0x0",
"messages": [
{
"guid": {
"creation_number": "0",
"account_address": "0x0"
},
"sequence_number": "0",
"type": "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f::just::RandomIndexEvent",
"data": {
"price": "44289819"
},
"event_index": 2,
"version": "6627015772"
}
]
},
"createdAt": "2025-02-18T05:52:53.559342615Z"
}
signature = "da5eedb3f1fa386e095dc4f66a8f21155d22964633e0e6f844c331296ef1abaa"
signing_key = "7b8664b96de828e3b3bacf538c51e0ddcfa4fa6c686e738d8c0aeff5c8545ae7"

print("Sample Body:", json.dumps(sample_body, indent=2))
print("Signing Key:", signing_key)

if is_valid_signature(sample_body, signature, signing_key):
print("Signature is valid.")
else:
print("Signature is invalid.")

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

public class SignatureValidation {
/**
* Validate the signature using the JSON body, header signature, and signing key.
*
* @param body JSON string (should be generated similar to JSON.stringify in Node.js)
* @param signature The signature string from the header
* @param signingKey The signing key used for signing
* @return Returns true if the signature is valid, otherwise false
* @throws Exception
*/
public static boolean isValidSignature(String body, String signature, String signingKey) throws Exception {
Mac sha256HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(signingKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256HMAC.init(keySpec);
byte[] hashBytes = sha256HMAC.doFinal(body.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
sb.append(String.format("%02x", b));
}
String computedSignature = sb.toString();
return computedSignature.equals(signature);
}

public static void main(String[] args) {
try {
String sampleBody = "{\"subscriptionId\":\"1\",\"sequenceNumber\":\"1\",\"description\":\"Signature Test\",\"protocol\":\"APTOS\",\"network\":\"TESTNET\",\"subscriptionType\":\"WEBHOOK\",\"notification\":{\"webhookUrl\":\"YOUR_WEBHOOK_ENDPOINT\"},\"eventType\":\"EVENT\",\"event\":{\"eventType\":\"0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f::just::RandomIndexEvent\",\"eventAccountAddress\":\"0x0\",\"messages\":[{\"guid\":{\"creation_number\":\"0\",\"account_address\":\"0x0\"},\"sequence_number\":\"0\",\"type\":\"0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f::just::RandomIndexEvent\",\"data\":{\"price\":\"44289819\"},\"event_index\":2,\"version\":\"6627015772\"}]}," +
"\"createdAt\":\"2025-02-18T05:52:53.559342615Z\"}";
String signature = "da5eedb3f1fa386e095dc4f66a8f21155d22964633e0e6f844c331296ef1abaa";
String signingKey = "7b8664b96de828e3b3bacf538c51e0ddcfa4fa6c686e738d8c0aeff5c8545ae7";

System.out.println("Sample Body: " + sampleBody);
System.out.println("Signing Key: " + signingKey);

if (isValidSignature(sampleBody, signature, signingKey)) {
System.out.println("Signature is valid.");
} else {
System.out.println("Signature is invalid.");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

You can run the examples above from the command line as follows:

//bash
node signatureValidation.js
python signatureValidation.py
//compile
javac SignatureValidation.java

//execute
java SignatureValidation

Store and manage the Signing Key securely

Secure storage of the Signing Key is required for signature verification and thus for message integrity and authentication. Keep the Signing Key safe at all times. Recommendations:

  • Do not hardcode the Signing Key in source code or config files. Store it in a secure external store and inject it at runtime.
  • Use a key management service such as AWS Secrets Manager or Azure Key Vault for storage and access control.
  • Limit access so that only the minimum set of users and applications can read the key.
  • Audit and monitor access and usage of the key, and log signature verification failures so you can respond to suspicious activity.

These practices reduce the risk of key exposure and strengthen Webhook integrity and authentication.

Implement idempotent message handling

When you process Webhook payloads in your business logic, ensure that duplicate deliveries of the same message are handled idempotently. For example, if a Webhook notifies you of an asset transfer and you trigger a user notification, process the event only on the first receipt and ignore subsequent duplicates. You can use a combination of subscriptionId and sequenceNumber as a unique identifier.

⚠️ sequenceNumber is not present in the message. How can I check it?

The sequenceNumber field is currently supported only on some blockchain networks. Support will be expanded to more networks soon; please refer to the Change Log for the latest updates.

Reliability

To ensure reliable delivery, all messages should be delivered even when your endpoint is down or slow. Consider the following strategies.

Best Practices

Track messages lost in transit

Store the latest Sequence Number from received Webhooks in a cache (e.g. Redis) or database and compare with previously received messages to detect gaps or duplicates. When a gap is found, call the Get Webhook History API to retrieve the missing messages.

Recover automatically after endpoint downtime

When your endpoint comes back up (e.g. after maintenance or an outage), use the Get Webhook History API to fetch messages in FAILED status and process them in batch. This helps maintain completeness of data across downtime.