Skip to main content

Flexible Webhook Security & Reliability


A Webhook endpoint is a public URL accessible from the internet. Because anyone can send requests to it without authentication, it is exposed to threats such as message tampering and replay attacks. This document covers the security threats to consider when operating an endpoint, best practices to mitigate them, and reliability strategies to prevent message loss.


Security Threats

Replay Attacks

An attacker captures a legitimate request and replays it multiple times to trigger duplicate processing. For example, if the same Webhook message is delivered repeatedly, a payment request could be processed multiple times or data consistency could be broken. To prevent this, implement your business logic to be idempotent.

Man-in-the-Middle (MITM) Attacks

An attacker intercepts communication between the client and server to eavesdrop on data or tamper with the payload. Protect the communication channel with HTTPS/TLS, and verify signatures to authenticate the message's integrity and origin.

Best Practices

Apply the following guidelines to minimize threats such as replay and MITM attacks. Each item is optional and can be applied selectively.

Verifying Message Integrity Using Signatures

The Nodit server includes a signature value in the x-signature header of every Webhook message. Verifying the signature with the Signing Key at the endpoint server confirms the integrity and origin of the message.

What Is a Signature?

A signature is a message authentication code (MAC) generated by hashing the original data using an algorithm such as HMAC-SHA256 and then applying a pre-shared secret key. If the key is shared between sender and receiver, integrity is verified as follows:

  1. The sender transmits the original data along with the signature.
  2. The receiver decrypts the signature using the secret key to obtain the Digest. Successful decryption authenticates the sender.
  3. The receiver independently computes a Hash from the received data to produce a Digest.
  4. If the Digest from step 2 matches the Digest from step 3, the data has not been tampered with.

Retrieving the Signing Key

A Signing Key is automatically generated when a Webhook is created. The same key is used for all messages within the same Webhook.

Viewing the Signing Key in the Console

Click the [Signing Key] button in the Webhook menu to view it.



Verifying Nodit Webhook Message Signatures

When a Webhook message is received, extract the signature value from the x-signature header and validate it using the Signing Key. The following are example implementations by language.

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 x-signature header
* @param {string} signingKey - The signing key retrieved from Nodit console
* @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');
}

// --- Usage Example ---

// Flexible Webhook payload format
const sampleBody = {
"subscriptionId": "1234567890123456789",
"sequenceNumber": 1,
"streamId": "77",
"streamSlug": "ethereum.mainnet.erc20.transfer",
"data": {
"block_number": 21000000,
"block_timestamp": 1730000000,
"from_address": "0x1111111111111111111111111111111111111111",
"log_index": 42,
"to_address": "0x2222222222222222222222222222222222222222",
"token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"transaction_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"transaction_index": 15,
"value": "1000000000000"
}
};

