> For the complete documentation index, see [llms.txt](https://docs.projectx.mx/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.projectx.mx/products/kepler/webhooks.md).

# Webhooks

### Event Filtering Options

When configuring a webhook, you can filter which events trigger a notification. You can use a combination of three criteria.

> ℹ️ **Note** If multiple filters are set, **all conditions must match (AND logic)** for the webhook to be triggered. If you set both an `address` and an `identifier`, only events matching *both* will trigger the webhook.

| Filter       | Description                                                                 | Example            |
| ------------ | --------------------------------------------------------------------------- | ------------------ |
| `address`    | The smart contract address that emitted the event. Must be an exact match.  | `erd1qqqq...`      |
| `identifier` | The event identifier/name. Must be an exact match.                          | `transfer`, `swap` |
| `topic`      | The first topic of the event (decoded from base64). Must be an exact match. | `claimed`          |

#### Configuration Rules

* **Requirement:** At least one filter (`address`, `identifier`, or `topic`) is mandatory.
* **Decoding:** Topics in MultiversX events are base64-encoded. The filter matches against the **decoded** value of the first topic.

#### Common Examples

| Configuration                                                                     | Result                                                       |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `address: erd1qqq...abc`                                                          | Receives **all** events from this specific address.          |
| `identifier: transfer`                                                            | Receives **all** `transfer` events from **any** address.     |
| <p><code>address: erd1qqq...abc</code><br><code>identifier: distribute</code></p> | Receives only `distribute` events from the specific address. |

***

### Webhook Delivery

When an event matches your filters, Kepler sends an HTTP `POST` request to your configured URL.

#### The Payload

The request body is a JSON object containing the event details and a timestamp.

```json
{
  "event": {
    "address": "erd1qqqqqqqqqqqqqpgqjhn0rrta3hceyguqlmkqgklxc0eh0r5rl3tsv6a9k0",
    "identifier": "distribute",
    "topics": [
      "ZGlzdHJpYnV0ZWQ=",
      "AAAAAAAAAAAFAPWuOkANricr0lRon9WkT4jj8pSeV4c=",
      "Q09MUy05ZDkxYjc=",
      "iscjBInoAAA="
    ],
    "data": "",
    "txHash": "2151f16aa3a491d5979d16d6e6ad128da9851d3d65bf63cf226d8083432a201b"
  },
  "timestamp": 1706540400,
  "webhook": "550e8400-e29b-41d4-a716-446655440000"
}
```

#### Markdown

| **Field**          | **Type**   | **Description**                                    |
| ------------------ | ---------- | -------------------------------------------------- |
| `event.address`    | `string`   | The smart contract address that emitted the event. |
| `event.identifier` | `string`   | The event identifier/name.                         |
| `event.topics`     | `string[]` | Array of base64-encoded topics.                    |
| `event.data`       | `string`   | Base64-encoded event data (may be empty).          |
| `event.txHash`     | `string`   | The transaction hash that triggered the event.     |
| `timestamp`        | `number`   | Unix timestamp (seconds) of webhook dispatch.      |
| `webhook`          | `string`   | Your webhook's unique identifier.                  |

#### Headers

| **Header**     | **Value**                   | **Description**                                          |
| -------------- | --------------------------- | -------------------------------------------------------- |
| `Content-Type` | `application/json`          | The media type of the body.                              |
| `X-Signature`  | Hex-string                  | Important: The cryptographic signature for verification. |
| `User-Agent`   | `ProjectX-Kepler/{version}` | Identifies the sender.                                   |

***

### Delivery Lifecycle

#### Expected Response

Your endpoint must return a status code in the 2xx range (e.g., `200 OK`) to acknowledge receipt. The response body is ignored.

> ⚠️ Warning
>
> Your server must respond within 10 seconds. If the request times out, it is treated as a failure and will be retried.

#### Retry Mechanism

If delivery fails (network error, timeout, or non-2xx status), Kepler retries automatically.

* Max Retries: 3 attempts (4 total requests).
* Strategy: Progressive backoff (5s, 10s, 15s).

Example Timeline:

1. `T+0s`: Initial attempt (Fails)
2. `T+5s`: Retry 1 (Fails)
3. `T+15s`: Retry 2 (Fails)
4. `T+30s`: Final Retry

***

### Security & Verification

To ensure requests originate from Kepler and haven't been tampered with, you must verify the `X-Signature` header.

#### Verification Logic

1. Serialize: Read the raw request body as a string.
2. Verify: Use the MultiversX message signing algorithm (Ed25519) to verify the body against the signature.
3. Public Key: Use the official Kepler signing address below.

Kepler Public Signing Address:

Plaintext

```
erd1c0mj9ctdg0xv872j4v3rs8ygcz2tdx7mtxffawj9trxfkvwg0njq7uzp58
```

#### Code Examples

**TypeScript / JavaScript**

```typescript
import { Address, Message, MessageComputer } from '@multiversx/sdk-core';

const SIGNING_ADDRESS = 'erd1c0mj9ctdg0xv872j4v3rs8ygcz2tdx7mtxffawj9trxfkvwg0njq7uzp58';

function verifyWebhookSignature(payload: string, signature: string): boolean {
  const messageComputer = new MessageComputer();
  
  const message = new Message({
    data: new Uint8Array(Buffer.from(payload)),
    address: Address.fromBech32(SIGNING_ADDRESS),
    signature: Buffer.from(signature, 'hex'),
  });

  return messageComputer.verifyMessage(message);
}

// Express.js usage
app.post('/webhook', express.text({ type: 'application/json' }), (req, res) => {
  const payload = req.body; // Ensure this is the RAW string body
  const signature = req.headers['x-signature'] as string;

  if (!verifyWebhookSignature(payload, signature)) {
    return res.status(401).send('Invalid signature');
  }

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

**Python**

```python
from multiversx_sdk import Address, MessageComputer, Message

SIGNING_ADDRESS = 'erd1c0mj9ctdg0xv872j4v3rs8ygcz2tdx7mtxffawj9trxfkvwg0njq7uzp58'

def verify_webhook_signature(payload: str, signature: str) -> bool:
    message_computer = MessageComputer()
    
    message = Message(
        data=payload.encode('utf-8'),
        address=Address.new_from_bech32(SIGNING_ADDRESS),
        signature=bytes.fromhex(signature)
    )
    
    return message_computer.verify_message(message)
```

***

### Best Practices

* Idempotency: In rare network conditions, you may receive the same webhook twice. Always use the `event.txHash` as a unique key to prevent processing the same event multiple times.
* Async Processing: Return a `200 OK` immediately, then process the data asynchronously (e.g., send it to a queue). This prevents timeouts.
* Security: Always use HTTPS. Validate the `timestamp` field (e.g., ensure it is within the last 5 minutes) to prevent replay attacks.

***

## Quick Start: Receive Your First Webhook

Follow this guide to set up a local webhook receiver, expose it to the internet, and trigger a test event from Kepler.

***

### Prerequisites

* A Kepler account.
* Node.js installed on your machine.
* A tool to tunnel local ports to the internet (e.g., [ngrok](https://ngrok.com/)).

***

### Step 1: Create a Simple Receiver

Create a file named `server.js` and paste the following code. This sets up a basic Express server that logs incoming webhooks to your console.

```javascript
const express = require('express');
const app = express();
const port = 3000;

// Middleware to parse JSON bodies
app.use(express.json());

app.post('/webhook', (req, res) => {
  // 1. Log the incoming event
  console.log('🔔 Webhook Received!');
  console.log('Event:', req.body.event.identifier);
  console.log('TxHash:', req.body.event.txHash);

  // 2. Respond quickly with 200 OK
  res.status(200).send('Received');
});

app.listen(port, () => {
  console.log(`🚀 Receiver running on http://localhost:${port}`);
});
```

Install dependencies and start the server:

Bash

```
npm install express
node server.js
```

***

### Step 2: Expose Your Server

Since Kepler cannot reach your `localhost` directly, use a tunneling service to create a public URL.

Bash

```bash
# If using ngrok
ngrok http 3000
```

Copy the HTTPS URL generated by ngrok (e.g., `https://a1b2-c3d4.ngrok-free.app`). Your full webhook URL will be: `https://a1b2-c3d4.ngrok-free.app/webhook`

***

### Step 3: Configure Kepler

1. Log in to your Kepler dashboard and navigate to Webhooks.
2. Click Create Webhook.
3. URL: Paste your ngrok URL (ensure it ends with `/webhook`).
4. Filters: Set up a filter to reduce noise. For testing, you might want to track a common token transfer.
   * Identifier: `transfer`
   * *(Leave Address and Topic empty to catch all transfers for now)*

> ℹ️ Tip Start with broad filters (like just an `identifier`) to ensure your connection works, then narrow them down with specific `address` or `topic` filters later.

***

### Step 4: Test and Verify

1. Trigger an action on the blockchain (or wait for a live event that matches your filter).
2. Check your terminal running `server.js`. You should see:

Plaintext

```
🔔 Webhook Received!
Event: transfer
TxHash: 2151f16aa3a491d5979d16d6e6ad128da9851d3d65bf63cf226d8083432a201b
```

3. Check the Kepler dashboard. The webhook status should show 200 OK.

***

### What's Next?

Now that you are receiving events:

* Secure your endpoint: Implement Signature Verification.
* Handle logic: Parse the `topics` array to decode transfer amounts and recipients.
* Go to production: Deploy your Node.js script to a cloud provider (AWS, Heroku, DigitalOcean).
