JSON-RPC and HTTP APIs - Request/Response Patterns | XRPL APIs & Integration | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
intermediate45 min

JSON-RPC and HTTP APIs - Request/Response Patterns

Learning Objectives

Implement JSON-RPC requests over HTTP to XRPL servers

Structure requests correctly with method, params, and API version

Parse response objects and handle error conditions appropriately

Compare performance characteristics of HTTP vs WebSocket for various use cases

Design caching strategies and efficient patterns for read-heavy workloads

Lesson 2 built a production WebSocket client with reconnection, heartbeats, subscription tracking, and gap detection. That's powerful—and complex. Sometimes you don't need powerful. Sometimes you need simple.

The Reality:

MANY XRPL INTEGRATIONS DON'T NEED WEBSOCKET:

✓ "Check if payment received" → One query
✓ "Get account balance" → One query
✓ "Look up transaction by hash" → One query
✓ "Verify address is valid" → One query
✓ "Get current exchange rate" → One query

- Connection establishment overhead
- Connection maintenance complexity
- Subscription management
- State to track

JSON-RPC: Request → Response → Done

If your use case is "make a query and get an answer," JSON-RPC is often the better choice. This lesson teaches you when and how.


JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol. The XRPL implementation follows JSON-RPC 2.0 with some variations.

Core Concept: You send a JSON object describing what you want. You get a JSON object back with the result.

HTTP POST → rippled server
┌─────────────────────────────────────┐
│ {                                   │
│   "method": "account_info",         │
│   "params": [{                      │
│     "account": "rXXX...",           │
│     "ledger_index": "validated"     │
│   }]                                │
│ }                                   │
└─────────────────────────────────────┘
                │
                ▼
┌─────────────────────────────────────┐
│ {                                   │
│   "result": {                       │
│     "account_data": {...},          │
│     "status": "success"             │
│   }                                 │
│ }                                   │
└─────────────────────────────────────┘
← HTTP Response

Standard XRPL JSON-RPC Request:

{
  "method": "command_name",
  "params": [{
    "param1": "value1",
    "param2": "value2",
    "api_version": 2
  }]
}

Key Elements:

  • method: The command name (e.g., "account_info", "tx", "submit")
  • params: Array containing one object with parameters (XRPL quirk: always an array with one element)
  • api_version: Should be 2 for new development (goes inside params object)
Pro Tip

Note Unlike standard JSON-RPC 2.0, XRPL doesn't require an `id` field for HTTP requests (the response is on the same HTTP connection). However, including it is harmless and useful for logging.

Successful Response:

{
  "result": {
    "account_data": {
      "Account": "rN7n3473SaZBCG4dFL83w7a1RXtXtbk2D9",
      "Balance": "123456789",
      "Flags": 0,
      "LedgerEntryType": "AccountRoot",
      "OwnerCount": 5,
      "PreviousTxnID": "ABC123...",
      "PreviousTxnLgrSeq": 12345678,
      "Sequence": 42,
      "index": "DEF456..."
    },
    "ledger_current_index": 87654321,
    "status": "success",
    "validated": true
  }
}

Error Response:

{
  "result": {
    "error": "actNotFound",
    "error_code": 19,
    "error_message": "Account not found.",
    "request": {
      "account": "rInvalidAddress...",
      "command": "account_info",
      "ledger_index": "validated"
    },
    "status": "error"
  }
}

Key Response Fields:

  • result.status: "success" or "error"
  • result.error: Error type identifier (if error)
  • result.error_message: Human-readable error description (if error)
  • result.validated: Whether data is from validated ledger (important!)

import requests
import json

class XRPLClient:
def init(self, server_url, api_version=2):
self.server_url = server_url
self.api_version = api_version
self.session = requests.Session() # Reuse connection

def request(self, method, params=None):
"""Make a JSON-RPC request to XRPL"""

payload = {
"method": method,
"params": [{
**(params or {}),
"api_version": self.api_version
}]
}

response = self.session.post(
self.server_url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=15
)

response.raise_for_status() # Raise for HTTP errors

