IBExWalletAPI
Integration

WebSocket Integration

IBEX FI API WebSocket Integration Guide

Overview

The IBEX FI API provides WebSockets to receive real-time updates for your balances and transactions. Data is synchronized with the blockchain through our BCReader infrastructure. The system uses a hybrid architecture that combines real-time WebSocket connections with reliable HTTP fallback endpoints, ensuring your application can always access wallet data.

Benefits of IBEX FI WebSockets

  • Real-time: Instant updates as soon as transactions are mined
  • Automatic: Subscriptions handled automatically by your Safe address
  • Initial data: Balance and transactions sent automatically after authentication
  • Secure: Standard JWT authentication
  • Reliable: Automatic reconnection and error handling
  • Efficient: No constant client-side polling
  • Fallback support: HTTP endpoints available when WebSocket is unavailable

Architecture

[Blockchain] → [BCReader] → [IBEX FI API] → [Your Application]
                   ↓ WS        ↓ WS         ↓ Updates UI
                                ↓ HTTP      ↓ Fallback

Hybrid Architecture

Client Application

┌─────────────────┐    ┌──────────────────┐
│   WebSocket     │ ←→ │   HTTP API       │
│   (real-time)   │    │   (fallback)     │
└─────────────────┘    └──────────────────┘
       ↓                        ↓
