Skip to main content
Webhook functionality is available for Growth and Enterprise plans only.

Webhook Overview

Mobula provides Webhook support to receive blockchain events in real-time. Currently, you can capture curated events like Swaps and Transfers across EVM, Solana, and SVM chains.

Endpoint Details

  • URL: https://api.mobula.io/api/1/webhook
  • Method: POST
  • Body (JSON): Same as the Webhook example
{
    "name": "MyFirstWebhook",
    "chainIds": ["evm:1", "evm:56"],
    "events": ["swap", "transfer"],
    "apiKey": "xxxxxxxxxx",
    "url": "https://webhook.com/xxxxxxxxxxx",
};

Parameters

ParameterTypeDescription
namestringA unique name for your webhook.
chainIdsstring[]Blockchain identifiers. Supported chains: EVM, Solana. Example: ["evm:1", "evm:8453", "solana:solana"]. More info
eventsstring[]Event types to subscribe to. Currently supported: "swap", "transfer"
filtersobjectOptional filters to refine which events are sent max 1k operation. See Filters Documentation
urlstringThe destination URL where Mobula will POST the event payloads.
apiKeystringYour API key to authenticate the webhook request.

Create a webhook

First we need to create a webhook:
await fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Sample Swap Webhook',
    chainIds: ['solana:solana'],
    events: ['swap'],
    filters: {
      or: [{ eq: ['poolType', 'raydium'] }, { eq: ['poolType', 'raydium-clmm'] }],
    },
    apiKey: 'YOUR API KEY',
    url: 'YOUR SERVER URL',
  }),
});
The API key is mandatory to create a webhook.
A successful webhook creation returns:
{
  "id": "********-****-****-****-************",
  "name": "Sample Swap Webhook",
  "chainIds": [ "solana:solana" ],
  "events": [ "swap" ],
  "filters": {
    "or": [
      [Object ...], [Object ...]
    ]
  },
  "webhookUrl": "YOUR SERVER URL",
  "webhookSecret": "whsec_37694783a9436b75fbf0c4d9d6f8ad4c213fece1c554b999680918670441a434",
  "createdAt": "2025-08-22T05:39:06.438Z",
  "apiKey": "YOUR API KEY"
}
Important: Save the webhookSecret immediately after webhook creation. This secret is only shown once and is required to verify webhook signatures. If you lose it, you’ll need to create a new webhook.
Your webhook is now active and will start sending events to the specified server URL.
If you receive data from your webhook in response, then your webhook is functional and will start receiving events.

List out Registered Webhooks

You can retrieve all registered webhooks using your API key:
curl -X GET "https://api.mobula.io/api/1/webhook?apiKey=YOUR_API_KEY"

Update a Webhook

No need to keep creating new webhooks every time — just update your existing one! Perfect for tracking new wallets, pools, or other events without the extra hassle. Think of it as giving your webhook a little upgrade — keeping it fresh, smart, and in sync with your latest needs. Webhooks support two modes for updating filters:
  • merge: Appends your new filters to the existing ones. Great for adding without losing what you already track.
  • replace: Completely replaces all existing filters with your new configuration. This is the default mode.

Required Parameters for Update

To update a webhook, you need:
  • streamId: The ID of the webhook. If you don’t remember it, you can retrieve it by listing your webhooks.
  • apiKey: The same API key used when creating the webhook.
  • filters: Your updated filter configuration.
  • mode: Optional. Use "merge" to append filters; "replace" (default) to overwrite existing filters.
await fetch(url, {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    streamId: "YOUR_STREAM_ID",
    apiKey: "YOUR_API_KEY",
    mode: "merge", // "merge" or "replace"
    filters: {
      eq: ["swapSenderAddress", "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd"]
    }
  }),
});

Delete a webhook

To delete a webhook, use the webhook ID:
await fetch(url + `/${webhook_id}`, {
  method: 'DELETE',
  headers: {
    'Content-Type': 'application/json',
  },
});
If you receive:
{
  "success": true,
  "message": "Webhook deleted successfully",
  "id": "WEBHOOK_ID"
}

Webhook Signature Verification

Mobula webhooks include cryptographic signatures to ensure the authenticity and integrity of the data you receive. Every webhook request includes two headers:
  • X-Signature: HMAC-SHA256 signature of the request body
  • X-Timestamp: Unix timestamp (seconds) when the request was created

How It Works

  1. Signature Generation: Mobula generates a signature using HMAC-SHA256 with your webhook secret
  2. Signature Format: The signature is sent as sha256=<hex_digest> in the X-Signature header
  3. Verification: You should verify the signature on your server to ensure the request is authentic

Signature Algorithm

The signature is computed as follows:
signature = HMAC-SHA256(secret, payload_body)
X-Signature = "sha256=" + hex(signature)
Where:
  • secret is your webhookSecret (starts with whsec_)
  • payload_body is the raw JSON string of the request body

Example: Verifying Webhook Signatures

Here’s a complete example of how to verify webhook signatures in different languages:

Example

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'whsec_...'; // Your webhook secret

