diff --git a/config/default.js b/config/default.js
index db2999dec..980939bdb 100644
--- a/config/default.js
+++ b/config/default.js
@@ -737,4 +737,11 @@ module.exports = {
   },
   TIMER_CHANGE_PROPOSER_SECOND: Number(process.env.TIMER_CHANGE_PROPOSER_SECOND) || 30,
   MAX_ROTATE_TIMES: Number(process.env.MAX_ROTATE_TIMES) || 2,
+  WEBHOOK: {
+    CALL_WEBHOOK_ON_CONFIRMATION: process.env.CALL_WEBHOOK_ON_CONFIRMATION,
+    CALL_WEBHOOK_MAX_RETRIES: process.env.CALL_WEBHOOK_MAX_RETRIES || 3,
+    CALL_WEBHOOK_INITIAL_BACKOFF: process.env.CALL_WEBHOOK_INITIAL_BACKOFF || 15000,
+    WEBHOOK_PATH: process.env.WEBHOOK_PATH, // For posting optional layer 2 transaction finalization details
+    WEBHOOK_SIGNING_KEY: process.env.WEBHOOK_SIGNING_KEY,
+  },
 };
diff --git a/nightfall-client/src/event-handlers/block-proposed.mjs b/nightfall-client/src/event-handlers/block-proposed.mjs
index 60a662184..c88d60d53 100644
--- a/nightfall-client/src/event-handlers/block-proposed.mjs
+++ b/nightfall-client/src/event-handlers/block-proposed.mjs
@@ -29,8 +29,14 @@ import {
 } from '../services/database.mjs';
 import { decryptCommitment } from '../services/commitment-sync.mjs';
 import { syncState } from '../services/state-sync.mjs';
+import DataPublisher from '../utils/dataPublisher.mjs';
 
-const { TIMBER_HEIGHT, HASH_TYPE, TXHASH_TREE_HASH_TYPE } = config;
+const {
+  TIMBER_HEIGHT,
+  HASH_TYPE,
+  TXHASH_TREE_HASH_TYPE,
+  WEBHOOK: { CALL_WEBHOOK_ON_CONFIRMATION, WEBHOOK_PATH, WEBHOOK_SIGNING_KEY },
+} = config;
 const { ZERO, WITHDRAW } = constants;
 
 const { generalise } = gen;
@@ -242,6 +248,31 @@ async function blockProposedEventHandler(data, syncing) {
       }),
     );
   }
+
+  // Send finalization details via webhook optionally, if CALL_WEBHOOK_ON_CONFIRMATION
+  // is enabled
+  if (CALL_WEBHOOK_ON_CONFIRMATION) {
+    if (!WEBHOOK_PATH) {
+      throw new Error('WEBHOOK_PATH is not set');
+    }
+    const dataToPublish = {
+      proposer: block.proposer,
+      blockNumberL2: block.blockNumberL2,
+      transactionHashes: block.transactionHashes,
+    };
+    logger.info({ msg: 'Calling webhook', url: WEBHOOK_PATH, data: dataToPublish });
+    try {
+      await new DataPublisher([
+        {
+          type: 'webhook',
+          url: `${WEBHOOK_PATH}`,
+          signingKey: WEBHOOK_SIGNING_KEY,
+        },
+      ]).publish(dataToPublish);
+    } catch (err) {
+      logger.error(`ERROR: Calling webhook ${JSON.stringify(err)}`);
+    }
+  }
 }
 
 export default blockProposedEventHandler;