result = response.json()

Check for XRPL-level errors

    if "result" in result:
        if result["result"].get("status") == "error":
            raise XRPLError(
                result["result"].get("error"),
                result["result"].get("error_message"),
                result["result"].get("error_code")
            )
        return result["result"]

raise XRPLError("unknown", "Unexpected response format")

def account_info(self, account, ledger_index="validated"):
return self.request("account_info", {
"account": account,
"ledger_index": ledger_index
})

def tx(self, transaction_hash):
return self.request("tx", {
"transaction": transaction_hash
})

def server_info(self):
return self.request("server_info")

class XRPLError(Exception):
def init(self, error_type, message, code=None):
self.error_type = error_type
self.message = message
self.code = code
super().init(f"{error_type}: {message}")

try:
info = client.account_info("rN7n3473SaZBCG4dFL83w7a1RXtXtbk2D9")
print(f"Balance: {int(info['account_data']['Balance']) / 1_000_000} XRP")
except XRPLError as e:
print(f"Error: {e}")
```

const fetch = require('node-fetch')  // or use built-in fetch in Node 18+

class XRPLClient {
constructor(serverUrl, apiVersion = 2) {
this.serverUrl = serverUrl
this.apiVersion = apiVersion
}

async request(method, params = {}) {
const payload = {
method: method,
params: [{
...params,
api_version: this.apiVersion
}]
}

const response = await fetch(this.serverUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
timeout: 15000
})

if (!response.ok) {
throw new Error(HTTP error: ${response.status})
}

const data = await response.json()

if (data.result?.status === 'error') {
throw new XRPLError(
data.result.error,
data.result.error_message,
data.result.error_code
)
}

return data.result
}

async accountInfo(account, ledgerIndex = 'validated') {
return this.request('account_info', {
account,
ledger_index: ledgerIndex
})
}

async tx(transactionHash) {
return this.request('tx', {
transaction: transactionHash
})
}

async serverInfo() {
return this.request('server_info')
}
}

class XRPLError extends Error {
constructor(errorType, message, code) {
super(${errorType}: ${message})
this.errorType = errorType
this.code = code
}
}

try {
const info = await client.accountInfo('rN7n3473SaZBCG4dFL83w7a1RXtXtbk2D9')
console.log(Balance: ${parseInt(info.account_data.Balance) / 1_000_000} XRP)
} catch (error) {
console.error('Error:', error.message)
}
}

main()
```

For quick testing and debugging, cURL works directly:

# Get account info
curl -X POST https://s1.ripple.com:51234  \
  -H "Content-Type: application/json" \
  -d '{
    "method": "account_info",
    "params": [{
      "account": "rN7n3473SaZBCG4dFL83w7a1RXtXtbk2D9",
      "ledger_index": "validated",
      "api_version": 2
    }]
  }'

# Get server info
curl -X POST https://s1.ripple.com:51234  \
  -H "Content-Type: application/json" \
  -d '{"method": "server_info", "params": [{"api_version": 2}]}'

# Look up transaction
curl -X POST https://s1.ripple.com:51234  \
  -H "Content-Type: application/json" \
  -d '{
    "method": "tx",
    "params": [{
      "transaction": "E08D6E9754025BA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7",
      "api_version": 2
    }]
  }'

XRPL errors fall into categories:

  • 500: Server error

  • 503: Server overloaded/unavailable

  • Connection refused: Server down

  • Timeout: Server not responding

  • actNotFound: Account doesn't exist

  • txnNotFound: Transaction not found

  • invalidParams: Bad request parameters

  • lgrNotFound: Ledger not available

  • noPermission: Admin method on public server

  • tooBusy: Server overloaded

  • unknownCmd: Unrecognized command

import requests
from requests.exceptions import Timeout, ConnectionError, HTTPError

class RobustXRPLClient:
def init(self, server_url, timeout=15, retries=3):
self.server_url = server_url
self.timeout = timeout
self.retries = retries
self.session = requests.Session()

def request(self, method, params=None):
last_error = None