┌─────────────────────────────────────────┐
│         IBEX FI API Server              │
│  ┌─────────────┐  ┌─────────────────┐  │
│  │ WebSocket   │  │ REST Endpoints  │  │
│  │ Manager     │  │ /v1.2/users/me/*  │  │
│  └─────────────┘  └─────────────────┘  │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│            BCReader Service             │
│     (blockchain data source)           │
└─────────────────────────────────────────┘

WebSocket Connection

Endpoints

// Staging
wss://api-staging.ibex.fi/ws
 
// Testnet  
wss://api-testnet.ibex.fi/ws
 
// Production
wss://api.ibex.fi/ws
 
// Custom domain
wss://your-domain.ibex.fi/ws

Authentication

Authenticate the connection via a JSON message after the WebSocket opens.

const ws = new WebSocket('wss://api-staging.ibex.fi/ws');
 
ws.onopen = () => {
  // Send the authentication message immediately after opening
  ws.send(JSON.stringify({
    type: 'auth',
    token: your_jwt_token,            // JWT obtained from auth endpoints
    clientName: 'IBEx Wallet Web App' // optional, useful for monitoring
  }));
};

Authentication message (client → server):

{
  "type": "auth",
  "token": "eyJhbGciOi...",
  "clientName": "IBEx Wallet Web App"
}

Important notes:

  • The server extracts rpId from the hostname and verifies that the JWT iss matches rpId.
  • On failure, the server sends auth_error with an explicit message (e.g., expired token).
  • The JWT token must be valid and not expired.

Connection Lifecycle

// Connection opened
ws.onopen = () => {
  console.log('WebSocket connection established');
  // Send authentication immediately
  ws.send(JSON.stringify({ type: 'auth', token: jwtToken }));
};
 
// Message received
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  handleWebSocketMessage(message);
};
 
// Connection closed
ws.onclose = (code, reason) => {
  console.log(`WebSocket closed: ${code} ${reason}`);
  // Implement reconnection logic
  scheduleReconnect();
};
 
// Connection error
ws.onerror = (error) => {
  console.error('WebSocket error:', error);
  // Fallback to HTTP API
  fallbackToHttp();
};

Received Messages

Successful Authentication and Initial Data

After successful authentication, you will automatically receive the following messages:

  1. auth_success (or connection_success)
  2. user_data (sent early; may be skipped if recently sent via HTTP — 3s dedup window)
  3. balance_data
  4. transaction_data
  5. chainid_data
  6. recovery_data
  7. user_data (sent again unconditionally after recovery_data)

Important: user_data may be received twice in the initial burst — clients should handle this gracefully (e.g. overwrite previous data). The order of messages 2–7 should not be relied upon for application logic.

Message Types

1. Authentication Success

{
  "type": "auth_success",
  "data": {
    "safeAddress": "0x1234...",
    "message": "Connected to real-time updates"
  },
  "timestamp": "2024-01-31T12:00:00.000Z"
}

Alternative format:

{
  "type": "connection_success",
  "data": {
    "safeAddress": "0x7CDb9e9b831C1639376aD8408650cE1a83D51D5a",
    "message": "Connected to real-time updates"
  },
  "timestamp": "2025-08-01T07:36:04.083Z"
}

2. Initial Balance Data

{
  "type": "balance_data",
  "data": {
    "safeAddress": "0x1234...",
    "balance": {
      "balances": [
        {
          "token": "0x...",
          "symbol": "EURe",
          "balance": "123.456789",
          "decimals": 18
        }
      ]
    }
  },
  "timestamp": "2024-01-31T12:00:00.000Z"
}

3. Initial Transaction Data

{
  "type": "transaction_data",
  "data": {
    "safeAddress": "0x1234...",
    "transactions": {
      "data": [
        {
          "hash": "0xabcdef...",
          "blockNumber": 12345678,
          "timestamp": "2024-01-31T11:59:00.000Z",
          "from": "0xabc...",
          "to": "0x1234...",
          "value": "100.0",
          "direction": "IN"
        }
      ]
    }
  },
  "timestamp": "2024-01-31T12:00:00.000Z"
}

4. Balance Update

Received when wallet balance changes:

{
  "type": "balance_update", 
  "data": {
    "address": "0x1234...",
    "balance": "123.456789",
    "updated_at": "2024-01-31T12:00:00.000Z"
  },
  "timestamp": "2024-01-31T12:00:00.000Z"
}

Alternative format:

{
  "type": "balance_update",
  "data": {
    "address": "0x7CDb9e9b831C1639376aD8408650cE1a83D51D5a",
    "balance": "1250000000000000000",
    "updated_at": "2025-08-01T07:36:04.083Z"
  },
  "timestamp": "2025-08-01T07:36:04.083Z"
}

5. New Transaction

Received when a new transaction is detected:

{
  "type": "new_transaction",
  "data": {
    "address": "0x1234...",
    "newTransaction": {
      "hash": "0xabcdef...",
      "blockNumber": 12345678,
      "timestamp": "2024-01-31T12:00:00.000Z",
      "from": "0xabc...",
      "to": "0xdef...",
      "tokenAddress": "0xcb444e90d8198415266c6a2724b7900fb12fc56e",
      "tokenType": "ERC20",
      "tokenSymbol": "EURe",
      "tokenName": "Monerium EUR emoney",
      "value": "100.0",
      "direction": "IN"
    },
    "recentTransactions": [ /* recent transactions (optional) */ ],
    "transactionCount": 1,
    "historyLimit": 5
  },
  "timestamp": "2024-01-31T12:00:00.000Z"
}

6. IBAN Status Update

Emitted when the IBAN status changes for a Safe address (e.g. from pending to approved). Triggered by the /system/users/iban webhook. Only sent when the status actually changes.

{
  "type": "user.iban.updated",
  "data": {
    "safeAddress": "0x1234...",
    "iban": "LI08088110102905K002E",
    "previousState": "pending",
    "newState": "approved",
    "updatedAt": "2025-08-01T07:36:04.083Z"
  },
  "timestamp": "2025-08-01T07:36:04.083Z"
}
  • previousState: Previous IBAN status (or "UNKNOWN" if first provisioning).
  • newState: New IBAN status (e.g. "approved", "pending", "rejected").
  • Broadcast to clients connected to the matching safeAddress.

7. KYC Status Update

Emitted when the KYC (KY) status changes for a user. Triggered by the /system/users/ky webhook. Broadcast to all Safe addresses associated with the user. Only sent when the status actually changes.