// Replace with actual values from x-signature header and Nodit console
const signature = "YOUR_X_SIGNATURE_HEADER_VALUE";
const signingKey = "YOUR_SIGNING_KEY";

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 x-signature header
:param signing_key: The signing key retrieved from Nodit console
:return: Returns True if the signature is valid, otherwise False
"""
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__':
# Flexible Webhook payload format
sample_body = {
"subscriptionId": "1234567890123456789",
"sequenceNumber": 1,
"streamId": "77",
"streamSlug": "ethereum.mainnet.erc20.transfer",
"data": {
"block_number": 21000000,
"block_timestamp": 1730000000,
"from_address": "0x1111111111111111111111111111111111111111",
"log_index": 42,
"to_address": "0x2222222222222222222222222222222222222222",
"token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"transaction_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"transaction_index": 15,
"value": "1000000000000"
}
}

# Replace with actual values from x-signature header and Nodit console
signature = "YOUR_X_SIGNATURE_HEADER_VALUE"
signing_key = "YOUR_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 of the request body
* @param signature The signature string from the x-signature header
* @param signingKey The signing key retrieved from Nodit console
* @return Returns true if the signature is valid, otherwise false
*/
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));
}
return sb.toString().equals(signature);
}

public static void main(String[] args) {
try {
// Flexible Webhook payload format
String sampleBody = "{\"subscriptionId\":\"1234567890123456789\",\"sequenceNumber\":1,\"streamId\":\"77\",\"streamSlug\":\"ethereum.mainnet.erc20.transfer\",\"data\":{\"block_number\":21000000,\"block_timestamp\":1730000000,\"from_address\":\"0x1111111111111111111111111111111111111111\",\"log_index\":42,\"to_address\":\"0x2222222222222222222222222222222222222222\",\"token_address\":\"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\",\"transaction_hash\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transaction_index\":15,\"value\":\"1000000000000\"}}";

// Replace with actual values from x-signature header and Nodit console
String signature = "YOUR_X_SIGNATURE_HEADER_VALUE";
String signingKey = "YOUR_SIGNING_KEY";

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

Save each code to a file and run it with the following commands.

node signatureValidation.js
python signatureValidation.py
javac SignatureValidation.java && java SignatureValidation

Storing the Signing Key Securely

The prerequisite for signature verification is keeping the Signing Key secure.

  • No plaintext hardcoding: Do not include the key directly in source code or configuration files. Store it in an external system and inject it at runtime.
  • Use a secrets management service: Store it in AWS Secrets Manager, Azure Key Vault, or a similar service, and enforce strict access controls.
  • Minimize access privileges: Limit the users and applications that can access the Signing Key to the minimum necessary.
  • Monitor access logs: Log all access and set up alerts to detect anomalous activity such as signature verification failures.

Implementing Idempotent Message Handling

Implement your system to guarantee idempotency even if the same Webhook message is delivered more than once. For example, if a user notification is triggered by an asset transfer event, only the first occurrence should be processed — subsequent duplicate messages should be ignored.

Use the following field combinations as the Idempotency Key for each stream type.

Stream Type

Idempotency Key

Block

`block_number`

ERC-20 Transfer

`block_number` + `transaction_hash` + `log_index`

ERC-721 Transfer

`block_number` + `transaction_hash` + `log_index`

ERC-1155 Transfer

`block_number` + `transaction_hash` + `log_index` + `batch_index`

Log

`block_number` + `transaction_hash` + `log_index`

Transaction Receipt

`block_number` + `hash`

Transaction Stream (Aptos Only)

`transaction_version`

Store Idempotency Keys in a cache such as Redis or a database to identify duplicate messages.


Reliability

Nodit Flexible Webhook ensures messages are not lost even during endpoint downtime or processing delays.

Webhook Retry Policy

Nodit Webhook automatically retries up to 1 time. If a 200 OK response is not received after calling the webhookUrl, it retries 15 seconds later. Messages that fail after retry are visible in the Console delivery history with a FAILED status.

Manual Resend with Easy Resend

Messages that fail automatic retry can be manually resent from the Console.

  1. View the delivery history on the Webhook detail page.
  2. Select the FAILED message.
  3. Click the Resend button to resend.
  4. Check the resend result immediately.

Retry records are not logged in the history.


Best Practices

Monitor Endpoint Availability

If the endpoint experiences downtime, messages may be lost even after the automatic retry (1 attempt).

  • Operate a health check endpoint to monitor listener server availability.
  • Integrate an alerting system (PagerDuty, Slack, etc.) for immediate response to downtime.

Track Failed Messages in the Console

Flexible Webhook does not support the Get Webhook History API. Check failed messages in the Console.

  • Review FAILED messages on a regular basis.
  • Analyze failure causes (response codes, timeouts, etc.) to prevent recurrence.
  • Use Easy Resend to manually resend messages as needed.

Guarantee Idempotency (Prevent Duplicates on Resend)

Automatic retries or Easy Resend may deliver the same message multiple times. Use the Idempotency Keys described in the idempotent message handling section to prevent duplicate processing.