Signature Validation

Webhook Security & Verification

Every webhook event includes a cryptographic signature in the header which you can use to verify the authenticity of the event.

Headers

HeaderDescription
x-mural-webhook-signaturebase64 encoded DER encoded ECDSA signature, to be used for authenticity verification
x-mural-webhook-signature-versionversion of the signature algorithm
x-mural-webhook-timestampISO 8601 timestamp of when the event was sent from Mural's system

Verification Walkthrough

Let's walk through a complete example of receiving and verifying a webhook event from Mural.

Step 1: Receive the Webhook Request

When an account is credited, your endpoint will receive a POST request with headers and a JSON body.

Headers received:

{
  "accept": "application/json, text/plain, */*",
  "content-type": "application/json",
  "x-mural-webhook-signature": "MEUCIFvw3RTZegE/F7WFkw3Um4pUYG0mf2PMvNsP4/Kje34SAiEAisCvaUgkWPOAcXBmTTRqREJ8lZvQ3frcFZYAkycjG84=",
  "x-mural-webhook-signature-version": "v0",
  "x-mural-webhook-timestamp": "2025-08-29T05:52:30.411Z",
  "user-agent": "axios/1.10.0",
  "content-length": "884",
  "accept-encoding": "gzip, compress, deflate, br",
  "host": "localhost:8081",
  "connection": "keep-alive"
}

Request body received:

{
  "eventId": "42ddfeb3-98b4-4b7f-b64b-d5032e8967e7",
  "deliveryId": "a242d45d-016a-402b-8d8f-eb335e7de308",
  "transactionId": "0x00355026c9adeb213059e97cd2096c7bc451f36e9350da538674f56dcdea78eb:log:2",
  "attemptNumber": 0,
  "eventCategory": "MURAL_ACCOUNT_BALANCE_ACTIVITY",
  "occurredAt": "2025-08-29T05:52:11.101Z",
  "payload": {
    "type": "account_credited",
    "accountId": "6eb39033-fc57-443e-a420-a64ad576b43d",
    "transactionId": "0xd8364d0f2c0d3035fb244cf43a17370bfc09b77e519e0a9cd1fa1417674377fd:log:1",
    "tokenAmount": {
      "blockchain": "POLYGON",
      "tokenAmount": 11,
      "tokenSymbol": "USDC",
      "tokenContractAddress": "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582"
    },
    "organizationId": "ff836239-bb82-41db-9968-b6a84752c51d",
    "transactionDetails": {
      "blockchain": "POLYGON",
      "transactionDate": "2025-08-29T05:51:57.000Z",
      "transactionHash": "0xa305f25f94cc877e01d155b0d18a4b667006168354730d381fd51832d8fd7583",
      "sourceWalletAddress": "0xcbf5950b3aaf13bcb7d7d65754d410c49739e0e1",
      "destinationWalletAddress": "0xcb2c65cab3b55dd775489b72168eed28cb221c66"
    },
    "accountWalletAddress": "0xCB2C65caB3b55DD775489b72168eEd28cB221C66"
  }
}

Step 2: Extract Verification Components

From the headers, extract:

  • x-mural-webhook-signature: The signature to verify
  • x-mural-webhook-timestamp: The timestamp when the webhook was sent

Step 3: Construct the Message to Verify

Concatenate the timestamp with the raw request body, separated by a period (.):

2025-08-29T05:52:30.411Z.{"eventId":"42ddfeb3-98b4-4b7f-b64b-d5032e8967e7","deliveryId":"a242d45d-016a-402b-8d8f-eb335e7de308","transactionId":"0x00355026c9adeb213059e97cd2096c7bc451f36e9350da538674f56dcdea78eb:log:2","attemptNumber":0,"eventCategory":"MURAL_ACCOUNT_BALANCE_ACTIVITY","occurredAt":"2025-08-29T05:52:11.101Z","payload":{"type":"account_credited","accountId":"6eb39033-fc57-443e-a420-a64ad576b43d","transactionId":"0xd8364d0f2c0d3035fb244cf43a17370bfc09b77e519e0a9cd1fa1417674377fd:log:1","tokenAmount":{"blockchain":"POLYGON","tokenAmount":11,"tokenSymbol":"USDC","tokenContractAddress":"0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582"},"organizationId":"ff836239-bb82-41db-9968-b6a84752c51d","transactionDetails":{"blockchain":"POLYGON","transactionDate":"2025-08-29T05:51:57.000Z","transactionHash":"0xa305f25f94cc877e01d155b0d18a4b667006168354730d381fd51832d8fd7583","sourceWalletAddress":"0xcbf5950b3aaf13bcb7d7d65754d410c49739e0e1","destinationWalletAddress":"0xcb2c65cab3b55dd775489b72168eed28cb221c66"},"accountWalletAddress":"0xCB2C65caB3b55DD775489b72168eEd28cB221C66"}}

Step 4: Verify the Signature

Using the public key provided when you created the webhook:

  1. Hash the concatenated message using SHA256
  2. Verify the x-mural-webhook-signature is a valid ECDSA signature of that hash. See the following example for how you might do this in TypeScript.

Verification Example (TypeScript)

import crypto from 'crypto';

function verifyMuralWebhook(
  requestBody: string,
  signature: string,
  timestamp: string,
  publicKey: string
): boolean {
  // Prevent replay attacks (5-minute window)
  const currentTime = new Date();
  const webhookTimestamp = new Date(timestamp);

  // Construct the message that was signed
  const messageToSign = `${webhookTimestamp.toISOString()}.${requestBody}`;

  // Decode the base64 signature
  const signatureBuffer = Buffer.from(signature, 'base64');

  // Verify the ECDSA signature using the public key
  try {
    const isValid = crypto.verify(
      'sha256',
      Buffer.from(messageToSign),
      {
        key: publicKey,
        dsaEncoding: 'der',
      },
      signatureBuffer
    );
    return isValid;
  } catch (error) {
    console.error('Signature verification failed:', error);
    return false;
  }
}

// Express.js middleware example
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-mural-webhook-signature'] as string;
  const timestamp = req.headers['x-mural-webhook-timestamp'] as string;
  const rawBody = JSON.stringify(req.body);

  // Public key provided by Mural (retrieve from your webhook configuration)
  const muralPublicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEimJ3SJgR23Gq1YMnlfHAxIM8OTQq
gyYCMS8keFJNXxEUhVVORpXAvXhScRltsHHprxjBZ2IpxZO28u+ljhZ3Vw==
-----END PUBLIC KEY-----`;

  if (!verifyMuralWebhook(rawBody, signature, timestamp, muralPublicKey)) {
    return res.status(401).send('Invalid signature');
  }

  // Process verified webhook
  console.log('Verified webhook:', req.body);
  res.status(200).send('OK');
});