{
  "type": "user.ky.updated",
  "data": {
    "safeAddress": "0x1234...",
    "previousKyc": "0",
    "newKyc": "2",
    "updatedAt": "2025-08-01T07:36:04.083Z"
  },
  "timestamp": "2025-08-01T07:36:04.083Z"
}
  • previousKyc: Previous KY value (e.g. "0", "1", "2", or "UNKNOWN").
  • newKyc: New KY value ("0" = not started, "1" = pending, "2" = verified).
  • If the user has multiple Safe addresses, a separate message is sent for each one.

8. Chain Data (ChainId)

{
  "type": "chainid_data",
  "data": {
    "defaultChainId": 100,
    "supportedChainIds": [100]
  },
  "timestamp": "2024-01-31T12:00:01.000Z"
}

9. Recovery Status

{
  "type": "recovery_data",
  "data": {
    "safeAddress": "0x1234...",
    "recoveryEnabled": false,
    "recoveryAddress": null,
    "delay": null,
    "pendingRecovery": false,
    "canExecute": false,
    "executeAfter": null
  },
  "timestamp": "2024-01-31T12:00:01.000Z"
}

10. User Data

User profile (signers, safes, KYC status, IBAN availability). The iban field is only present when the IBAN has been provisioned and approved; it contains only the chainId (the actual IBAN/BIC strings are not exposed over WebSocket).

{
  "type": "user_data",
  "data": {
    "id": "external-user-uuid",
    "ky": "2",
    "signers": [
      {
        "id": "signer-id",
        "safes": [
          {
            "address": "0x1234...",
            "threshold": 1,
            "iban": { "chainId": 100 }
          }
        ]
      }
    ]
  },
  "timestamp": "2024-01-31T12:00:01.000Z"
}
  • ky: Raw KYC status value ("0" = not started, "1" = pending, "2" = verified).
  • iban: Present only when the IBAN is approved. Contains chainId only — the IBAN/BIC strings are not included. Field is absent when no approved IBAN exists for that Safe.

HTTP Fallback Endpoints

When WebSocket connections fail or are unavailable, use these HTTP endpoints as fallback:

1. Get User Balances

Endpoint: GET /v1.2/users/me/balances

Headers:

Authorization: Bearer {your_jwt_token}
Content-Type: application/json

Response:

{
  "timestamp": "2025-08-01T07:39:06.960Z",
  "balance": {
    "address": "0x7cdb9e9b831c1639376ad8408650ce1a83d51d5a",
    "balance": "1250000000000000000"
  }
}

2. Get User Transactions

Endpoint: GET /v1.2/users/me/transactions

Headers:

Authorization: Bearer {your_jwt_token}
Content-Type: application/json

Query Parameters (optional):

  • limit: Number of transactions to return
  • offset: Number of transactions to skip

Response:

{
  "timestamp": "2025-08-01T07:39:06.960Z",
  "data": [
    {
      "hash": "0x...",
      "from": "0x...",
      "to": "0x...",
      "value": "1000000000000000000",
      "timestamp": "2025-08-01T07:36:04.083Z"
    }
  ]
}

3. Direct BCReader Proxy

Endpoint: GET /v1.2/bcreader/\{verb\}

Headers:

Authorization: Bearer {your_jwt_token}
Content-Type: application/json

Examples:

  • GET /v1.2/bcreader/balances
  • GET /v1.2/bcreader/transactions?address=0x...

Implementation Examples

JavaScript Vanilla

class IBEXWebSocket {
  constructor(apiUrl, jwtToken) {
    this.apiUrl = apiUrl;
    this.jwtToken = jwtToken;
    this.ws = null;
    this.reconnectInterval = null;
    this.maxReconnectAttempts = 5;
    this.reconnectAttempts = 0;
  }
 