for attempt in range(self.retries):
try:
return self._make_request(method, params)

except Timeout:
last_error = XRPLError("timeout", "Request timed out")
continue # Retry

except ConnectionError as e:
last_error = XRPLError("connection", str(e))
continue # Retry

except HTTPError as e:
if e.response.status_code == 503:
last_error = XRPLError("overloaded", "Server overloaded")
continue # Retry
raise # Don't retry other HTTP errors

except XRPLError as e:
# Decide if retryable
if e.error_type in ['tooBusy', 'slowDown']:
last_error = e
continue # Retry
raise # Don't retry other XRPL errors

raise last_error

def _make_request(self, method, params=None):
payload = {
"method": method,
"params": [{**(params or {}), "api_version": 2}]
}

response = self.session.post(
self.server_url,
json=payload,
timeout=self.timeout
)
response.raise_for_status()

result = response.json()

if result.get("result", {}).get("status") == "error":
raise XRPLError(
result["result"]["error"],
result["result"].get("error_message", "Unknown error"),
result["result"].get("error_code")
)

return result["result"]
```

ERROR                   RETRYABLE?   ACTION
──────────────────────────────────────────────────────
timeout                 Yes          Retry with backoff
connectionError         Yes          Retry, maybe try different server
tooBusy                 Yes          Retry with longer backoff
slowDown                Yes          Retry with longer backoff
overloaded (503)        Yes          Retry with longer backoff

actNotFound No Account doesn't exist, handle in logic
txnNotFound No Transaction doesn't exist
invalidParams No Fix request parameters
lgrNotFound No Ledger not available (try different server or index)
noPermission No Can't use this method on public server

unknownCmd No Typo or unsupported command
malformedRequest No Fix request format
```


HTTP connections have overhead (TCP handshake, TLS negotiation). Reuse them:

# BAD: New connection per request
def get_balance_bad(account):
    response = requests.post(url, json=payload)  # New connection each time
    return response.json()

# GOOD: Reuse session
session = requests.Session()

def get_balance_good(account):
    response = session.post(url, json=payload)  # Reuses connection
    return response.json()

Impact: Connection reuse can be 2-5x faster for multiple requests.

When you need multiple independent queries, parallelize:

import asyncio
import aiohttp

async def parallel_account_info(accounts):
    async with aiohttp.ClientSession() as session:
        tasks = [
            fetch_account(session, account) 
            for account in accounts
        ]
        return await asyncio.gather(*tasks)

async def fetch_account(session, account):
    payload = {
        "method": "account_info",
        "params": [{"account": account, "api_version": 2}]
    }
    async with session.post(url, json=payload) as response:
        data = await response.json()
        return data["result"]

# Usage
accounts = ["rXXX...", "rYYY...", "rZZZ..."]
results = asyncio.run(parallel_account_info(accounts))
// JavaScript parallel requests
async function parallelAccountInfo(accounts) {
  const promises = accounts.map(account => 
    client.accountInfo(account)
  )
  return Promise.all(promises)
}

Some data changes rarely and can be cached:

from functools import lru_cache
from datetime import datetime, timedelta

class CachedXRPLClient:
    def __init__(self, server_url):
        self.client = XRPLClient(server_url)
        self.cache = {}
        self.cache_ttl = {}

def _get_cached(self, key, ttl_seconds):
        """Get from cache if not expired"""
        if key in self.cache:
            if datetime.now() < self.cache_ttl[key]:
                return self.cache[key]
        return None

def _set_cached(self, key, value, ttl_seconds):
        """Store in cache with TTL"""
        self.cache[key] = value
        self.cache_ttl[key] = datetime.now() + timedelta(seconds=ttl_seconds)

def server_info(self):
        """Server info - cache for 60 seconds"""
        key = "server_info"
        cached = self._get_cached(key, 60)
        if cached:
            return cached

result = self.client.server_info()
        self._set_cached(key, result, 60)
        return result

def account_info(self, account, ledger_index="validated"):
        """Account info - cache briefly (5 seconds) for validated"""
        key = f"account_info:{account}:{ledger_index}"
        ttl = 5 if ledger_index == "validated" else 3600  # Historical can cache longer