diff --git a/nightfall-client/src/utils/dataPublisher.mjs b/nightfall-client/src/utils/dataPublisher.mjs
new file mode 100644
index 000000000..ada6033ae
--- /dev/null
+++ b/nightfall-client/src/utils/dataPublisher.mjs
@@ -0,0 +1,170 @@
+/* eslint-disable max-classes-per-file */
+import axios from 'axios';
+import crypto from 'crypto';
+import config from 'config';
+
+const {
+  WEBHOOK: { CALL_WEBHOOK_MAX_RETRIES, CALL_WEBHOOK_INITIAL_BACKOFF },
+} = config;
+
+/**
+ * Class for signing request payloads.
+ */
+class PayloadSigner {
+  /**
+   * @param {string} signingKey - The signing key.
+   */
+  constructor(signingkey) {
+    this.signingKey = signingkey;
+  }
+
+  /**
+   * Signs a payload.
+   * @param {*} data - The data to be signed.
+   * @returns {string} The signature.
+   * @throws {Error} If the data is not an object or is null.
+   */
+  sign(data) {
+    if (typeof data !== 'object' || data === null) {
+      throw new Error('Data must be an object');
+    }
+
+    const hmac = crypto.createHmac('sha256', this.signingKey);
+    hmac.update(JSON.stringify(data));
+    return hmac.digest('hex');
+  }
+}
+
+/**
+ * Class for handling retries with exponential backoff and jitter.
+ */
+class RetryHandler {
+  /**
+   * @param {number} maxRetries - The maximum number of retries.
+   * @param {number} initialBackoff - The initial backoff time in milliseconds.
+   */
+  constructor(maxRetries, initialBackoff) {
+    this.maxRetries = maxRetries;
+    this.initialBackoff = initialBackoff;
+  }
+
+  /**
+   * Executes a request function with retries.
+   * @param {Function} requestFunction - The function to execute with retries.
+   * @returns {*} The result of the request function.
+   * @throws {Error} If the request function fails after all retries.
+   */
+  async executeWithRetry(requestFunction, retries = 0) {
+    let timeIdToClear;
+    if (retries >= this.maxRetries) {
+      throw new Error('Failed to execute request after retries');
+    }
+
+    try {
+      return await requestFunction();
+    } catch (error) {
+      // Passing an invalid ID to clearTimeout() silently does nothing; no exception is thrown.
+      clearTimeout(timeIdToClear);
+      // Have an exponential back-off strategy with jitter
+      const newBackoff = this.initialBackoff * 2 ** retries + Math.random() * this.initialBackoff;
+      timeIdToClear = await new Promise(resolve => setTimeout(resolve, newBackoff));
+      return this.executeWithRetry(requestFunction, retries + 1);
+    }
+  }
+}
+
+/**
+ * Class for publishing data to webhook destinations.
+ */
+class WebhookPublisher {
+  /**
+   * @param {PayloadSigner} signer - The payload signer instance.
+   * @param {RetryHandler} retryHandler - The retry handler instance.
+   */
+  constructor(signer, retryHandler) {
+    this.signer = signer;
+    this.retryHandler = retryHandler;
+  }
+
+  /**
+   * Validates the destination configuration
+   * @param {Object} destination - the webhook destination configuration
+   * @throws {Error} If the destination configuration is invalid
+   */
+  static validateDestination(destination) {
+    if (!destination || typeof destination !== 'object') {
+      throw new Error('Invalid destination configuration: must be an object');
+    }
+
+    if (!destination.url || typeof destination.url !== 'string') {
+      throw new Error('Invalid destination configuration: url must be a string');
+    }
+  }
+
+  /**
+   * Publishes data to a webhook destination.
+   * @param {Object} destination - The webhook destination configuration.
+   * @param {*} data - The data to be published.
+   */
+  async publish(destination, data) {
+    WebhookPublisher.validateDestination(destination);
+    const headers = {};
+
+    if (destination.signingKey) {
+      const signature = this.signer.sign(data);
+      headers['X-Signature-SHA256'] = signature;
+    }
+
+    const requestFunction = async () => {
+      const response = await axios.post(destination.url, data, { headers });
+      if (response.status < 200 || response.status >= 300) {
+        throw new Error(`Webhook failed with status: ${response.status}`);
+      }
+    };
+
+    await this.retryHandler.executeWithRetry(requestFunction);
+  }
+}
+
+/**
+ * Class for publishing data to various destinations.
+ */
+export default class DataPublisher {
+  /**
+   * @param {Array} options - An array of destination configurations.
+   */
+  constructor(options) {
+    this.options = options || [];
+  }
+
+  /**
+   * Publishes data to all configured destinations/transports.
+   * @param {*} data - The data to be published.
+   */
+  async publish(data) {
+    const promises = this.options.map(async destination => {
+      try {
+        const signer = new PayloadSigner(destination.signingKey);
+        const retryHandler = new RetryHandler(
+          destination.maxRetries || CALL_WEBHOOK_MAX_RETRIES,
+          destination.initialBackoff || CALL_WEBHOOK_INITIAL_BACKOFF,
+        );
+
+        switch (destination.type) {
+          case 'webhook': {
+            const publisher = new WebhookPublisher(signer, retryHandler);
+            await publisher.publish(destination, data);
+            break;
+          }
+          // Other destination types can be added here as needed
+          default:
+            console.error('Unknown destination type:', destination.type);
+        }
+      } catch (err) {
+        console.error('Unable to publish to destination: ', err);
+      }
+    });
+
+    return Promise.all(promises);
+  }
+}