Skip to main content

Migrate from Horizon to RPC

Applications using Horizon's REST-like API will need to be updated to use the RPC JSON-RPC API when migrating from Horizon to RPC. This guide provides an overview of the key differences between the two APIs and how to migrate your application.

Request / Response Format

Horizon's REST-like API uses HTTP methods and status codes to communicate with clients. Responses are JSON in the HAL format. See Horizon's Response Format.

RPC's JSON-RPC API uses JSON-RPC 2.0 to communicate with clients. Requests to the API are JSON objects that contain one or more method invocations. Responses are also JSON objects that contain a result for each invocation in the request. See JSON-RPC.

Both formats utilise JSON for the overall structure which are relatively simple and do not require any special client code, although there are client SDKs available. Some values contained within are XDR encoded and can be decoded using Stellar SDKs.

Response Differences

The endpoint mapping below pairs each Horizon endpoint with an RPC method, but the two responses do not have the same shape. Horizon curates the ledger for you. RPC hands you the ledger entry as the protocol stores it. Plan for these differences before you swap one call for the other.

ConcernHorizonRPC
ShapeOne aggregated resource per endpointOne ledger entry for each requested key that exists
AmountsDecimal string with seven places, such as "1252.7872975"Signed 64-bit integer of stroops, sent as a string, such as "12527872975"
EncodingParsed JSON fieldsBase64 XDR, or JSON when you pass xdrFormat
Flags and thresholdsNamed booleans and named thresholdsA raw uint32 bitmask and a four-byte hex string
Entry not foundHTTP 404HTTP 200, and the key is absent from entries, which is empty when no key exists
CollectionsHAL _links and a paging_token for pagingNo paging, and a limit of 200 keys per call

The RPC column describes the decoded ledger entry. A default response holds those fields inside the base64 XDR, so you see them only after you decode it, or after you ask for the JSON view with xdrFormat.

Example: Account Balances

Horizon's GET /accounts/{address} response is an aggregation. It joins the account entry with the account's trustline entries. The AccountEntry structure holds no trustlines of its own. It holds only a count of the account's sub-entries. So one Horizon call becomes one getLedgerEntries call with an account key plus one trustline key per asset, and you must already know each asset.

The example below uses a Testnet account. A Testnet data reset clears it, so use your own account and asset to follow along. This Horizon request returns both balances at once:

curl "https://horizon-testnet.stellar.org/accounts/GA2242THTLFWPCW2SF3TTLDWJ3C543UUNFTFIMDJUZYXNW2EMEAXZBRH"

Horizon answers with one resource. The response below is trimmed to the fields this comparison uses:

{
"subentry_count": 1,
"thresholds": {
"low_threshold": 0,
"med_threshold": 0,
"high_threshold": 0
},
"flags": {
"auth_required": false,
"auth_revocable": false,
"auth_immutable": false,
"auth_clawback_enabled": false
},
"balances": [
{
"balance": "1252.7872975",
"limit": "922337203685.4775807",
"is_authorized": true,
"asset_type": "credit_alphanum4",
"asset_code": "USDC",
"asset_issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
},
{
"balance": "9009.2998203",
"asset_type": "native"
}
],
"signers": [
{
"weight": 1,
"key": "GA2242THTLFWPCW2SF3TTLDWJ3C543UUNFTFIMDJUZYXNW2EMEAXZBRH",
"type": "ed25519_public_key"
}
]
}

The same data over RPC takes two keys. The first key is the account, the second is the USDC trustline. See Building ledger keys for how to build each one.

{
"jsonrpc": "2.0",
"id": 1,
"method": "getLedgerEntries",
"params": {
"keys": [
"AAAAAAAAAAA1rmpnmstnitqRdzmsdk7F3m6UaWZUMGmmcXbbRGEBfA==",
"AAAAAQAAAAA1rmpnmstnitqRdzmsdk7F3m6UaWZUMGmmcXbbRGEBfAAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3lay"
],
"xdrFormat": "json"
}
}

RPC answers with one entry for each key it finds. Each entry repeats the key it answers, in the keyJson field. This is the result object of that response, trimmed the same way:

{
"entries": [
{
"keyJson": {
"account": {
"account_id": "GA2242THTLFWPCW2SF3TTLDWJ3C543UUNFTFIMDJUZYXNW2EMEAXZBRH"
}
},
"dataJson": {
"account": {
"balance": "90092998203",
"num_sub_entries": 1,
"flags": 0,
"thresholds": "01000000",
"signers": []
}
},
"lastModifiedLedgerSeq": 1397061
},
{
"keyJson": {
"trustline": {
"account_id": "GA2242THTLFWPCW2SF3TTLDWJ3C543UUNFTFIMDJUZYXNW2EMEAXZBRH",
"asset": {
"credit_alphanum4": {
"asset_code": "USDC",
"issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
}
}
}
},
"dataJson": {
"trustline": {
"balance": "12527872975",
"limit": "9223372036854775807",
"flags": 1
}
},
"lastModifiedLedgerSeq": 3657583
}
],
"latestLedger": 4561910
}