  connect() {
    try {
      console.log('🔌 Connecting to IBEX WebSocket...');
      this.ws = new WebSocket(this.apiUrl);
 
      this.ws.onopen = () => {
        console.log('✅ Connected to IBEX WebSocket');
        this.reconnectAttempts = 0;
        // Send the authentication message
        this.ws.send(JSON.stringify({ 
          type: 'auth', 
          token: this.jwtToken, 
          clientName: 'Demo Client' 
        }));
      };
 
      this.ws.onmessage = (event) => {
        this.handleMessage(JSON.parse(event.data));
      };
 
      this.ws.onclose = (event) => {
        console.log(`👋 Connection closed: ${event.code} ${event.reason}`);
        this.scheduleReconnect();
      };
 
      this.ws.onerror = (error) => {
        console.error('❌ WebSocket error:', error);
        this.fallbackToHttp();
      };
 
    } catch (error) {
      console.error('❌ Connection failed:', error);
      this.scheduleReconnect();
    }
  }
 
  handleMessage(message) {
    console.log('📥 Received:', message);
    
    switch(message.type) {
      case 'auth_success':
      case 'connection_success':
        console.log('🔐 Connected to Safe:', message.data.safeAddress);
        this.onAuthSuccess(message.data);
        break;
        
      case 'balance_update':
        console.log('💰 Balance updated:', message.data.balance);
        this.onBalanceUpdate(message.data);
        break;
        
      case 'new_transaction':
        console.log('🔄 New transaction:', message.data.newTransaction?.hash);
        this.onNewTransaction(message.data);
        break;
 
      case 'user.iban.updated':
        console.log('🏦 IBAN updated:', message.data.newState);
        break;
 
      case 'user.ky.updated':
        console.log('🪪 KYC updated:', message.data.newKyc);
        break;
        
      default:
        console.log('📝 Other message type:', message.type);
    }
  }
 
  scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('❌ Max reconnect attempts reached, using HTTP fallback');
      this.fallbackToHttp();
      return;
    }
 
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    this.reconnectAttempts++;
    
    console.log(`🔄 Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
    
    this.reconnectInterval = setTimeout(() => {
      this.connect();
    }, delay);
  }
 
  async fallbackToHttp() {
    try {
      // Get balances via HTTP
      const balanceResponse = await fetch(`${this.apiUrl.replace('wss://', 'https://').replace('ws://', 'http://')}/v1.2/users/me/balances`, {
        headers: {
          'Authorization': `Bearer ${this.jwtToken}`,
          'Content-Type': 'application/json'
        }
      });
      const balanceData = await balanceResponse.json();
      this.onBalanceUpdate(balanceData.balance);
 
      // Get transactions via HTTP
      const txResponse = await fetch(`${this.apiUrl.replace('wss://', 'https://').replace('ws://', 'http://')}/v1.2/users/me/transactions?limit=10`, {
        headers: {
          'Authorization': `Bearer ${this.jwtToken}`,
          'Content-Type': 'application/json'
        }
      });
      const txData = await txResponse.json();
      txData.data.forEach(tx => this.onNewTransaction({ transaction: tx }));
    } catch (error) {
      console.error('❌ HTTP fallback failed:', error);
    }
  }
 
  // Override these methods in your implementation
  onAuthSuccess(data) {
    // Handle successful connection
    // Update UI to show real-time status
  }
 
  onBalanceUpdate(data) {
    // Handle balance updates
    // Update balance display in your UI
    const balanceElement = document.getElementById('balance');
    if (balanceElement) {
      balanceElement.textContent = `${data.balance || data.balances?.[0]?.balance || '0'} EURe`;
    }
  }
 
  onNewTransaction(data) {
    // Handle new transactions
    // Add transaction to history list
    const transactionList = document.getElementById('transactions');
    if (transactionList) {
      const tx = data.transaction || data.newTransaction;
      const txElement = document.createElement('div');
      txElement.innerHTML = `
        <div class="transaction">
          <span>${tx.direction || 'N/A'}</span>
          <span>${tx.value} ${tx.tokenSymbol || 'EURe'}</span>
          <span>${tx.hash.substring(0, 10)}...</span>
        </div>
      `;
      transactionList.prepend(txElement);
    }
  }
 
  disconnect() {
    if (this.reconnectInterval) {
      clearTimeout(this.reconnectInterval);
    }
    if (this.ws) {
      this.ws.close();
    }
  }
}
 
// Usage
const ibexWS = new IBEXWebSocket('wss://api-staging.ibex.fi/ws', 'your-jwt-token-here');
ibexWS.connect();

React Hook

import { useEffect, useRef, useState } from 'react';
 
interface BalanceUpdate {
  address: string;
  balance: string;
  updated_at: string;
}
 
interface Transaction {
  hash: string;
  blockNumber: number;
  timestamp: string;
  from: string;
  to: string;
  tokenSymbol: string;
  value: string;
  direction: 'IN' | 'OUT';
}
 
interface NewTransactionUpdate {
  address: string;
  transaction?: Transaction;
  newTransaction?: Transaction;
}
 
export const useIBEXWebSocket = (apiUrl: string, jwtToken: string) => {
  const ws = useRef<WebSocket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [balance, setBalance] = useState<string | null>(null);
  const [transactions, setTransactions] = useState<Transaction[]>([]);
  const [safeAddress, setSafeAddress] = useState<string | null>(null);
 
  useEffect(() => {
    if (!jwtToken) return;
 
    const connect = () => {
      try {
        ws.current = new WebSocket(apiUrl);
 
        ws.current.onopen = () => {
          console.log('✅ IBEX WebSocket connected');
          setIsConnected(true);
          ws.current?.send(JSON.stringify({ 
            type: 'auth', 
            token: jwtToken, 
            clientName: 'React Client' 
          }));
        };
 
        ws.current.onmessage = (event) => {
          const message = JSON.parse(event.data);
          
          switch(message.type) {
            case 'auth_success':
            case 'connection_success':
              setSafeAddress(message.data.safeAddress);
              break;
              
            case 'balance_update':
              setBalance(message.data.balance || message.data.balances?.[0]?.balance);
              break;
              
            case 'new_transaction':
              const tx = message.data.newTransaction;
              if (tx) {
                setTransactions(prev => [tx, ...prev]);
              }
              break;
 
            case 'user.iban.updated':
              console.log('IBAN status:', message.data.newState);
              break;
 
            case 'user.ky.updated':
              console.log('KYC status:', message.data.newKyc);
              break;
          }
        };
 
        ws.current.onclose = () => {
          console.log('👋 IBEX WebSocket disconnected');
          setIsConnected(false);
          // Reconnect after 5 seconds
          setTimeout(connect, 5000);
        };
 
        ws.current.onerror = (error) => {
          console.error('❌ IBEX WebSocket error:', error);
        };
 
      } catch (error) {
        console.error('❌ IBEX WebSocket connection failed:', error);
        setTimeout(connect, 5000);
      }
    };
 
    connect();
 
    return () => {
      if (ws.current) {
        ws.current.close();
      }
    };
  }, [apiUrl, jwtToken]);
 
  return {
    isConnected,
    balance,
    transactions,
    safeAddress
  };
};
 
// Usage in a React component
const WalletComponent = () => {
  const { isConnected, balance, transactions, safeAddress } = useIBEXWebSocket(
    'wss://api-staging.ibex.fi/ws',
    localStorage.getItem('jwt_token') || ''
  );
 
  return (
    <div>
      <div className="status">
        {isConnected ? '🟢 Connected' : '🔴 Disconnected'}
      </div>
      
      <div className="safe-info">
        <strong>Safe Address:</strong> {safeAddress}
      </div>
      
      <div className="balance">
        <strong>Balance:</strong> {balance} EURe
      </div>
      
      <div className="transactions">
        <h3>Recent Transactions</h3>
        {transactions.map(tx => (
          <div key={tx.hash} className="transaction">
            <span className={tx.direction}>{tx.direction}</span>
            <span>{tx.value} {tx.tokenSymbol}</span>
            <span>{tx.hash.substring(0, 10)}...</span>
            <span>{new Date(tx.timestamp).toLocaleString()}</span>
          </div>
        ))}
      </div>
    </div>
  );
};

Vue.js Composition API

import { ref, onMounted, onUnmounted } from 'vue';
 
export const useIBEXWebSocket = (apiUrl: string, jwtToken: string) => {
  const ws = ref<WebSocket | null>(null);
  const isConnected = ref(false);
  const balance = ref<string | null>(null);
  const transactions = ref<any[]>([]);
  const safeAddress = ref<string | null>(null);
 
  const connect = () => {
    try {
      ws.value = new WebSocket(apiUrl);
 
      ws.value.onopen = () => {
        console.log('✅ IBEX WebSocket connected');
        isConnected.value = true;
        ws.value?.send(JSON.stringify({ 
          type: 'auth', 
          token: jwtToken, 
          clientName: 'Vue Client' 
        }));
      };
 
      ws.value.onmessage = (event) => {
        const message = JSON.parse(event.data);
        
        switch(message.type) {
          case 'auth_success':
          case 'connection_success':
            safeAddress.value = message.data.safeAddress;
            break;
            
          case 'balance_update':
            balance.value = message.data.balance || message.data.balances?.[0]?.balance;
            break;
            
          case 'new_transaction':
            const tx = message.data.newTransaction;
            if (tx) {
              transactions.value.unshift(tx);
            }
            break;
 
          case 'user.iban.updated':
            console.log('IBAN status:', message.data.newState);
            break;
 
          case 'user.ky.updated':
            console.log('KYC status:', message.data.newKyc);
            break;
        }
      };
 
      ws.value.onclose = () => {
        console.log('👋 IBEX WebSocket disconnected');
        isConnected.value = false;
        setTimeout(connect, 5000);
      };
 
    } catch (error) {
      console.error('❌ IBEX WebSocket connection failed:', error);
      setTimeout(connect, 5000);
    }
  };
 
  onMounted(() => {
    if (jwtToken) {
      connect();
    }
  });
 
  onUnmounted(() => {
    if (ws.value) {
      ws.value.close();
    }
  });
 
  return {
    isConnected,
    balance,
    transactions,
    safeAddress
  };
};

Error Handling

WebSocket Error Codes

CodeReasonAction
1008Invalid JWT tokenRefresh the token and reconnect
1011Server errorWait and reconnect
1006Abnormal closureReconnect automatically

HTTP API Errors

CodeDescriptionSolution
401UnauthorizedRefresh JWT token
403ForbiddenCheck user permissions
404Not foundVerify endpoint URL and user data
500Internal server errorRetry with exponential backoff

Handling Expired Tokens

ws.onclose = (event) => {
  if (event.code === 1008) {
    console.log('🔑 Token expired, refreshing...');
    // Refresh your JWT token
    refreshToken().then(newToken => {
      jwtToken = newToken;
      connect();
    });
  } else {
    scheduleReconnect();
  }
};

Fallback to HTTP

async fallbackToHttp() {
  try {
    // Get balances via HTTP
    const balances = await fetch(`${apiUrl}/v1.2/users/me/balances`, {
      headers: {
        'Authorization': `Bearer ${jwtToken}`,
        'Content-Type': 'application/json'
      }
    }).then(r => r.json());
    
    this.onBalanceUpdate(balances.balance);
 
    // Get transactions via HTTP
    const transactions = await fetch(`${apiUrl}/v1.2/users/me/transactions?limit=10`, {
      headers: {
        'Authorization': `Bearer ${jwtToken}`,
        'Content-Type': 'application/json'
      }
    }).then(r => r.json());
    
    transactions.data.forEach(tx => this.onNewTransaction({ transaction: tx }));
 
  } catch (error) {
    console.error('HTTP fallback failed:', error);
  }
}

Best Practices

1. Authentication

// ✅ Good: Validate the token before connecting
if (!jwtToken || isTokenExpired(jwtToken)) {
  await refreshToken();
}
 
// ❌ Bad: Connect with an expired token
new WebSocket(url, { headers: { Authorization: `Bearer ${expiredToken}` }});

2. Connection Management

  • Implement exponential backoff for reconnection attempts
  • Set maximum reconnection limits to avoid infinite loops
  • Use connection heartbeat/ping to detect disconnections early
// ✅ Good: Exponential backoff with cap
const delay = Math.min(1000 * Math.pow(2, attempts), 30000);
 
// ❌ Bad: Immediate reconnection loop
setInterval(() => connect(), 1000);

3. Resource Cleanup

// ✅ Good: Clean up on app shutdown
window.addEventListener('beforeunload', () => {
  ibexWS.disconnect();
});
 
// ✅ Good: Clean up in useEffect (React)
useEffect(() => {
  return () => ws.current?.close();
}, []);

4. Error Handling

  • Always implement HTTP fallback when WebSocket fails
  • Handle different error codes appropriately (401, 403, 500, etc.)
  • Gracefully degrade functionality when services are unavailable

5. Performance Optimization

  • Buffer messages during disconnection and sync when reconnected
  • Implement message deduplication to handle potential duplicates
  • Use efficient JSON parsing for high-frequency updates
// ✅ Good: Debounce rapid updates
const debouncedBalanceUpdate = debounce((balance) => {
  updateBalanceUI(balance);
}, 100);
 
// ✅ Good: Limit transaction history length
setTransactions(prev => [newTx, ...prev.slice(0, 99)]);

6. Security

  • Validate JWT tokens before connection attempts
  • Handle token expiration gracefully with refresh logic
  • Use secure WebSocket connections (wss://) in production

7. Data Management

  • Cache recent data to improve user experience during reconnections
  • Implement optimistic updates for better perceived performance
  • Handle data consistency between WebSocket and HTTP responses

Keep-alive (Ping/Pong)

  • The server automatically sends a ping every 30 seconds.
  • Standard WebSocket clients automatically respond with a pong — no client action is required.
  • On closure, reconnect with exponential backoff.

Rate Limiting

  • WebSocket connections: 1 connection per user
  • HTTP API calls: 100 requests per minute per user
  • Burst allowance: 20 requests in 10 seconds

Debugging

Useful Logs

// Enable verbose logs
const DEBUG = true;
 
if (DEBUG) {
  ws.onmessage = (event) => {
    console.log('[IBEX WS] Received:', JSON.parse(event.data));
  };
}

Connection Check

// Inspect connection state
console.log('WebSocket state:', {
  readyState: ws.readyState,
  url: ws.url,
  protocol: ws.protocol
});
 
// 0: CONNECTING, 1: OPEN, 2: CLOSING, 3: CLOSED

Support

For any questions or issues:

Changelog

v1.1.0 (April 2026)

  • Fixed user_data payload: iban field contains only { chainId } when approved (IBAN/BIC strings are not exposed)
  • Fixed ky field: uses raw values ("0", "1", "2") not human-readable strings
  • Documented user.iban.updated and user.ky.updated push events with full payload and field descriptions
  • Corrected initial message sequence: user_data is sent before balance_data and may be received twice
  • Added user.iban.updated and user.ky.updated to all code examples (JS, React, Vue)

v1.0.0 (2024-01-31)

  • ✅ WebSocket connection with JWT authentication
  • ✅ Real-time balance updates
  • ✅ New transaction notifications
  • ✅ Automatic reconnection
  • ✅ Multi-Safe per user support
  • ✅ HTTP fallback endpoints
  • ✅ IBAN and KYC status updates

Last Updated: April 2026
Version: 1.1