Skip to content

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:

  1. x-api-key header (recommended)
  2. Authorization: Bearer header
  3. api_key query parameter (when headers are not practical)

Note: The native browser EventSource API doesn't support custom headers. Use fetch with ReadableStream for browser applications, or the eventsource npm package for Node.js.

javascript
// 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_key as 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):

javascript
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

javascript
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)

bash
npm install eventsource
javascript
const 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:

javascript
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

javascript
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:

  1. Server sends an event with an id field
  2. Client processes the event
  3. Client sends {"type": "ack", "id": "EVENT_ID"} within 60 seconds
  4. 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.

javascript
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:

javascript
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:

json
{
  "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:

json
{
  "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:

json
{
  "remaining": 0,
  "eventCount": 150
}

The connection will close after this event.

Error Handling

SSE Error Handling

javascript
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

javascript
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

javascript
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:

jsx
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:

javascript
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:

javascript
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 CaseRecommended Protocol
Browser apps, simple clientsSSE
Critical financial operationsWebSocket
High-frequency event processingWebSocket
Quick prototypingSSE

Stream Limits by Plan

Each stream allows 1 concurrent connection (either SSE or WebSocket, not both simultaneously).

PlanMax Warp StreamsConcurrent Connections
Free11
Team11
Professional1510
Business2510
EnterpriseCustomCustom

Reconnection & Event Replay

When you reconnect to a stream, events from your disconnection period can be replayed:

  • SSE: Pass Last-Event-ID header
  • WebSocket: Pass lastEventId query 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:

ActionCompute Units
Event received100 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 id matches the received event's id field

Next Steps

Released under the MIT License.