Both responses describe the same account. Read them side by side:

  • Horizon's native balance of 9009.2998203 is the account entry's balance of 90092998203 stroops.
  • Horizon's USDC balance of 1252.7872975 is the trustline entry's balance of 12527872975 stroops. It is a separate entry, so it needs a separate key.
  • Horizon's limit of 922337203685.4775807 is the trustline entry's limit of 9223372036854775807, the largest value a 64-bit amount can hold.
  • Horizon's thresholds object splits the entry's four-byte thresholds string. That string is [weight of master|low|medium|high], so "01000000" means a master key weight of 1 and no other threshold set.
  • Horizon's signers array adds the master key, which the raw account entry never lists.
  • Horizon's flags booleans expand the entry's flags bitmask. The trustline's flags value of 1 is AUTHORIZED_FLAG, which Horizon reports as is_authorized.
note

Pass "xdrFormat": "json" to read an entry without an SDK, as the responses above do. Do not build an application on that JSON. The getLedgerEntries specification warns that its shape changes whenever the underlying XDR changes. Decode the default base64 XDR with one of the SDKs instead.

Amounts Are Raw Integers

Every classic amount on the ledger is a signed 64-bit integer of stroops. Horizon divides it by ten million and returns a string with seven decimal places. RPC returns the stroop count itself. It is a JSON string, not a JSON number, because a 64-bit integer does not fit a JSON number safely. Divide it in your own code, and use a big-number type so you do not lose precision. See Amount precision.

A Missing Entry Is Not an Error

Horizon answers 404 when a resource does not exist. RPC answers 200 and leaves the key out of entries, so a request for two keys can return one entry. If no key exists, entries is empty. The specification requires only latestLedger in the result, so read entries defensively.

Match each returned entry back to its own key with the entry's keyJson field, or key in the default base64 format. Do not match on the position you sent it in.

Endpoint Mapping

Applications that use the following Horizon endpoints can typically migrate directly to the RPC using the referenced methods.

Endpoints without mappings do not have a direct replacement in the RPC API. To build similar functionality in an application, please consider partnering with an indexer or using the information listed below to build your own indexed representation of horizon endpoints. Consider using other Data products for analytics use cases.

