> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mobula.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Get started

> Mobula supports Webhooks for capturing raw blockchain events and curated events such as Swaps and Transfers across EVM, Solana, and SVM chains(i.e. Swaps, Transfers).

<Tip> Webhook functionality is available for **Growth** and **Enterprise** plans only. </Tip>

## 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](/indexing-stream/stream/websocket/multi-events-stream#usage-examples)

```json theme={null}
{
    "name": "MyFirstWebhook",
    "chainIds": ["evm:1", "evm:56"],
    "events": ["swap", "transfer"],
    "apiKey": "xxxxxxxxxx",
    "url": "https://webhook.com/xxxxxxxxxxx",
};
```

### Parameters

| Parameter  | Type      | Description                                                                                                                                                   |
| ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`     | string    | A unique name for your webhook.                                                                                                                               |
| `chainIds` | string\[] | Blockchain identifiers. Supported chains: **EVM**, **Solana**. Example: `["evm:1", "evm:8453", "solana:solana"]`. [More info](/blockchains/intro-blockchains) |
| `events`   | string\[] | Event types to subscribe to. **Currently supported:** `"swap"`, `"transfer"`                                                                                  |
| `filters`  | object    | Optional filters to refine which events are sent `max 1k operation`. See [Filters Documentation](/indexing-stream/stream/filters)                             |
| `url`      | string    | The destination URL where Mobula will POST the event payloads.                                                                                                |
| `apiKey`   | string    | Your API key to authenticate the webhook request.                                                                                                             |

### Create a webhook

First we need to create a webhook:

```typescript theme={null}
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',
  }),
});
```

<Tip> The API key is mandatory to create a webhook. </Tip>

A successful webhook creation returns:

```json theme={null}
{
  "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"
}
```

<Warning> **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.</Warning>

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

```bash theme={null}
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.

```ts theme={null}
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:

```typescript theme={null}
await fetch(url + `/${webhook_id}`, {
  method: 'DELETE',
  headers: {
    'Content-Type': 'application/json',
  },
});
```

If you receive:

```json theme={null}
{
  "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

```typescript theme={null}
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:**

```json theme={null}
{
  "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"
    }
  ]
}
```

This webhook example uses the wrapped SOL SPL mint (`So11111111111111111111111111111111111111112`) because indexed swap events expose on-chain token mints. For swap quote requests, use `So11111111111111111111111111111111111111111` when the user is swapping native SOL from their wallet balance.

### Testing Webhook Signatures

You can test your webhook signature verification with this example:

```typescript theme={null}
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> **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.</Note>

## Usage Examples

Before diving into the examples, make sure to check the data models for both [**EVM and SVM chains**](/indexing-stream/stream/data-model/evm-data-model#transfer-model) for **swaps** and **transfers**.

<Note> **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!</Note>

* 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](/indexing-stream/stream/filters) across swaps and transfers to suit your application needs.

### Sample Swap Filters

<Note>
  The filter logic and data model are the same for **SVM** and **EVM** swaps.\
  Check out the curated swap model: [EVM transfers model](/indexing-stream/stream/data-model/curated-data-model).
</Note>

Here is a basic template for creating a webhook:

```json theme={null}
{
    "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**:

```json theme={null}
{
  "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**:

```json theme={null}
"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**:

```json theme={null}
{
  "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**:

```json theme={null}
"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

<Note>
  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](/indexing-stream/stream/data-model/evm-data-model#transfer-model).
</Note>

Here is a basic template for creating a webhook for **transfers**:

```json theme={null}
{
  "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):

```json theme={null}
{
  "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`**:

```json theme={null}
{
  "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.

```json theme={null}
{
  "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.

<CardGroup>
  <Card title="Support" icon="Telegram" href="https://t.me/mobuladevelopers?start=Mobula_API_Support_Key">
    Telegram
  </Card>

  <Card title="Support" icon="Slack" href="https://join.slack.com/t/mobulaapi/shared_invite/zt-29zrrpjnl-I0tyD73sy7zKy8q~KLL3Ug">
    Slack
  </Card>

  <Card title="Need help?" icon="envelope" href="mailto:contact@mobulalabs.org">
    Email
  </Card>
</CardGroup>
