Real-time Sui events without running an indexer: webhooks, SSE, and WebSockets with server-side filtering. React to on-chain activity in milliseconds.
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.
Different use cases need different delivery mechanisms. Here's what we support and when to use each.
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.
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.
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.

Four subscription types cover most use cases:
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.
Monitor wallet activity. Know when an address sends a transaction or receives objects.
Use cases: Whale watching, user activity feeds, account monitoring.
Monitor balance changes for a specific token on a specific address.
Use cases: Treasury monitoring, payment confirmations, automated accounting.
Monitor NFT and object state changes - created, mutated, deleted, wrapped, unwrapped.
Use cases: NFT marketplaces, game item tracking, dynamic NFT updates.

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.
nested.inner.value)Numeric types (u8 through u256):
eq - equalsneq - not equalsgt - greater thangte - greater than or equallt - less thanlte - less than or equalStrings and addresses:
eq - exact matchneq - not equalcontains - substring matchBoolean:
eq - equalsneq - not equalsLet's say you only care about PingPong events where someone sent more than 1 SUI (1 SUI = 1,000,000,000 MIST):
{
"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.

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.
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:
public struct PingPong has copy, drop {
from: address,
amount: u64,
}
Here's TypeScript to trigger it using Inodra's gRPC gateway:
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.
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:
{
"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.

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:
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.
If you don't want to expose a webhook endpoint, Warp streams bring events to you over a persistent connection:
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.
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.
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:
If you're an enterprise team that needs custom indexing beyond what webhooks and Warp provide, let's talk.
1. Sign up: Create your account - 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: Webhooks and Warp - full reference for setup and filtering.
Questions? Discord - we're around daily. Plus use code JSON50 for 50% off your first month of any plan.
- Argos, Inodra Team
All 13 providers on Mysten Labs' official Sui gRPC list compared: GraphQL, archival, JSON-RPC continuity after the July 2026 shutdown, streaming, webhooks.
Sui's public JSON-RPC endpoints shut down in July 2026 - testnet July 8, mainnet by July 31. What breaks, the fastest fix, and how to migrate.
Production gRPC and GraphQL on Sui mainnet, backed by full archival history.