Horizon EndpointCorresponding RPC Method(s)Indexer EquivalentAnalytics Resources
GET /getLatestLedger getVersionInfo getHealth getNetworkNot applicableAnalyst Guide
GET /ledgersgetLedgersCreate a getLedgers with full history, using Galexie, to build a historical ledger view.Ledgers
GET /ledgers/{seq}getLedgers (with filter for sequence)Use a getLedgers view with full history, filtered by ledger sequenceLedgers
GET /ledgers/{seq}/transactionsgetTransactions (with filter for ledger sequence)Use getTransactions with ledger sequence filtering to retrieve transactions for a specific ledgerTransactions
GET /ledgers/{seq}/operationsgetTransactionsUse getTransactions for the given ledger, then parse the transaction's XDR for individual operations.Operations
GET /ledgers/{seq}/paymentsgetEvents getTransactions ⚠️Use getEvents (CAP-67 asset events, available since Protocol 23 on nodes that emit classic events) and parse getTransactions meta XDR to tie those events back to individual operations.Payments
GET /ledgers/{seq}/effectsgetEvents getTransactions ⚠️Use getEvents for the effects CAP-67 models (asset movement and trustline authorization), and parse getTransactions meta XDR for every other effect type.Effects
POST /transactionssendTransactionNot applicableNot applicable
POST /transactions_asyncsendTransactionNot applicableNot applicable
GET /transactionsgetTransactionsUse getTransactions to build a historical transaction listTransactions
GET /transactions/{hash}getTransactionUse getTransaction to retrieve a specific transaction by its hashTransactions
GET /transactions/{hash}/operationsgetTransactionFilter getTransaction by hash and then parse its XDR for operationsOperations
GET /transactions/{hash}/paymentsNo direct RPC equivalent; use getEvents or parse getTransactionsFilter getTransaction by hash and analyze its events and operation types to identify payments.Payments
GET /transactions/{hash}/effectsNo direct RPC equivalent; use getEvents or parse getTransactionsFilter getTransaction by hash and analyze its events and metadata for relevant data.Effects
GET /operationsgetTransactionsIngest all historical ledgers/transactions and build and build a view of operations for filtering.Operations
GET /operations/{id}No direct RPC equivalentStore Horizon's operation ID and map it back to the ledger sequence and transaction index to retrieve the relevant transaction via getTransactions.Operations
GET /operations/{id}/effectsNo direct RPC equivalentRetrieve the operation by ID (as above) and then parse its associated transaction and events for effects.Effects
GET /fee_statsgetFeeStats simulateTransactionIndexed data not recommended.Fee Stats
GET /accountsNo direct RPC equivalentIngest all ledger history via getLedgers to build and maintain a complete list of accounts.Accounts
GET /accounts/{address}getLedgerEntriesUse getLedgerEntries for a specific account address. Note: RPC will not provide trustline information associated with the account directly, as Horizon does. You will need to derive this from ledger entries. See Example: Account Balances.Accounts
GET /claimable_balancesNo direct RPC equivalentIngest all ledger history via getLedgers to build and maintain a complete list of claimable balances.Claimable Balances
GET /claimable_balances/{id}getLedgerEntriesUse getLedgerEntries to retrieve a specific claimable balance by ID.Claimable Balances
GET /claimable_balances/{id}/transactionsNo direct RPC equivalentTrace transactions that interact with the specific claimable balance ID from their historical ledger data.Claimable Balances
GET /claimable_balances/{id}/operationsNo direct RPC equivalenttrace operations related to the specific claimable balance ID from your historical ledger data.Claimable Balances
GET /liquidity_poolsNo direct RPC equivalentingest all ledger history via getLedgers to build and maintain a complete list of liquidity pools.Liquiditity Pools
GET /liquidity_pools/{id}getLedgerEntriesUse getLedgerEntries to retrieve a specific liquidity pool by ID.Liquiditity Pools
GET /liquidity_pools/{id}/transactionsNo direct RPC equivalentTrace transactions that interact with the specific liquidity pool ID from their historical ledger data.Liquiditity Pools
GET /liquidity_pools/{id}/operationsNo direct RPC equivalentTrace operations related to the specific liquidity pool ID from their historical ledger data.Liquiditity Pools
GET /liquidity_pools/{id}/effectsNo direct RPC equivalentTrace effects related to the specific liquidity pool ID from their historical ledger data.Liquiditity Pools
GET /liquidity_pools/{id}/tradesNo direct RPC equivalentInfer trades related to the specific liquidity pool ID from historical ledger data (e.g., from getEvents and getTransactions metadata).Liquiditity Pools
GET /offersNo direct RPC equivalentIngest all ledger history via getLedgers to build and maintain a complete list of offers.Offers
GET /offers/{id}getLedgerEntriesUse getLedgerEntries to retrieve a specific offer by ID.Offers
GET /offers/{id}/tradesNo direct RPC equivalentInfer trades related to the specific offer ID from historical ledger data (e.g., from getEvents and getTransactions metadata).Offers
GET /paymentsgetEvents getTransactions ⚠️Use getEvents (CAP-67 asset events) and process getTransactions metadata for payment-like operations across all history.Payments
GET /effectsgetEvents getTransactions ⚠️Use getEvents for the effects CAP-67 models, and process getTransactions metadata to derive the remaining effect types across all history.Effects
GET /tradesgetEvents getTransactions ⚠️Infer trades from getEvents and getTransactions metadata across all history, as there is no direct "trade event" in RPC.Trades
tip

The getTransactions method can be used to retrieve events batched by transaction. The events are contained in the meta XDR of the transaction (field resultMetaXdr).

warning

The getEvents method is not a direct replacement for Horizon's endpoints.

Since CAP-67 shipped in Protocol 23, the method can return events from classic operations as well as from contracts. Those unified asset events are emitted as transfer, mint, burn, clawback, fee, and set_authorized, so a single stream can cover the movement of assets and changes to trustline authorization.

Classic events are not on by default. They are only present if the Stellar Core instance backing the RPC runs with EMIT_CLASSIC_EVENTS=true. If you also need to cover ledgers from Protocol 22 and earlier, that additionally requires BACKFILL_STELLAR_ASSET_EVENTS=true. If you run your own RPC, set what your history range needs before relying on this mapping. If you use a provider, confirm with them. See Events for more detail.

That is still narrower than Horizon's effects. Anything CAP-67 does not model, such as signer updates, data entry changes, sequence number bumps, and offer management, has to come from the meta XDR of the transaction. The getTransactions method returns that meta XDR (field resultMetaXdr), which also contains events from contracts.