# 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).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.projectx.mx/products/kepler/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
