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 ResponseStandard 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)
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}")
Usage
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
}
}
// Usage
async function main() {
const client = new XRPLClient('https://s1.ripple.com:51234" target="_blank" rel="noopener noreferrer" class="text-cyan-400 hover:text-cyan-300 underline hover:no-underline transition-colors inline-flex items-center gap-1">https://s1.ripple.com:51234">https://s1.ripple.com:51234 ')
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 minuteUSE 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.
- JSON-RPC 2.0: https://www.jsonrpc.org/specification
- Note: XRPL uses a variant with params array
- HTTP API: https://xrpl.org/docs/references/http-websocket-apis
- Request Format: https://xrpl.org/docs/references/http-websocket-apis/request-formatting
- HTTP Keep-Alive: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive
- Connection Pooling in Python requests: https://requests.readthedocs.io/en/latest/user/advanced/
- AWS Lambda Best Practices: https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html
- Serverless HTTP: https://www.serverless.com/blog/serverless-http
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
- Provides relief after WebSocket complexity—"sometimes simple is better"
- Covers serverless integration (increasingly common deployment model)
- Teaches error handling specific to JSON-RPC
- 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
JSON-RPC is stateless simplicity:
Send request, get response, no connection to maintain. Perfect for serverless and simple use cases.
Error handling has layers:
HTTP errors (status codes) and XRPL errors (result.status) must both be handled. Some errors are retryable, some aren't.
Connection reuse improves performance:
Use HTTP sessions/connection pooling to avoid repeated TCP/TLS handshakes.
Caching is safe for immutable data:
Validated transactions and historical ledgers never change. Cache them aggressively.
Know when HTTP isn't enough:
Real-time requirements mean WebSocket. Don't poll when you can subscribe. ---