Skip to content

Setup & Connection

This guide covers how to connect to Warp streams via WebSocket or SSE, authenticate, handle events, and implement best practices for reliable real-time streaming.

Recommendation: WebSocket is the recommended protocol - client ACKs give you exactly-once delivery without building deduplication logic. SSE is a good fit for quick prototypes and environments where WebSocket isn't available.

Authentication

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. You can rotate keys instantly from the dashboard if one leaks.

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, and rotate keys from the dashboard if you suspect one leaked.

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 is returned to the buffer and redelivered when you reconnect with lastEventId or mode=resume (exactly-once preserved). A default (live) reconnect discards the buffer, including un-ACKed events.

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 connect modes & reconnection

While you're disconnected, the stream buffers events for up to 30 minutes (max 2,000 - see Reconnection & Event Replay). The mode you connect with decides what happens to that buffer. It applies to that connection only - every connect, including every reconnect, chooses again:

  • No parameters (live, default): the buffer is discarded; events flow from connect-time forward.
  • &mode=resume: the buffer is replayed, then live events follow.
  • &lastEventId=<id>: buffered events after that ID are replayed, then live events follow (mode is ignored when lastEventId is set).

A reconnect loop that tracks the last seen event ID and passes it back never misses or duplicates events:

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

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

Browser EventSource (query parameter auth)

Native browser EventSource can't set custom headers. Authenticate with the api_key query parameter instead - you get the browser's built-in reconnection with Last-Event-ID handling for free:

javascript
const es = new EventSource(
  `https://mainnet-api.inodra.com/v1/warp/${streamId}/stream?api_key=${apiKey}`
)

es.addEventListener('connected', (e) => {
  console.log('Connected:', JSON.parse(e.data))
})

// Listen for your stream type: 'event', 'address', 'coin', or 'object'
es.addEventListener('event', (e) => {
  console.log('Event:', JSON.parse(e.data))
})

es.onerror = () => {
  // EventSource reconnects automatically; this fires on connection loss
  console.warn('Connection lost, browser will retry…')
}

Security Note: We don't recommend query-parameter auth where headers are possible - see the SSE Authentication section.

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

Connect modes: live (default) vs resume

While you're disconnected, the stream buffers events for up to 30 minutes (max 2,000 - see Reconnection & Event Replay). The mode you connect with decides what happens to that buffer. It applies to that connection only - every connect, including every reconnect, chooses again:

  • No parameters (live, default): the buffer is discarded; events flow from connect-time forward. This keeps high-volume streams real-time instead of replaying a deep stale backlog.
  • ?mode=resume: the buffer is replayed, then live events follow.
  • Last-Event-ID header: buffered events after that ID are replayed, then live events follow (mode is ignored when this header is sent).

Native EventSource sets Last-Event-ID automatically on reconnect, so browser EventSource clients resume across reconnects without losing events.

javascript
// live (default) - newest events only, buffer discarded
await fetch(`https://mainnet-api.inodra.com/v1/warp/${streamId}/stream`, {
  headers: { 'x-api-key': apiKey }
})

// replay the full buffer
await fetch(`https://mainnet-api.inodra.com/v1/warp/${streamId}/stream?mode=resume`, {
  headers: { 'x-api-key': apiKey }
})

// resume from a specific event id
const headers = { 'x-api-key': apiKey }
if (lastEventId) headers['Last-Event-ID'] = lastEventId
await fetch(`https://mainnet-api.inodra.com/v1/warp/${streamId}/stream`, { headers })

Event Types

Both WebSocket and SSE 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 - Request Limit Reached

Sent when your credit quota is exhausted; the server closes the connection right after (WebSocket connections close with code 1008 and reason QUOTA_EXCEEDED):

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

The connection will close after this event.

Error Handling

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

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
}

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 proper close handling (WebSocket) or AbortController (SSE) for clean shutdown

2. 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
  }
}

3. 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
}

4. Choose the Right Protocol

WebSocket is the recommended default. SSE remains a good choice for simple, browser-native clients:

Use CaseRecommended Protocol
Critical financial operationsWebSocket
High-frequency event processingWebSocket
Browser apps, simple clientsSSE
Quick prototypingSSE

Stream Limits by Plan

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

PlanMax Warp StreamsConcurrent Connections
Free11
Team11
Professional1510
Business2510
EnterpriseCustomCustom

Reconnection & Event Replay

Two separate things are at play. Keeping them apart makes every scenario predictable:

  1. Buffering always happens. While no client is connected, every stream buffers events for up to 30 minutes (max 2,000 events; oldest dropped first) - regardless of how you previously connected.
  2. The mode is chosen per connection. Each connect (including every reconnect) independently decides what to do with that buffer. Nothing is remembered between connections - connecting with mode=resume once does not make future connects resume.
You connect withYou receive
No parameters (default)Live events only - the buffer is discarded
?mode=resumeThe full buffer, then live events
lastEventId query (WebSocket) / Last-Event-ID header (SSE)Buffered events after that ID, then live events

So: if you connect with mode=resume, disconnect, and reconnect without parameters, the events from your downtime are discarded. To never miss events, pass the last event ID you processed (or mode=resume) on every reconnect.

Buffer limits:

  • Retention period: 30 minutes (after 30 minutes with no connection, the buffer is deleted entirely)
  • 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.

Credit Usage

Warp streams are billed based on credits:

ActionCredits
Event received2 Credits
Connection time~48 Credits/min

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.