XRP Ledger (Ripple)
XRP Ledger reads for wallets and apps, without running or trusting a single node. Every read is sent to three independent XRPL providers. It is answered only when all three return the same answer for the same validated ledger. If one provider is down, lagging, rate limiting or says something different, you get an error, never one provider's word.
Endpoint
POST https://mainnet.subfrost.io/v4/YOUR_API_KEY/ripple
The API key is a path segment, or an X-Subfrost-Api-Key header, which takes
precedence (see Authentication). The body is
rippled's JSON-RPC shape, so any XRPL client that speaks rippled's HTTP API
works unchanged:
{ "method": "account_info", "params": [ { "account": "rD86YcZ35wxoTJEhrtmascwW932h2Qa7yE" } ] }
Answers are rippled-shaped too: {"result": {…, "status": "success"}}.
How an answer is agreed
- The edge reads every provider's newest validated ledger and pins your read to the oldest of them, which every provider has.
- Your request goes to all three providers at that ledger.
- If every answer is identical, it is returned. The only fields removed are the
server-specific ones (echoed
request, pagelimit/marker,warnings).
The providers are xrplcluster.com (XRPL Labs, rippled), s2.ripple.com (Ripple, Clio) and honeycluster.io (Clio): three operators, three networks.
Every agreed answer carries headers you can check:
| header | value |
|---|---|
X-Subfrost-Quorum | 3/3 (providers that agreed / providers asked) |
X-Subfrost-Quorum-Providers | xrplcluster.com,s2.ripple.com,honeycluster.io |
X-Subfrost-Ledger-Index | the validated ledger the answer is from |
X-Subfrost-Ledger-Hash | its hash, when the answer names one |
They are exposed to browsers (Access-Control-Expose-Headers).
Only validated ledgers
Omit ledger_index, or send "validated", to read the newest ledger every
provider has validated. A number works if it is not newer than that.
"current" and "closed" are refused (notValidated): those ledgers differ
from server to server, so there is nothing to agree on. ledger_hash is refused
too; send the index instead.
Methods
| method | notes |
|---|---|
account_info | balance, Sequence, OwnerCount, Flags, account_flags. An unfunded or deleted account is actNotFound at the pinned ledger, naming the account and the ledger (see Missing accounts and objects). queue is not served. |
account_lines | the complete list of trust lines in one answer (up to 2,000). Use peer to narrow it. marker is not accepted. |
account_objects | the complete list (up to 2,000); type and deletion_blockers_only work. |
account_tx | limit 1–200 (default 50), forward, ledger_index_min / ledger_index_max (-1 = all; the max is capped at the pinned ledger), api_version 1 or 2. Page with the marker the answer returns (this endpoint's own marker; pass it back unchanged). binary is not served. |
tx | by hash. Returned only once every provider has it in a validated ledger. Otherwise you get quorumPending: retry after a few seconds. Unknown everywhere: txnNotFound. |
ledger | header, optionally transactions / expand. |
ledger_entry | any selector, e.g. {"deposit_preauth": {"owner": "r…", "authorized": "r…"}}. A missing object is entryNotFound, naming what was asked and the ledger (see below). |
server_info | info.validated_ledger (seq, hash, base_fee_xrp, reserve_base_xrp, reserve_inc_xrp) from the agreed ledger and its fee settings, plus load_factor, the highest any provider reports. Nothing else from a single server is included. |
submit | a signed tx_blob, sent to all three providers; one acceptance is enough. The answer lists each provider's verdict in submitted_to. Sign-and-submit (secret, seed, tx_json) is refused: never send a secret to any API. |
ping | liveness. |
Missing accounts and objects
"This account (or object) does not exist" is an answer like any other: all three
providers must say it at the same validated ledger. So that you can tie it to
what you asked, an agreed actNotFound (account_info, account_lines,
account_objects, account_tx) or entryNotFound (ledger_entry) carries:
| field | value |
|---|---|
account | the address you asked about, taken from your request after validation (never from a provider). For ledger_entry, present with an account_root selector. |
index | ledger_entry only: your index (uppercase), or the AccountRoot index of your account_root |
ledger_index | the validated ledger it was read at (for account_tx, ledger_index_max) |
ledger_hash | that ledger's hash, agreed by all three providers |
validated | true |
request | your request echoed, as rippled does: your params plus command |
{ "method": "account_info", "params": [ { "account": "rf1Z17eJnPCaSuxnCCjFQbj5DD1uffxN6z" } ] }
{
"result": {
"status": "error",
"error": "actNotFound",
"error_code": 19,
"error_message": "Account not found.",
"account": "rf1Z17eJnPCaSuxnCCjFQbj5DD1uffxN6z",
"ledger_index": 107663945,
"ledger_hash": "F64C2129C4BB407692236C51B8D60D56D69577113596A4A75BE94E0E6AD506E4",
"validated": true,
"request": { "command": "account_info", "account": "rf1Z17eJnPCaSuxnCCjFQbj5DD1uffxN6z" }
}
}
The X-Subfrost-Quorum: 3/3, X-Subfrost-Ledger-Index and
X-Subfrost-Ledger-Hash headers are set on these answers too. Successful
answers are unchanged.
Addresses are checked first
Every address in a request (account, peer, account_root, and account,
owner, authorized, destination, accounts inside a ledger_entry
selector) must be a classic r… address with a valid checksum. Anything else,
including public keys, seeds and X-addresses, is refused with actMalformed
(HTTP 400) before any provider is asked.
Common checks
Deposit status of an address D:
{ "method": "account_tx", "params": [ { "account": "D…", "api_version": 2, "limit": 20 } ] }
lists payments into D (with meta.delivered_amount) and, once it has been
swept, the AccountDelete. After the sweep, account_info on D answers
actNotFound.
Can a destination receive a payment? Read account_info on it and check
account_data.Flags:
| flag | bit | meaning |
|---|---|---|
lsfRequireDestTag | 0x00020000 | a destination tag is required |
lsfDepositAuth | 0x01000000 | only preauthorized senders: check ledger_entry deposit_preauth |
lsfDisallowXRP | 0x00080000 | the owner asks not to be sent XRP |
A destination that answers actNotFound exists only once it is sent at least the
base reserve (server_info → reserve_base_xrp). Trust lines, for issued
currencies, are in account_lines.
Errors
{ "result": { "status": "error", "error": "quorumDisagree", "error_message": "…", "request": { … } } }
error | HTTP | what to do |
|---|---|---|
quorumMissing | 503 | a provider did not answer; retry (Retry-After) |
quorumRateLimited | 503 | a provider is rate limiting; retry |
quorumPending | 503 | not yet validated everywhere (tx); retry in a few seconds |
quorumDisagree | 500 | providers disagree; nothing is returned |
notValidated | 400 | current / closed asked for |
lgrNotFound | 400 | ledger newer than the newest validated by every provider, or older than full history |
actMalformed | 400 | an address in the request is not a valid classic r… address |
invalidParams, tooManyEntries, unknownCmd | 400 | the request itself |
rippled's own answers (txnNotFound, and a submit's engine_result) come back
as rippled returns them. actNotFound and entryNotFound come back with the
account and ledger added, as described above.