cached = self._get_cached(key, ttl)
        if cached:
            return cached

result = self.client.account_info(account, ledger_index)
        self._set_cached(key, result, ttl)
        return result

def tx(self, tx_hash):
        """Transaction lookup - cache forever (immutable)"""
        key = f"tx:{tx_hash}"

cached = self._get_cached(key, float('inf'))
        if cached:
            return cached

result = self.client.tx(tx_hash)

# Only cache if validated (final)
        if result.get("validated"):
            self._set_cached(key, result, 86400 * 365)  # 1 year

return result

Cache TTL Guidelines:

DATA TYPE                    RECOMMENDED TTL
─────────────────────────────────────────────
Validated transaction        Forever (immutable)
Historical ledger data       Hours to days
Account settings             Minutes (rarely change)
Account balance (validated)  Seconds (changes with each tx)
Current ledger               Never (changes every 3-5s)
Order book                   Never (changes constantly)
Server info                  1 minute

USE JSON-RPC (HTTP) WHEN:

✓ Single queries or small batches
✓ Serverless functions (Lambda, Cloud Functions)
✓ CLI tools and scripts
✓ Low request frequency (<1/second average)
✓ No real-time requirements
✓ Simplicity is prioritized
✓ Stateless architecture preferred

USE WEBSOCKET WHEN:

✓ Real-time updates needed
✓ High request frequency (>1/second sustained)
✓ Subscription features required
✓ Long-running services
✓ Minimizing latency critical
✓ Monitoring/alerting applications
```

Many production systems use both:

                 ┌─────────────────────────────────────┐
                 │          Your Application           │
                 └─────────────────────────────────────┘
                        │                    │
              ┌─────────┴─────────┐   ┌──────┴──────────┐
              │                   │   │                  │
              ▼                   ▼   ▼                  ▼
    ┌─────────────────┐    ┌─────────────────┐    ┌────────────┐
    │ Payment Monitor │    │   API Gateway   │    │   Lambda   │
    │  (WebSocket)    │    │ (HTTP JSON-RPC) │    │ Functions  │
    │                 │    │                 │    │ (JSON-RPC) │
    │ • Subscribe to  │    │ • Balance API   │    │            │
    │   transactions  │    │ • Tx lookup API │    │ • Webhooks │
    │ • Push notifs   │    │ • Rate limited  │    │ • Reports  │
    └─────────────────┘    └─────────────────┘    └────────────┘

Real-world numbers (approximate):

OPERATION                    HTTP         WEBSOCKET
──────────────────────────────────────────────────────
Single request latency      50-200ms      30-150ms
Connection setup            100-500ms     100-500ms (once)
10 sequential requests      500-2000ms    300-1500ms
10 parallel requests        100-300ms     100-300ms
1000 requests (1/second)    ~50-200s      ~30-150s
Subscription update         N/A           <100ms

- New TCP connection: ~50-100ms (if not reused)
- TLS handshake: ~50-100ms (if not reused)
- Request/response: ~30-100ms

With connection reuse, HTTP approaches WebSocket performance
for request/response patterns.

JSON-RPC is ideal for serverless:

import json
import requests

def lambda_handler(event, context):
    """Lambda function to check XRP balance"""

account = event.get('account')
    if not account:
        return {
            'statusCode': 400,
            'body': json.dumps({'error': 'account required'})
        }

try:
        balance = get_xrp_balance(account)
        return {
            'statusCode': 200,
            'body': json.dumps({
                'account': account,
                'balance_xrp': balance
            })
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }

def get_xrp_balance(account):
    payload = {
        "method": "account_info",
        "params": [{
            "account": account,
            "ledger_index": "validated",
            "api_version": 2
        }]
    }

response = requests.post(
        "https://s1.ripple.com:51234", 
        json=payload,
        timeout=10
    )

data = response.json()

if data["result"]["status"] == "error":
        raise Exception(data["result"]["error_message"])

drops = int(data["result"]["account_data"]["Balance"])
    return drops / 1_000_000
DO:
✓ Set reasonable timeouts (10-15 seconds)
✓ Handle cold starts (first request slower)
✓ Use connection pooling where platform supports
✓ Keep functions focused (single purpose)
✓ Return meaningful error messages

DON'T:
✗ Try to maintain WebSocket connections
✗ Cache across invocations (unless using external cache)
✗ Make too many sequential requests (parallelize instead)
✗ Ignore cold start impact on latency
```


