Setup & Connection
This guide covers how to connect to Warp streams via SSE or WebSocket, authenticate, handle events, and implement best practices for reliable real-time streaming.
Authentication
SSE Authentication
SSE streams accept API key authentication via:
x-api-keyheader (recommended)Authorization: Bearerheaderapi_keyquery parameter (when headers are not practical)
Note: The native browser
EventSourceAPI doesn't support custom headers. UsefetchwithReadableStreamfor browser applications, or theeventsourcenpm package for Node.js.
// Recommended: Using header
const response = await fetch(`https://mainnet-api.inodra.com/v1/warp/${streamId}/stream`, {
headers: { 'x-api-key': apiKey }
})
// Alternative: Using query parameter (when headers are not practical)
const response = await fetch(
`https://mainnet-api.inodra.com/v1/warp/${streamId}/stream?api_key=${apiKey}`
)Security Warning: Using
api_keyas a query parameter may expose your API key in server access logs, browser history, and referrer headers. Prefer header-based authentication when possible. If you must use query parameters, consider using short-lived or scoped API keys.
WebSocket Authentication
WebSocket connections use query parameters for authentication (headers don't work reliably with WebSocket upgrades):
const ws = new WebSocket(`wss://mainnet-api.inodra.com/v1/warp/${streamId}/ws?api_key=${apiKey}`)Security Note: WebSocket upgrade requests don't reliably support custom headers, so query parameter authentication is the standard approach. The same security considerations apply - your API key may appear in logs. Consider using scoped API keys for WebSocket connections.
SSE Connection
Basic SSE Connection
const streamId = 'your-stream-id'
const apiKey = 'your-api-key'
const abort = new AbortController()
process.on('SIGINT', () => abort.abort())
const res = await fetch(`https://mainnet-api.inodra.com/v1/warp/${streamId}/stream`, {
headers: { 'x-api-key': apiKey },
signal: abort.signal
})
let buffer = ''
for await (const chunk of res.body) {
buffer += new TextDecoder().decode(chunk)
const parts = buffer.split('\n\n')
buffer = parts.pop()
for (const part of parts) {
const dataLine = part.split('\n').find((l) => l.startsWith('data:'))
if (dataLine) console.log(JSON.parse(dataLine.slice(5)))
}
}SSE with eventsource Package (Node.js)
npm install eventsourceconst EventSource = require('eventsource')
const eventSource = new EventSource(`https://mainnet-api.inodra.com/v1/warp/${streamId}/stream`, {
headers: { 'x-api-key': apiKey }
})
eventSource.addEventListener('event', (e) => {
const data = JSON.parse(e.data)
console.log('Event:', data)
})
eventSource.addEventListener('connected', (e) => {
console.log('Connected:', JSON.parse(e.data))
})SSE Reconnection with Last-Event-ID
SSE supports automatic reconnection using the Last-Event-ID header:
const headers = { 'x-api-key': apiKey }
// On reconnect, pass the last received event ID
if (lastEventId) {
headers['Last-Event-ID'] = lastEventId
}
const res = await fetch(`https://mainnet-api.inodra.com/v1/warp/${streamId}/stream`, { headers })WebSocket Connection
Basic WebSocket Connection
const streamId = 'your-stream-id'
const apiKey = 'your-api-key'
const ws = new WebSocket(`wss://mainnet-api.inodra.com/v1/warp/${streamId}/ws?api_key=${apiKey}`)
ws.onopen = () => {
console.log('WebSocket connected')
}
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
switch (msg.event) {
case 'connected':
console.log('Stream connected:', msg.data)
break
case 'event':
case 'address':
case 'coin':
case 'object':
console.log('Received:', msg.data)
// Send ACK for exactly-once delivery
if (msg.id) {
ws.send(JSON.stringify({ type: 'ack', id: msg.id }))
}
break
case 'quota_exceeded':
console.log('Quota exceeded, disconnecting')
break
case 'error':
console.error('Error:', msg.data)
break
}
}
ws.onclose = (event) => {
console.log(`Connection closed: ${event.code} ${event.reason}`)
}WebSocket ACK Protocol
WebSocket connections support exactly-once delivery through client acknowledgments:
- Server sends an event with an
idfield - Client processes the event
- Client sends
{"type": "ack", "id": "EVENT_ID"}within 60 seconds - Server marks the event as delivered
Important timing:
- ACK within 60s: Exactly-once delivery guaranteed. If you disconnect and reconnect, you won't receive this event again.
- No ACK within 60s: Server auto-acknowledges to prevent message buildup. Falls back to at-least-once delivery.
- Disconnect before ACK: Event will be redelivered on reconnect (exactly-once preserved).
Best Practice: ACK events as soon as you've processed them. Don't batch ACKs or delay unnecessarily.
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
if (msg.id) {
// Process the event first
processEvent(msg.data)
// Then send ACK
ws.send(JSON.stringify({ type: 'ack', id: msg.id }))
}
}WebSocket Reconnection with lastEventId
WebSocket uses a query parameter for reconnection:
let lastEventId = null
function connect() {
let url = `wss://mainnet-api.inodra.com/v1/warp/${streamId}/ws?api_key=${apiKey}`
if (lastEventId) {
url += `&lastEventId=${lastEventId}`
}
const ws = new WebSocket(url)
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
if (msg.id) {
lastEventId = msg.id
ws.send(JSON.stringify({ type: 'ack', id: msg.id }))
}
}
ws.onclose = () => {
// Reconnect after delay
setTimeout(connect, 1000)
}
}
connect()Event Types
Both SSE and WebSocket send the same event types with the same structure.
connected - Connection Established
Sent immediately when the connection is established:
{
"connId": "conn_abc123",
"subscriptionId": "sub_xyz789",
"subscriptionType": "event",
"protocol": "sse",
"eventType": "0x2::coin::Transfer"
}The protocol field will be "sse" or "websocket" depending on connection type.
event - Blockchain Event
Sent when a matching blockchain event occurs:
{
"activityType": "package_event",
"txDigest": "5KmR...",
"eventSequence": 0,
"checkpoint": 12345678,
"timestamp": 1704729600000,
"type": "0x2::coin::Transfer",
"sender": "0xabc...",
"data": {
"amount": "1000000000",
"recipient": "0xdef..."
}
}quota_exceeded - CU Limit Reached
Sent when your compute unit quota is exhausted:
{
"remaining": 0,
"eventCount": 150
}The connection will close after this event.
Error Handling
SSE Error Handling
try {
const res = await fetch(url, { headers: { 'x-api-key': apiKey }, signal: abort.signal })
// ... process stream
} catch (err) {
if (err.name === 'AbortError') return // Intentional shutdown
console.error('Connection error:', err)
// Implement reconnection logic
}WebSocket Error Handling
ws.onerror = (error) => {
console.error('WebSocket error:', error)
}
ws.onclose = (event) => {
if (event.code !== 1000) {
// Abnormal close, reconnect
setTimeout(connect, 1000)
}
}Reconnection with Exponential Backoff
let reconnectAttempts = 0
async function connect() {
try {
// ... connection logic
reconnectAttempts = 0 // Reset on successful connection
} catch (err) {
if (err.name === 'AbortError') return
if (reconnectAttempts++ < 10) {
const delay = Math.min(1000 * 2 ** reconnectAttempts, 30000)
setTimeout(connect, delay)
}
}
}React Integration
Example React hook for Warp streams:
import { useEffect, useState } from 'react'
function useWarpStream(streamId, apiKey) {
const [events, setEvents] = useState([])
const [error, setError] = useState(null)
useEffect(() => {
const abort = new AbortController()
;(async () => {
try {
const res = await fetch(`https://mainnet-api.inodra.com/v1/warp/${streamId}/stream`, {
headers: { 'x-api-key': apiKey },
signal: abort.signal
})
let buffer = ''
for await (const chunk of res.body) {
buffer += new TextDecoder().decode(chunk)
const parts = buffer.split('\n\n')
buffer = parts.pop()
for (const part of parts) {
const dataLine = part.split('\n').find((l) => l.startsWith('data:'))
if (dataLine) setEvents((prev) => [...prev, JSON.parse(dataLine.slice(5))].slice(-100))
}
}
} catch (err) {
if (err.name !== 'AbortError') setError('Connection lost')
}
})()
return () => abort.abort()
}, [streamId, apiKey])
return { events, error }
}
// Usage
function LiveEvents({ streamId }) {
const { events, error } = useWarpStream(streamId, process.env.NEXT_PUBLIC_INODRA_API_KEY)
return (
<div>
{error && <div>Error: {error}</div>}
<ul>
{events.map((event, i) => (
<li key={i}>
{event.type} - {event.txDigest}
</li>
))}
</ul>
</div>
)
}Best Practices
1. Handle Reconnections Gracefully
Your application should:
- Implement exponential backoff for repeated failures
- Store the last event ID to resume from where you left off
- Use AbortController (SSE) or proper close handling (WebSocket) for clean shutdown
2. Implement Deduplication (SSE)
SSE provides at-least-once delivery. Use txDigest to deduplicate:
const seen = new Set()
function handleEvent(data) {
if (seen.has(data.txDigest)) return
seen.add(data.txDigest)
// Process event...
if (seen.size > 10000) seen.clear() // Prevent memory leak
}3. Use WebSocket ACKs for Exactly-Once
If you need guaranteed exactly-once delivery, use WebSocket and always ACK events after processing:
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
if (msg.id) {
processEvent(msg.data) // Process first
ws.send(JSON.stringify({ type: 'ack', id: msg.id })) // Then ACK
}
}4. Choose the Right Protocol
| Use Case | Recommended Protocol |
|---|---|
| Browser apps, simple clients | SSE |
| Critical financial operations | WebSocket |
| High-frequency event processing | WebSocket |
| Quick prototyping | SSE |
Stream Limits by Plan
Each stream allows 1 concurrent connection (either SSE or WebSocket, not both simultaneously).
| Plan | Max Warp Streams | Concurrent Connections |
|---|---|---|
| Free | 1 | 1 |
| Team | 1 | 1 |
| Professional | 15 | 10 |
| Business | 25 | 10 |
| Enterprise | Custom | Custom |
Reconnection & Event Replay
When you reconnect to a stream, events from your disconnection period can be replayed:
- SSE: Pass
Last-Event-IDheader - WebSocket: Pass
lastEventIdquery parameter
Configuration:
- Retention period: 30 minutes
- Max buffered events: 2,000 per stream
- Overflow behavior: Oldest events dropped when buffer is full
- Event ID format: Checkpoint number (will be updated to a more granular identifier soon)
Note: These limits are designed for reconnection scenarios (network blips, deploys). For long disconnections (30+ minutes), you may miss events. Use webhooks if you need guaranteed delivery over longer periods.
Compute Unit Usage
Warp streams are billed based on compute units:
| Action | Compute Units |
|---|---|
| Event received | 100 CU |
| Connection time | ~48 CU/minute |
Connection time is billed regardless of event volume to account for the persistent connection resources.
Troubleshooting
Connection Refused
- Verify your API key is valid
- Check that the stream ID exists and is active
- Ensure you haven't exceeded connection limits
Events Not Arriving
- Verify the stream is active in the dashboard
- Check that your filter criteria match on-chain activity
- Review the stream type and configuration
Frequent Disconnections
- Check your network stability
- Implement proper reconnection logic with exponential backoff
- Consider using WebSocket for more reliable connections
WebSocket ACKs Not Working
- Ensure you're sending valid JSON:
{"type": "ack", "id": "EVENT_ID"} - Send ACK only after processing the event
- Check that the
idmatches the received event'sidfield
Next Steps
- Warp Overview - Introduction to real-time streaming
- Webhooks - Alternative push-based delivery
- API Reference - Full API documentation