// Use raw body parser for signature verification
app.use(express.raw({ type: 'application/json' }));

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-signature'] as string;
  const timestamp = req.headers['x-timestamp'] as string;

  if (!signature || !timestamp) {
    return res.status(400).send('Missing signature headers');
  }

  // Get raw body as string
  const rawBody = req.body.toString('utf8');

  // Compute expected signature
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');

  // Verify signature
  if (signature !== `sha256=${expectedSignature}`) {
    return res.status(401).send('Invalid signature');
  }

  // Optional: Replay protection (check timestamp is not too old)
  const now = Math.floor(Date.now() / 1000);
  const age = Math.abs(now - Number(timestamp));
  if (age > 300) { // 5 minutes
    return res.status(400).send('Request too old');
  }

  // Parse and process the webhook payload
  const payload = JSON.parse(rawBody);
  console.log('Webhook verified:', payload);

  res.status(200).send('OK');
});

Example Webhook Request

When Mobula sends a webhook, the request looks like this: Headers:
Content-Type: application/json
X-Signature: sha256=3d00cad6bf3aad921e3034b0a5df8bdef189557232a5b5f876dab639f5bc95e6
X-Timestamp: 1705416000
Body:
{
  "streamId": "d628fe5d-f550-4c8b-b1c2-c0a91d9d1cfb",
  "chainId": "solana:solana",
  "data": [
    {
      "type": "swap",
      "hash": "5zCETicUCJqJ5Z3wbfFPZqtSpHPYqnggs1wX7ZRpump",
      "blockNumber": 12345678,
      "protocol": "raydium",
      "tokenIn": "So11111111111111111111111111111111111111112",
      "tokenOut": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "amountIn": "1000000000",
      "amountOut": "500000000",
      "amountUSD": "1250.50"
    }
  ]
}

Testing Webhook Signatures

You can test your webhook signature verification with this example:
import crypto from 'node:crypto';

const WEBHOOK_SECRET = 'YOUR_WEBHOOK_SECRET';
const payload = JSON.stringify({
  streamId: 'test-stream-id',
  chainId: 'solana:solana',
  data: [{ type: 'swap', hash: 'test-hash' }]
});

// Generate signature (what Mobula does)
const signature = crypto
  .createHmac('sha256', WEBHOOK_SECRET)
  .update(payload)
  .digest('hex');

console.log('X-Signature:', `sha256=${signature}`);
console.log('X-Timestamp:', Math.floor(Date.now() / 1000));

// Verify signature (what your server should do)
const receivedSignature = `sha256=${signature}`;
const expectedSignature = crypto
  .createHmac('sha256', WEBHOOK_SECRET)
  .update(payload)
  .digest('hex');

const isValid = receivedSignature === `sha256=${expectedSignature}`;
console.log('Signature valid:', isValid);

Security Best Practices

  1. Always verify signatures: Never process webhook data without verifying the signature
  2. Store secrets securely: Keep your webhookSecret in environment variables, never in code
  3. Implement replay protection: Check the X-Timestamp header to prevent replay attacks (recommended: reject requests older than 5 minutes)
  4. Use HTTPS: Always use HTTPS endpoints for webhook receivers
  5. Handle errors gracefully: Return appropriate HTTP status codes (200 for success, 401 for invalid signature, 400 for missing headers)

Backward Compatibility

Note: Older webhooks created before signature support was added may not have a webhookSecret. These webhooks will be sent without signature headers. If you need signature verification, create a new webhook to receive a secret.

Usage Examples

Before diving into the examples, make sure to check the data models for both EVM and SVM chains for swaps and transfers.
Pro Tip for Devs: Dive into these data models and experiment with filters — your imagination is the only limit! Mix and / or, combine keys, and watch your streams come alive!
  • Explore poolType, poolAddress, transactionFrom, transactionTo, and other keys in the data models.
  • Combine multiple conditions using and / or operators to capture exactly the events you want.
  • Mix and match filters across swaps and transfers to suit your application needs.

Sample Swap Filters

The filter logic and data model are the same for SVM and EVM swaps.
Check out the curated swap model: EVM transfers model.
Here is a basic template for creating a webhook:
{
    "name": "MyFirstWebhook",
    "chainIds": ["solana:solana"],
    "events": ["swap"],
    "apiKey": "YOUR_API_KEY",
    "filters": {},
    "url": "YOUR_SERVER_URL"
}

Filter Swaps by Token Address

This example captures all swaps involving the $SPARK token 5zCETicUCJqJ5Z3wbfFPZqtSpHPYqnggs1wX7ZRpump from all supported pools on Solana:
{
  "filters": {
    "or": [
      { "eq": ["addressToken0", "5zCETicUCJqJ5Z3wbfFPZqtSpHPYqnggs1wX7ZRpump"] },
      { "eq": ["addressToken1", "5zCETicUCJqJ5Z3wbfFPZqtSpHPYqnggs1wX7ZRpump"] }
    ]
  }
}

Filter Swaps by Minimum USD Value