JSON-RPC is simpler: No connection management, no state, standard HTTP tools work

Connection reuse matters: Session/connection pooling significantly improves performance

Caching validated data is safe: Validated transactions and historical ledgers are immutable

Serverless compatibility: JSON-RPC works perfectly with Lambda, Cloud Functions, etc.

⚠️ Rate limits on public servers: Not well-documented; varies by load

⚠️ Optimal timeout values: Depends on network, server load, query complexity

⚠️ Cache invalidation timing: How quickly do you need updated balances?

🔴 Polling for real-time needs: HTTP polling is wasteful and slow compared to WebSocket subscriptions

🔴 Not handling server errors: 503 and timeouts happen; must retry appropriately

🔴 Caching non-validated data: Current ledger data can change; cache only validated data

🔴 Over-relying on single server: Public servers can be slow or unavailable; have fallbacks

JSON-RPC is the right choice when you don't need WebSocket's complexity. For serverless functions, CLI tools, and simple queries, it's clearly superior. The mistake is using HTTP when you actually need real-time updates—polling every second is wasteful when subscriptions exist. Know when simplicity serves you and when it limits you.


Assignment: Build a client wrapper that intelligently uses HTTP or WebSocket based on the operation.

Requirements:

Part 1: HTTP Client (40%)

  • Request method supporting all XRPL commands
  • Proper error handling (HTTP and XRPL errors)
  • Configurable timeout and retries
  • Connection reuse (session management)

Part 2: Smart Protocol Selection (30%)

  • Uses HTTP for simple queries (account_info, tx, server_info)
  • Uses WebSocket for subscriptions (if available)
  • Allows forcing specific protocol
  • Falls back gracefully if WebSocket unavailable

Part 3: Caching Layer (30%)

  • Cache validated transactions indefinitely

  • Cache account info with short TTL (configurable)

  • Cache server info with medium TTL

  • Provide cache statistics (hits/misses)

  • HTTP client handles successful requests

  • HTTP client handles errors correctly

  • Protocol selection chooses HTTP for account_info

  • Protocol selection chooses WebSocket for subscribe

  • Cache returns cached transactions

  • Cache expires account info appropriately

  • HTTP client correctness: 30%

  • Protocol selection logic: 30%

  • Caching implementation: 25%

  • Code quality: 15%

Time Investment: 2-3 hours

Submission: Code module with tests and usage examples

Value: This unified client simplifies your application code—you use one interface and the library handles protocol details.


1. Protocol Choice (Tests Understanding):

A serverless function needs to check if a specific transaction has been confirmed. Which protocol should it use?

A) WebSocket, to subscribe for confirmation
B) JSON-RPC, to query the transaction directly
C) Either works equally well
D) Neither—serverless can't access XRPL

Correct Answer: B

Explanation: Serverless functions are stateless and short-lived. They can't maintain WebSocket connections or wait for subscriptions. For checking transaction status, a single JSON-RPC request to the tx command is perfect: query the transaction, check if validated is true, return result. Option A is wrong because serverless can't maintain subscriptions. Option C is wrong because WebSocket is poorly suited to serverless.


2. Error Classification (Tests Knowledge):

You receive an error with type "actNotFound" when querying account_info. What should your code do?

A) Retry the request—it's a transient error
B) Handle it as expected—the account doesn't exist on the ledger
C) Try a different server—this server has incomplete data
D) Check your API version—this error indicates version mismatch

Correct Answer: B

