# Indexer as a Service: Real-Time Sui Events

> Real-time Sui events without running an indexer: webhooks, SSE, and WebSockets with server-side filtering. React to on-chain activity in milliseconds.

Published 2026-01-30 · argos.sui, Cofounder · https://inodra.com/blog/indexer-as-a-service

Every Sui application needs to react to on-chain events. A DeFi protocol needs to update positions when swaps happen. A game needs to know when players mint items. A wallet needs to show transactions as they confirm.

The traditional approach? Poll an RPC endpoint every few seconds, parse the results, and hope you didn't miss anything between requests. It works, but it's wasteful, slow, and brittle.

There's a better way. Inodra delivers Sui events to your application in real-time - via webhooks, Server-Sent Events, or WebSockets. You define what you care about, we handle the indexing and delivery.

This post walks through all three delivery methods, shows you how to filter events before they reach your backend, and includes working code examples using a simple PingPong contract on mainnet.

## The Three Ways to Receive Events

Different use cases need different delivery mechanisms. Here's what we support and when to use each.

### Webhooks

HTTP POST requests to your endpoint whenever matching events occur. Your server receives a JSON payload, processes it, and returns a 2xx status code.

**Best for:** Backend integrations, serverless functions, async processing pipelines.

**Reliability:** Automatic retries with exponential backoff - up to 10 attempts over approximately 17 hours. Every webhook includes an HMAC signature so you can verify it came from us.

**Trade-off:** You need a publicly accessible endpoint. If your server is down, events queue up and retry.

### Warp SSE (Server-Sent Events)

A persistent HTTP connection where we push events to you. Your client opens a connection and keeps it open - events arrive the moment they happen.

**Best for:** Real-time dashboards, monitoring tools, any UI that needs live data without polling.

**Reliability:** At-least-once delivery. If your connection drops, reconnect with the `Last-Event-ID` header and we'll replay anything you missed.

**Trade-off:** Events flow one direction (server to client). If you need to acknowledge receipt, use WebSockets.

### Warp WebSocket

Bidirectional connection with an ACK protocol. You receive events and send back acknowledgments - we don't consider an event delivered until you confirm it.

**Best for:** Exactly-once delivery requirements, trading bots, anything where missing an event is unacceptable.

**Reliability:** No event is marked delivered until your client ACKs it. If you disconnect without ACKing, the event replays on reconnect.

**Trade-off:** More complex client implementation. You need to handle the ACK protocol correctly.

![Creating a webhook in the Inodra dashboard](/blog/indexer-as-a-service/webhook-creation-dialog.png)

## What You Can Monitor

Four subscription types cover most use cases:

### Event Subscriptions

Monitor Move package events by type. This is the most common pattern - subscribe to a specific event struct from a deployed package.

**Example:** Track every `PingPong` event from our demo contract.

**Use cases:** DeFi protocol events, NFT mints, game actions, governance votes.

### Address Subscriptions

Monitor wallet activity. Know when an address sends a transaction or receives objects.

**Use cases:** Whale watching, user activity feeds, account monitoring.

### Coin Subscriptions

Monitor balance changes for a specific token on a specific address.

**Use cases:** Treasury monitoring, payment confirmations, automated accounting.

### Object Subscriptions

Monitor NFT and object state changes - created, mutated, deleted, wrapped, unwrapped.

**Use cases:** NFT marketplaces, game item tracking, dynamic NFT updates.

![Creating an event subscription in the dashboard](/blog/indexer-as-a-service/event-subscription-form.png)

## Event Payload Filtering

Here's where things get interesting. Most notification systems blast you with everything and expect your backend to filter. That works until you're paying for compute on events you don't care about.

Inodra filters at the source. Define conditions on event fields, and only matching events get delivered.

### How It Works

- Define up to 10 filter conditions per subscription
- Filters use AND logic - all conditions must match
- Supports nested fields with dot notation (e.g., `nested.inner.value`)

### Supported Operators

**Numeric types (u8 through u256):**
- `eq` - equals
- `neq` - not equals
- `gt` - greater than
- `gte` - greater than or equal
- `lt` - less than
- `lte` - less than or equal

**Strings and addresses:**
- `eq` - exact match
- `neq` - not equal
- `contains` - substring match

**Boolean:**
- `eq` - equals
- `neq` - not equals

### Practical Example

Let's say you only care about PingPong events where someone sent more than 1 SUI (1 SUI = 1,000,000,000 MIST):