This example captures all large swaps of the $SPARK token worth more than $25 USD:
"filters": {
  "or": [
    {
      "and": [
        { "eq": ["addressToken1", "5zCETicUCJqJ5Z3wbfFPZqtSpHPYqnggs1wX7ZRpump"] },
        { "gte": ["amountUSD", "1000"] }
      ]
    },
    {
      "and": [
        { "eq": ["addressToken0", "5zCETicUCJqJ5Z3wbfFPZqtSpHPYqnggs1wX7ZRpump"] },
        { "gte": ["amountUSD", "1000"] }
      ]
    }
  ]
}

Track Multiple Whales swaps by Single Webhook

This example captures all swaps from specific wallet addresses:
{
  "filters": {
    "or": [
      { "eq": ["swapSenderAddress", "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd"] },
      { "eq": ["swapSenderAddress", "DWvAGkfeHTNd2SWkQh6LsaxEmR3TLn1VuxCZiyb1r98Z"] },
      { "eq": ["swapSenderAddress", "H1gJR25VXi5Ape1gAU7fTWzZFWaCpuP3rzRtKun8Dwo2"] },
      { "eq": ["swapSenderAddress", "7Lp4JBapgNhXoxpJtR2twufh7oaQyqngqJpy7HFJcn7h"] }
    ]
  }
}

Track Multiple Whales by Token or Swap Amount

This example captures swaps from specific wallet addresses and filters by swap amount in USD:
"filters": {
  "or": [
    {
      "and": [
        { "eq": ["swapSenderAddress", "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd"] },
        { "gte": ["amountUSD", "2500"] }
      ]
    },
    {
      "and": [
        { "eq": ["swapSenderAddress", "DWvAGkfeHTNd2SWkQh6LsaxEmR3TLn1VuxCZiyb1r98Z"] },
        { "gte": ["amountUSD", "1000"] }
      ]
    },
    {
      "and": [
        { "eq": ["swapSenderAddress", "H1gJR25VXi5Ape1gAU7fTWzZFWaCpuP3rzRtKun8Dwo2"] },
        { "gte": ["amountUSD", "3000"] }
      ]
    },

  ]
}

Sample Transfer Filters

The filter logic is the same for both SVM and EVM transfers
For EVM transfers, just update the identifiers according to the EVM transfers model.
Here is a basic template for creating a webhook for transfers:
{
  "name": "MyFirstWebhook",
  "chainIds": ["solana:solana"],
  "events": ["transfer"],
  "apiKey": "YOUR_API_KEY",
  "filters": {},
  "url": "YOUR_SERVER_URL"
}

Filter Transfer by Multiple Wallets

This example captures all transfers from multiple wallets, with some conditions on the transfer amount (e.g., amountUSD greater than 200 or 300):
{
  "filters": {
    "or": [
      { "eq": ["transactionFrom", "ASde6y8pBCU1aityWHRpqT7pEAcEonjCgFUMeh5egRes"] },
      { "eq": ["transactionFrom", "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"] },
      { "eq": ["transactionFrom", "5zf7uryG7jXMEnKvSM2WmmHcJu3Q2fXBpUjZkE7XC2Xr"] },
      { "eq": ["transactionFrom", "CSSJFgoeqidqVtHKSNP7i7s6WX8APHfH2kYGdLV195Jb"] },
      {
        "and": [
          { "eq": ["transactionFrom", "H1gJR25VXi5Ape1gAU7fTWzZFWaCpuP3rzRtKun8Dwo2"] },
          { "gte": ["amount", "3000"] }
        ]
      },
      {
        "and": [
          { "eq": ["transactionTo", "5e7u41Ykt3WgnbJaQutMX9b7LZM4qh9Qsd5Y7RTsP5t4"] },
          { "lte": ["amount", "1"] }
        ]
      }
    ]
  }
}

Filter Transfers by Sender

This stream captures all transfers sent to a specific address:
{
  "filters": {
    "eq": ["transactionTo", "ASde6y8pBCU1aityWHRpqT7pEAcEonjCgFUMeh5egRes"]
  }
}

Filter Transfers From Sender and Receiver (End-to-End)

This example demonstrates how to capture all transfers sent from or received by specific addresses, combining multiple conditions in a single webhook.
{
  "filters": {
    "or": [
      {
        "and": [
          { "eq": ["transactionFrom", "2zqLokC98qfedXyXZHeL4sEdFcmmTFizvb1UQeRweWxp"] },
          { "eq": ["transactionTo", "suqh5sHtr8HyJ7q8scBimULPkPpA557prMG47xCHQfK"] }
        ]
      },
      {
        "and": [
          { "eq": ["transactionFrom", "3i51cKbLbaKAqvRJdCUaq9hsnvf9kqCfMujNgFj7nRKt"] },
          { "eq": ["transactionTo", "bangc1iPWdP4b6zNGf4yAsJm21KVH8sic71dFosH8AQ"] }
        ]
      }
    ]
  }
}

Can’t find what you’re looking for? Reach out to us, response times < 1h.