Explanation: "actNotFound" means the account doesn't exist in the ledger—it has never been funded or doesn't exist at that address. This is a definitive answer, not a transient error. Your application logic should handle "account not found" as a valid state (e.g., user hasn't funded their wallet yet). Options A and C suggest retry/failover, which won't help—the account genuinely doesn't exist. Option D is incorrect; this isn't a version issue.


3. Caching Safety (Tests Critical Thinking):

Which of these can be safely cached for hours or longer?

A) Current account balance
B) Validated transaction by hash
C) Current ledger index
D) Order book state

Correct Answer: B

Explanation: Once a transaction is validated (in a closed ledger), it is immutable—it will never change. You can cache it forever. Options A, C, and D all represent current state that changes constantly: balances change with every transaction affecting the account, the ledger index increases every 3-5 seconds, and order books change with every trade.


4. Connection Efficiency (Tests Application):

Your service makes 100 XRPL queries per minute. How should you optimize HTTP performance?

A) Use a new connection for each request for isolation
B) Use session/connection pooling to reuse TCP connections
C) Switch to WebSocket since HTTP is too slow
D) Make all 100 requests in parallel

Correct Answer: B

Explanation: Connection pooling (HTTP keep-alive, session reuse) eliminates the overhead of TCP handshake and TLS negotiation for each request. At 100 requests/minute (~1.7/second), this overhead is significant. Option A wastes resources on connection setup. Option C is overkill—HTTP with connection reuse handles this volume fine. Option D (100 parallel requests) would overwhelm the server and likely hit rate limits.


5. Request Structure (Tests Comprehension):

What's wrong with this JSON-RPC request?

{
  "method": "account_info",
  "account": "rXXX...",
  "ledger_index": "validated"
}

A) Nothing—this is correct format
B) Parameters must be inside a "params" array
C) Missing "id" field
D) "ledger_index" should be a number

Correct Answer: B

Explanation: XRPL JSON-RPC requires parameters inside a params array containing a single object. The correct format is:

{
  "method": "account_info",
  "params": [{
    "account": "rXXX...",
    "ledger_index": "validated"
  }]
}

Option A is wrong—the flat structure doesn't work. Option C is incorrect—id is optional for HTTP. Option D is wrong—"validated" is a valid string value for ledger_index.


For Next Lesson:
Lesson 4 explores the official client libraries (xrpl.js, xrpl-py, xrpl4j)—how they abstract these protocols, when to use their high-level methods versus raw API calls, and how to navigate their documentation.


End of Lesson 3

Total words: ~4,500
Estimated completion time: 45 minutes reading + 2-3 hours for deliverable


  1. Provides relief after WebSocket complexity—"sometimes simple is better"
  2. Covers serverless integration (increasingly common deployment model)
  3. Teaches error handling specific to JSON-RPC
  4. Establishes caching patterns used throughout course

Pedagogical Contrast:
After Lesson 2's 200+ line WebSocket client, showing JSON-RPC's simplicity makes the "right tool for the job" lesson concrete. Students should feel "oh, I could have just done this" for simple use cases.

  • "Should I always use HTTP for simplicity?" → No, real-time needs WebSocket
  • "Is HTTP slower?" → Per-request yes, but for occasional queries the difference is negligible
  • "Can I mix protocols?" → Yes, and you should—that's the deliverable

Lesson 4 Setup:
Now students understand both protocols at the raw level. Lesson 4 introduces client libraries that abstract these details, helping students decide when abstraction helps and when to go raw.

Key Takeaways

1

JSON-RPC is stateless simplicity:

Send request, get response, no connection to maintain. Perfect for serverless and simple use cases.

2

Error handling has layers:

HTTP errors (status codes) and XRPL errors (result.status) must both be handled. Some errors are retryable, some aren't.

3

Connection reuse improves performance:

Use HTTP sessions/connection pooling to avoid repeated TCP/TLS handshakes.

4

Caching is safe for immutable data:

Validated transactions and historical ledgers never change. Cache them aggressively.

5

Know when HTTP isn't enough:

Real-time requirements mean WebSocket. Don't poll when you can subscribe. ---