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
| Header | Description |
|---|---|
x-mural-webhook-signature | base64 encoded DER encoded ECDSA signature, to be used for authenticity verification |
x-mural-webhook-signature-version | version of the signature algorithm |
x-mural-webhook-timestamp | ISO 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 verifyx-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:
- Hash the concatenated message using SHA256
- Verify the
x-mural-webhook-signatureis 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');
});Updated about 2 months ago