```json
{
  "field": "amount",
  "moveType": "u64",
  "operator": "gt",
  "value": "1000000000"
}
```

Add this filter to your subscription, and small pings never touch your infrastructure. You only process the events that matter.

![Setting up field filters to only receive events where amount > 1 SUI](/blog/indexer-as-a-service/field-filter-setup.png)

### Index On Demand

Payload filtering requires us to decode and index your Move package's event types. If your package isn't indexed yet and you want to use filters, reach out - we'll index it for you. This is manual right now while we build out self-serve indexing, but we typically turn these around within a day.

## Let's Trigger Some Events

Time for working code. We'll use a simple PingPong contract deployed on Sui mainnet:

```
Package: 0x7438a2f45d1b5548df81e714cbe5bbcb77c0dd25198373acfae368634279d221
Event: 0x7438a2f45d1b5548df81e714cbe5bbcb77c0dd25198373acfae368634279d221::ping_pong::PingPong
```

The contract is simple. You call `ping` with some SUI, it emits a `PingPong` event with your address and the amount, then returns your coins:

```move
public struct PingPong has copy, drop {
    from: address,
    amount: u64,
}
```

Here's TypeScript to trigger it using Inodra's gRPC gateway:

```typescript
import { SuiGrpcClient } from "@mysten/sui/grpc";
import { GrpcTransport } from "@protobuf-ts/grpc-transport";
import { ChannelCredentials } from "@grpc/grpc-js";
import { Transaction } from "@mysten/sui/transactions";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";

const PING_PONG_PACKAGE = "0x7438a2f45d1b5548df81e714cbe5bbcb77c0dd25198373acfae368634279d221";
const INODRA_API_KEY = "your_api_key"; // Get yours at inodra.com

async function ping() {
  const transport = new GrpcTransport({
    host: "mainnet-grpc.inodra.com:443",
    channelCredentials: ChannelCredentials.createSsl(),
    meta: { "x-api-key": INODRA_API_KEY },
  });
  const client = new SuiGrpcClient({ network: "mainnet", transport });
  const keypair = Ed25519Keypair.fromSecretKey(/* your secret key */);

  const tx = new Transaction();
  const [coin] = tx.splitCoins(tx.gas, [1_000_000]); // 0.001 SUI
  const [returnedCoin] = tx.moveCall({
    target: `${PING_PONG_PACKAGE}::ping_pong::ping`,
    arguments: [coin],
  });
  tx.transferObjects([returnedCoin], keypair.toSuiAddress());

  const result = await client.signAndExecuteTransaction({
    transaction: tx,
    signer: keypair,
  });
  console.log("TX Digest:", result.Transaction?.digest);
}

ping();
```

Run either version, and within seconds your webhook fires or your Warp stream pushes the event.

## What Your Webhook Receives

When a matching event occurs, we POST to your endpoint with these headers:

```
Content-Type: application/json
X-Dedupe-Key: {txDigest}:{eventSequence}
X-Inodra-Timestamp: 1706620800
X-Inodra-Signature: t=1706620800,v1=abc123...
User-Agent: Inodra-Notifications/1.0 (+https://inodra.com)
```

The `X-Dedupe-Key` header is useful if you need idempotent processing - same key means same event.

The payload structure:

```json
{
  "payloadVersion": 1,
  "payload": {
    "webhookId": "wh_abc123",
    "activityType": "package_event",
    "txDigest": "ABC123...",
    "eventSequence": 0,
    "checkpoint": 12345678,
    "timestamp": 1706620800000,
    "type": "0x7438...::ping_pong::PingPong",
    "sender": "0x1234...",
    "data": {
      "from": "0x1234...",
      "amount": "1000000"
    }
  }
}
```

The `data` field contains your decoded event fields. For PingPong, that's the sender's address and the amount in MIST.

![Webhook payload received](/blog/indexer-as-a-service/webhook-payload-example.png)

## Verifying Webhook Signatures

Never trust a webhook without verification. Anyone could POST to your endpoint claiming to be Inodra.

Every webhook includes an `X-Inodra-Signature` header with a timestamp and HMAC-SHA256 signature. Here's how to verify it:

