GuidesAPI Reference
Log In
Guides

Webhooks

Overview

Webhooks enable real-time notifications for critical events in your organization's payment operations. Receive instant updates when accounts are credited or debited, ensuring your systems stay synchronized with Mural account balance updates.

Creating Webhooks

Configure webhooks through the Mural API to receive HTTP POST requests when specific events occur:

  1. Create the Webhook, which will create the Webhook in a DISABLED state. This will return a public key in PEM format that you can use to verify webhook event signatures coming from Mural.
  2. Enable the Webhook by making a call to update the webhook with the status set to ACTIVE. This means that Mural will begin sending events to the registered Webhook.
  3. Implement your endpoint and implement your webhook signature validation (see below for more on this)
  4. Send a test event by making a call to the send event endpoint to test your integration.

Supported Events

The request body for every event sent by Mural will look like the following, where the eventCategory and corresponding payload types are defined by each event:

interface WebhookEventRequestBody {
  eventId: string;
  deliveryId: string;
  attemptNumber: number;
  eventCategory: MuralEventCategory;
  occurredAt: string; // ISO 8601 datetime
  payload: Payload;
}

Account Balance Activity

CategoryPayload TypeDescriptionTrigger
mural_account_balance_activityaccount_creditedFunds received in a Mural accountIncoming blockchain transaction confirmed
mural_account_balance_activityaccount_debitedFunds sent from a Mural accountOutgoing blockchain transaction confirmed

Account Credited and Debited Payloads:

interface AccountCreditedPayload {
  type: 'account_credited';
  accountId: string;
  organizationId: string;
  transactionId: string; // this is the id that will appear in the Transactions API
  accountWalletAddress: string;
  tokenAmount: {
    blockchain: string;
    tokenAmount: number;
    tokenSymbol: string;
    tokenContractAddress: string;
  };
  transactionDetails: {
    blockchain: string;
    transactionDate: string; // ISO 8601 datetime
    transactionHash: string;
    sourceWalletAddress: string;
    destinationWalletAddress: string;
  };
}
interface AccountDebitedPayload {
  type: 'account_debited';
  accountId: string;
  organizationId: string;
  transactionId: string; // this is the id that will appear in the Transactions API
  accountWalletAddress: string;
  tokenAmount: {
    blockchain: string;
    tokenAmount: number;
    tokenSymbol: string;
    tokenContractAddress: string;
  };
  transactionDetails: {
    blockchain: string;
    transactionDate: string; // ISO 8601 datetime
    transactionHash: string;
    sourceWalletAddress: string;
    destinationWalletAddress: string;
  };
}

Example Webhook Request Body and Headers

Raw request body:

{
  "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"
  }
}

Headers:

{
  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'
}

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-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

To verify the signature:

  1. concatenate the x-mural-webhook-timestamp value with the request body, joining with a .See below for an example using the event body from above:
    1. 2025-08-28T20:52:00.295Z.{"eventId":"42ddfeb3-98b4-4b7f-b64b-d5032e8967e7","deliveryId":"a242d45d-016a-402b-8d8f-eb335e7de308","attemptNumber":0,"eventCategory":"MURAL_ACCOUNT_BALANCE_ACTIVITY","occurredAt":"2025-08-29T05:52:11.101Z","payload":{"type":"account_credited","accountId":"6eb39033-fc57-443e-a420-a64ad576b43d","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"}}
  2. Using the public key associated with the Webhook (returned by the create webhook endpoint), verify the x-mural-signature is authentic.
    1. hash the concatenated message from (1)(i) using SHA256
    2. Verify that the x-mural-webhook-signature is a valid ECDSA signature of that hash, produced with Mural’s private key.

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-signature'] as string;
  const timestamp = req.headers['x-mural-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');
});

Delivery Guarantees

  • Ordering: Events are delivered in the order they occur per Webhook
  • Retries: Failed deliveries (non 200 http responses) are retried with exponential backoff for up to 5 minutes