```typescript
import crypto from "crypto";

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const [tPart, vPart] = signature.split(",");
  const timestamp = tPart.replace("t=", "");
  const expectedSig = vPart.replace("v1=", "");

  const computedSig = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${payload}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expectedSig),
    Buffer.from(computedSig)
  );
}

// In your webhook handler
app.post("/webhook", (req, res) => {
  const signature = req.headers["x-inodra-signature"];
  const isValid = verifyWebhookSignature(
    JSON.stringify(req.body),
    signature,
    process.env.WEBHOOK_SECRET
  );

  if (!isValid) {
    return res.status(401).send("Invalid signature");
  }

  // Process the event...
  res.status(200).send("OK");
});
```

The signature includes a timestamp to prevent replay attacks. Consider rejecting webhooks where the timestamp is more than a few minutes old.

## Connecting to Warp SSE

If you don't want to expose a webhook endpoint, Warp streams bring events to you over a persistent connection:

```typescript
const subscriptionId = "sub_abc123"; // from your dashboard
const apiKey = "your_api_key";

// Note: api_key query param is convenient for EventSource which doesn't support custom headers
// For server-side use, prefer the x-api-key header instead
const eventSource = new EventSource(
  `https://mainnet-api.inodra.com/v1/warp/${subscriptionId}/stream?api_key=${apiKey}`
);

eventSource.addEventListener("event", (e) => {
  const data = JSON.parse(e.data);
  console.log("PingPong event:", data);
  // { activityType: "package_event", txDigest: "...", checkpoint: 12345678, ... }
});

eventSource.addEventListener("connected", (e) => {
  const info = JSON.parse(e.data);
  console.log("Connected to Warp stream:", info.subscriptionId);
});

eventSource.onerror = (e) => {
  console.error("Stream error, will auto-reconnect");
};
```

The browser's EventSource API handles reconnection automatically. If you need to resume from where you left off, the `Last-Event-ID` header is sent on reconnect - events are retained for 1 hour.

For Node.js, use a library like `eventsource` that supports the same API.

## What's Included in Each Tier

Every tier gets access to the same features - webhooks, Warp streams, payload filtering. The limits scale with your needs:

| Feature | Free | Team | Pro | Business |
|---------|------|------|-----|----------|
| Webhooks | 1 | 1 | 15 | 25 |
| Warp Streams | 1 | 1 | 15 | 25 |
| Event Delivery | 100 CU | 100 CU | 100 CU | 100 CU |
| Log Retention | 0 days | 7 days | 30 days | 90 days |

The free tier is real infrastructure. One webhook, one stream, full filtering capabilities. Build your prototype, validate the idea, upgrade when you need more.

## For Enterprise Teams

Some teams need more than event delivery. They need custom indexing logic, historical data queries, and infrastructure that scales with their protocol.

We're building toward full indexer-as-a-service:

- **Custom workloads** - Run your own indexing logic on our infrastructure
- **Data persistence** - We store your indexed data, you query it via API
- **Dedicated resources** - No noisy neighbors, predictable performance

If you're an enterprise team that needs custom indexing beyond what webhooks and Warp provide, <a href="https://calendly.com/inodra" target="_blank" rel="noopener noreferrer">let's talk</a>.

## Get Started

**1. Sign up:** [Create your account](https://inodra.com/login?signup=true&returnTo=/dashboard) - no credit card required for free tier.

**2. Create a subscription:** Dashboard → Webhooks or Warp → New subscription. Pick event type, add your package and event, optionally add filters.

**3. Start receiving events:** Point it at your endpoint (webhooks) or connect your client (Warp). Events flow within seconds of on-chain confirmation.

**Docs:** <a href="https://docs.inodra.com/webhooks/" target="_blank" rel="noopener noreferrer">Webhooks</a> and <a href="https://docs.inodra.com/warp" target="_blank" rel="noopener noreferrer">Warp</a> - full reference for setup and filtering.

**Questions?** <a href="https://dc.gg/inodra" target="_blank" rel="noopener noreferrer">Discord</a> - we're around daily. Plus use code JSON50 for 50% off your first month of any plan.

*- Argos, Inodra Team*

## Related posts

- [Best Sui RPC Providers in 2026](/blog/best-sui-rpc-providers) - how Sui data platforms compare on streams, webhooks and the new data primitives
- [gRPC and GraphQL on Sui Mainnet](/blog/grpc-graphql-mainnet) - the gateways that power webhooks and Warp streams under the hood
- [The Sui JSON-RPC Shutdown: What Breaks, When, and How to Keep Your App Running](/blog/sui-json-rpc-shutdown-survival-guide) - stop polling deprecated endpoints and move to push delivery
- [Introducing Inodra](/blog/introducing-inodra) - the full platform these delivery methods are part of