Skip to main content

GraphQL for Sui RPC (Beta)

⚙️Early-Stage Feature

This content describes an alpha/beta feature or service. These early stage features and services are in active development, so details are likely to change.

This feature or service is currently available in

  • Devnet
  • Testnet
  • Mainnet

GraphQL provides a flexible and powerful way to query the Sui network. This page covers the core concepts you need to understand when working with GraphQL for Sui RPC, including request headers, query composition with variables and fragments, pagination strategies, query scope, and service limits.

For practical examples and a quick start guide, see Querying Sui RPC with GraphQL.

For comprehensive GraphQL fundamentals, consult the introductory documentation from GraphQL and GitHub.

info

Refer to Access Sui Data for an overview of options to access Sui network data.

The GraphQL RPC release stage is currently in beta. Refer to the high-level timeline for releases.

caution

Switch any apps that still use the GraphQL Alpha endpoints (https://sui-mainnet.mystenlabs.com/graphql, https://sui-testnet.mystenlabs.com/graphql, and https://sui-devnet.mystenlabs.com/graphql) to the GraphQL Beta endpoints as soon as possible to avoid disruption of service.

  • GraphQL Beta Mainnet: https://graphql.mainnet.sui.io/graphql
  • GraphQL Beta Testnet: https://graphql.testnet.sui.io/graphql
  • GraphQL Beta Devnet: https://graphql.devnet.sui.io/graphql

Headers

The service accepts the following optional HTTP request headers:

  • x-sui-rpc-version: Specifies which RPC version to use. Currently only one version is supported.
  • x-sui-rpc-show-usage: Returns the response with extra query complexity information.

By default, each response contains the following HTTP response headers:

  • x-sui-rpc-request-id: A unique identifier for the request. This appears in application logs for debugging.
  • x-sui-rpc-version: The version of the service that handled the request.
$ curl -i -X POST https://graphql.testnet.sui.io/graphql\
--header 'x-sui-rpc-show-usage: true' \
--header 'Content-Type: application/json' \
--data '{
"query": "query { epoch { referenceGasPrice } }"
}'
Click to open
Output
HTTP/2 200
content-type: application/json
content-length: 179
x-sui-rpc-request-id: f5442058-47ab-4360-8295-61c009f38516
x-sui-rpc-version: 1.56.1-
vary: origin, access-control-request-method, access-control-request-headers
access-control-allow-origin: *
date: Tue, 09 Sep 2025 23:34:04 GMT
via: 1.1 google
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

{
"data": {
"epoch": {
"referenceGasPrice": "1000"
}
},
"extensions": {
"usage": {
"input": {
"nodes": 2,
"depth": 2
},
"payload": {
"query_payload_size": 67,
"tx_payload_size": 0
},
"output": {
"nodes": 2
}
}
}
}

Variables

Variables offer a way to introduce dynamic inputs to a re-usable or static query. Declare variables in the parameters to a query or mutation, using the $ symbol and its type (in this example Int), which must be a scalar, enum, or input type. In the query body, refer to it by its name prefixed with the $ symbol.

If you declare a variable but don't use it, or define it in the query, the query fails to execute. To learn more, read the GraphQL documentation on variables.

In the following example, a variable supplies the ID of the epoch being queried:

query ($epochID: Int) {
epoch(id: $epochID) {
referenceGasPrice
}
}

Variables:

{
"epochID": 100
}

Within the GraphQL IDE

When using the GraphQL online integrated development environment (IDE), supply variables as a JSON object to the query in the Variables pane at the bottom of the main editing window. You receive a warning if you supply a variable but don't declare it.

Within requests

When making a request to the GraphQL service using a tool such as curl, pass the query and variables as two fields of a single JSON object:

$ curl -X POST https://sui-testnet.mystenlabs.com/graphql \
--header 'Content-Type: application/json' \
--data '{
"query": "query ($epochID: Int) { epoch(id: $epochID) { referenceGasPrice } }",
"variables": { "epochID": 100 }
}'

Fragments

Fragments are reusable units that you can include in queries as needed. To learn more, consult the official GraphQL documentation. The following example uses fragments to factor out a reusable snippet representing a Move value:

query DynamicField {
object(
address: "0xb57fba584a700a5bcb40991e1b2e6bf68b0f3896d767a0da92e69de73de226ac"
) {
dynamicField(
name: {
type: "0x2::kiosk::Lock",
bcs: "NLArx1UJguOUYmXgNG8Pv8KbKXLjWtCi6i0Yeq1Vhfw=",
}
) {
...DynamicFieldSelect
}
}
}

fragment DynamicFieldSelect on DynamicField {
name {
...MoveValueFields
}
value {
...DynamicFieldValueSelection
}
}

fragment DynamicFieldValueSelection on DynamicFieldValue {
__typename
... on MoveValue {
...MoveValueFields
}
... on MoveObject {
hasPublicTransfer
contents {
...MoveValueFields
}
}
}

fragment MoveValueFields on MoveValue {
type {
repr
}
json
bcs
}

Pagination

GraphQL supports queries that fetch multiple kinds of potentially nested data. For example, the following query retrieves the first 10 transactions in epoch 97 along with the digest, sender's address, gas object returned after paying for the transaction, gas price, and gas budget:

query {
epoch(epochId: 97) {
transactions(first: 10) {
pageInfo {
hasNextPage
endCursor
}
nodes {
digest
sender {
address
}
effects {
gasEffects {
gasObject {
address
}
}
}
gasInput {
gasPrice
gasBudget
}
}
}
}
}

If there are too many transactions to return in a single response, the service applies a limit on the maximum page size for variable size responses (like the transactionBlock query) and you must fetch further results through pagination.

Connections

Fields that return a paginated response accept the following optional parameters:

  • first: Limit on page size that is met by dropping excess results from the end.
  • after: Cursor that bounds the results from below, exclusively.
  • last: Limit on page size that is met by dropping excess results from the start.
  • before: Cursor that bounds the results from above, exclusively.

They also return a type that conforms to the GraphQL Cursor Connections Specification, meaning its name ends in Connection, and it contains at minimum the following fields:

  • pageInfo of type PageInfo, which indicates whether there are more pages before or after the page returned.
  • nodes: The content of the paginated response as a list of the type being paginated (TransactionBlock in the previous example).
  • edges: Similar to nodes but associating each node with its cursor.

Cursors

Cursors are opaque identifiers for paginated results. The only valid source for a cursor parameter (like after and before) is a cursor field from a previous paginated response (like PageInfo.startCursor, PageInfo.endCursor, or Edge.cursor). The underlying format of the cursor is an implementation detail, and is not guaranteed to remain fixed across versions of the GraphQL service, so do not rely on it. Generating cursors out of thin air is not expected or supported.

Cursors are used to bound results from below (with after) and above (with before). In both cases, the bound is exclusive, meaning it does not include the result that the cursor points to in the bounded region.

Consistency

Cursors for queries that paginate live objects also guarantee consistent pagination. They encode the checkpoint at which the query was first executed so that later pages are scoped by the same checkpoint, even if newer checkpoints are available. If both an after and a before cursor are provided, they must both be from the same checkpoint, otherwise the query produces an error.

By default, RPCs offer roughly 1 hour of retention for consistent pagination.

Page limits

After results are bounded using cursors, a page size limit is applied using the first and last parameters. The service requires these parameters to be less than or equal to the max page size limit, and if you provide neither, it selects a default. In addition to setting a limit, first and last control where excess elements are discarded from. For example, if there are 10 potential results (R0, R1, through R9) after cursor bounds have been applied, then:

  • A limit of first: 3 would select R0, R1, R2.
  • A limit of last: 3 would select R7, R8, R9.
info

It is an error to apply both a first and a last limit.

Scope

GraphQL requests are evaluated in a scope that controls the checkpoint being viewed. The GraphQL service responds to queries as if this is the last checkpoint to have been executed. By default, this is set to the latest checkpoint that the service has all data for. To avoid returning partial responses, the service does not allow queries to specify a later checkpoint than this, but it can be set to an earlier checkpoint to perform historical queries.

Optionally, the scope provides a root object bound that applies only to queries that fetch dynamic fields. The query fetches dynamic fields as they existed at the end of a specific checkpoint or when their root object reached a given version. For any wrapped or child (object-owned) object, the root object is defined recursively as:

  • The root object of the object it is wrapped in, if it is wrapped
  • The root object of its owner, if it is owned by another object
  • The object itself

If a dynamic field's root object has version v, its own version, w is the latest version such that w <= v meaning the latest version of the dynamic field that existed when its root object was at version v.

If a root object bound is not provided, dynamic fields are fetched at the checkpoint being viewed, while if a checkpoint-based root object bound exists, it does not impact the checkpoint being viewed.

Finally, the GraphQL service treats queries nested under executed Mutation.executeTransaction and simulated Query.simulateTransaction transactions as being in a special scope that exists just after the transaction that was executed or simulated, without having indexed that transaction.

The scope a query is evaluated in impacts which fields are available. In particular:

  • Live object set queries are not available under executed transaction scopes or when a root object binding specifies a particular version. These queries rely on data that is indexed at the checkpoint level.
  • Queries that paginate through history are not available under executed transaction scopes. Before indexing occurs, the system cannot determine where in the history the transaction falls.

Setting checkpoint scope

You can run queries against a historical checkpoint using Checkpoint.query. For consistent live object set queries (such as fetching an address' owned objects or balances, or an object's dynamic fields), you can continue to paginate using a cursor obtained from a previous query at that checkpoint.

query AtCheckpoint($cp: UInt53!) {
checkpoint(sequenceNumber: $cp) {
query {
transactions(last: 5) {
nodes {
digest
}
}
}
}
}

Setting root version scope

Queries nested under the fetch of an object at a specific version are scoped by a root object bound at its version. For example, in the following query, the dynamic field with name 42u64 is fetched as it existed when its owning object was at version $v:

query ObjectsDynamicFields($id: SuiAddress!, $v: UInt53!) {
object(address: $id, version: $v) {
dynamicField(name: { literal: "42u64" }) {
value {
... on MoveValue {
json
}
}
}
}
}

This property applies recursively to nested dynamic field queries. It also applies if the root object is fetched at a specific checkpoint using the atCheckpoint parameter or at the latest checkpoint (by omitting all parameters). In these cases, the root object bound is checkpoint-based.

The rootVersion parameter overrides this implicit bound. It fetches the object as if it was subject to a root version bound governed by the parameter, which also applies to queries nested underneath it. This is necessary when fetching a dynamic field directly (as an object), because an object's version is not updated when its children are updated unless it is a root object.

While nested within a scope that has a root object bound, you can reset or override the bound using the objectAt or addressAt queries. These fields query the same entity but at a different position in its history. If these fields are not provided any parameters, they fetch the state of the object at the latest known checkpoint to the GraphQL service.

For more examples of scope usage, see Querying Sui RPC with GraphQL.

Scope

GraphQL requests are evaluated in a scope that controls the checkpoint being viewed. The GraphQL service responds to queries as if this is the last checkpoint to have been executed. By default, this is set to the latest checkpoint that the service has all data for. To avoid returning partial responses, the service does not allow queries to specify a later checkpoint than this, but it can be set to an earlier checkpoint to perform historical queries.

Optionally, the scope provides a root object bound that applies only to queries that fetch dynamic fields. The query fetches dynamic fields as they existed at the end of a specific checkpoint or when their root object reached a given version. For any wrapped or child (object-owned) object, the root object is defined recursively as:

  • The root object of the object it is wrapped in, if it is wrapped
  • The root object of its owner, if it is owned by another object
  • The object itself

If a dynamic field's root object has version v, its own version, w is the latest version such that w <= v meaning the latest version of the dynamic field that existed when its root object was at version v.

If a root object bound is not provided, dynamic fields are fetched at the checkpoint being viewed, while if a checkpoint-based root object bound exists, it does not impact the checkpoint being viewed.

Finally, the GraphQL service treats queries nested under executed Mutation.executeTransaction and simulated Query.simulateTransaction transactions as being in a special scope that exists just after the transaction that was executed or simulated, without having indexed that transaction.

The scope a query is evaluated in impacts which fields are available. In particular:

  • Live object set queries are not available under executed transaction scopes or when a root object binding specifies a particular version. These queries rely on data that is indexed at the checkpoint level.
  • Queries that paginate through history are not available under executed transaction scopes. Before indexing occurs, the system cannot determine where in the history the transaction falls.

Setting checkpoint scope

You can run queries against a historical checkpoint using Checkpoint.query. For consistent live object set queries (such as fetching an address' owned objects or balances, or an object's dynamic fields), you can continue to paginate using a cursor obtained from a previous query at that checkpoint.

query AtCheckpoint($cp: UInt53!) {
checkpoint(sequenceNumber: $cp) {
query {
transactions(last: 5) {
nodes {
digest
}
}
}
}
}

query NextBalancesPage($address: SuiAddress!, $after: String!) {
address(address: $address) {
balances(after: $after, first: 10) {
pageInfo {
hasNextPage
endCursor
}
nodes {
objectId
balance
}
}
}
}

AtCheckpoint returns the last 5 transactions to execute as of the end of the checkpoint with sequence number $cp, while NextBalancesPage fetches the next 10 balances for $address after the page that ended at cursor $after. You can also combine both approaches, to ensure that the first page of a live object query is fetched from a specific checkpoint:

query FirstBalancesPageAtCheckpoint($address: SuiAddress!, $cp: UInt53) {
checkpoint(sequenceNumber: $cp) {
query {
address(address: $address) {
balances(first: 10) {
pageInfo {
hasNextPage
endCursor
}
nodes {
objectId
balance
}
}
}
}
}
}

Responses to these queries are subject to retention. If $cp is outside the retention window Query.transactions, AtCheckpoint returns no results, while an attempt to continue paginating NextBalancesPage at a checkpoint outside the consistent range returns an error.

Setting root version scope

Queries nested under the fetch of an object at a specific version are scoped by a root object bound at its version. For example, in the following query, the dynamic field with name 42u64 is fetched as it existed when its owning object was at version $v:

query ObjectsDynamicFields($id: SuiAddress!, $v: UInt53!) {
object(address: $id, version: $v) {
dynamicField(name: { literal: "42u64" }) {
value {
... on MoveValue {
json
}
}
}
}
}

This property applies recursively to nested dynamic field queries. It also applies if the root object is fetched at a specific checkpoint using the atCheckpoint parameter or at the latest checkpoint (by omitting all parameters). In these cases, the root object bound is checkpoint-based.

The rootVersion parameter overrides this implicit bound. It fetches the object as if it was subject to a root version bound governed by the parameter, which also applies to queries nested underneath it. This is necessary when fetching a dynamic field directly (as an object), because an object's version is not updated when its children are updated unless it is a root object.

In the following query, $id is the address of a dynamic field fetched as it existed when its root object was at version $r. The nested query fetches a nested dynamic field with name 42u64 owned by a wrapped object stored in field foo of the dynamic field. The nested fetch is subject to the same root object version bound, $r.

query NestedDynamicFields($id: SuiAddress!, $r: UInt53!) {
object(address: $id, rootVersion: $v) {
asMoveObject {
asDynamicField {
value {
... on MoveValue {
extract(path: "foo->[42u64]") {
json
}
}
}
}
}
}
}

Queries that return an Address can be used to make nested dynamic field queries on wrapped objects. An Address does not have an associated version, but Query.address can accept rootVersion or atCheckpoint parameters to set root object bounds for nested dynamic field queries. The following query fetches a dynamic field with name 42u64 owned by an object with address $id (which can belong to a wrapped object) as it existed when its root object was at version $r:

query WrappedDynamicField($id: SuiAddress!, $r: UInt53!) {
address(address: $id, rootVersion: $r) {
dynamicField(name: { literal: "42u64" }) {
value {
... on MoveValue {
json
}
}
}
}
}

While nested within a scope that has a root object bound, you can reset or override the bound using the objectAt or addressAt queries. These fields query the same entity but at a different position in its history. If these fields are not provided any parameters, they fetch the state of the object at the latest known checkpoint to the GraphQL service. The following query fetches the latest version of an object that was previously fetched at version $v:

query LatestDynamicField($id: SuiAddress!, $v: UInt53!) {
object(address: $id, version: $v) {
objectAt {
version
}
}
}

Limits

The GraphQL service for Sui RPC is rate-limited on all available instances to keep network throughput optimized and to protect against excessive or abusive calls to the service.

Rate limits

Queries are rate-limited at the number of attempts per minute to ensure high availability of the service to all users.

Query limits

In addition to rate limits, queries are also validated against a number of rules on their complexity, such as the number of nodes, the depth of the query, or their payload size. Query the serviceConfig field to retrieve these limits. An example of how to query for some of the available limits follows:

{
serviceConfig {
maxQueryDepth
maxQueryNodes
maxOutputNodes
defaultPageSize(type: "Query", field: "transactions")
maxPageSize(type: "Query", field: "objects")
queryTimeoutMs
maxQueryPayloadSize
maxTypeArgumentDepth
maxTypeArgumentWidth
maxTypeNodes
maxMoveValueDepth
}
}

Retention

Different queries are subject to their own retention policies. For example, live object set queries work only for recent checkpoints (measured in minutes or hours), while transaction pagination might be available only back to certain checkpoints (measured in weeks or months), depending on the filters applied. Data outside these ranges is pruned. You can query the upper and lower bounds of that range for your query as follows:

{
serviceConfig {
retention(type: "Query", field: "transactions", filter: "affectedAddress") {
first { sequenceNumber }
last { sequenceNumber }
}
}
}

The results are the first and last checkpoint for which pagination continues to work and produce a consistent result.

Beta schema changes

Some of the schema updates for the beta version include:

Renames and deprecations

  • TransactionBlock is being replaced with Transaction
  • Owner has been merged with Address
  • dryRunTransactionBlock is being replaced with simulateTransaction
  • bcs fields are qualified with the type of BCS data they hold to avoid confusion.
  • changedObject and inputObject filters in transactions query are being replaced with affectedObject.
  • address.coins and address.stakedSuis are being deprecated in favor of address.objects with the corresponding type filter.
  • MoveValue.data has been deprecated because it does not add value over MoveValue.json, which provides a more clear representation of the data.

Consistency

GraphQL Beta offers the ability to run a query at a fixed checkpoint (retention permitting). This allows clients to query the chain state at a particular snapshot in time over multiple arbitrary requests (GraphQL Alpha offered consistency across multiple requests as long as they were for pages of a paginated query).

Versioning

APIs for traversing different versions of packages and objects have been added. You can now list a package or object's versions, or go from one version of a package or object to another version by number, or the latest version.

Splitting up query APIs

Paginated query APIs were previously responsible for 3 kinds of query pattern, which are broken up into 2 separate APIs:

  • A dedicated multi-get API that accepts a list of keys and returns a list of results.
  • A query API that offers the original core functionality: Efficient paginated results without having to supply a checkpoint range, but for limited combinations of filters.

Relaxed consistency cursors

Cursors in GraphQL Alpha indicated the checkpoint that pagination started at. Future pages would be fetched as if from this checkpoint. This is helpful for paginating owned objects of an address but cannot express queries that update over time (like tracking a stream of events in real time). Cursors in GraphQL Beta have relaxed this constraint — only cursors for pages whose results do not flow in the same direction as time (such as owned object queries) remember their checkpoint.

The old behavior is reproducible by explicitly setting the checkpoint to query from (see the Consistency section).

Preserving entity IDs

The service retains the ability to identify entities that have been pruned when they are referred to by other entities. For example, even if an object's previous transaction has been pruned, the service is still able to return the transaction digest.

Unpruned point lookups

Point look-ups (accessing an object by its ID and version, a transaction by its digest, a checkpoint by its sequence number, and so on) are generally unpruned if GraphQL has access to an Archival Service.

More detailed retention API

The service provides more details about the retention of its underlying data through the ServiceConfig.availableRange field.

Governance

APIs related to governance and staking are removed in favor of using view functions to access the data at its source of truth on-chain.

Owned coin pagination

As there is no longer a dedicated API for fetching the coins owned by an address, whenever coins are returned by an owned object API (such as Query.objects with an owner filter or IAddressable.objects), they are grouped by coin type and sorted in descending balance order.

Dry-run, gas estimation, and coin selection

Dry-run now supports the same input schema as gRPC's SimulateTransaction in JSON form. This means that it can also accept transactions where the inputs have not been fully resolved or serialized, perform gas estimation, and coin selection. The feature is now called simulateTransaction.

Querying Sui RPC with GraphQL

Practical guide to making queries of the Sui RPC using the GraphQL service, with examples for common tasks.

GraphQL for Sui RPC (Beta)

GraphQL is a public service for the Sui RPC that enables you to efficiently interact with the Sui network.

GraphQL and General-Purpose Indexer (Beta)

The GraphQL RPC Beta service offers a structured way for your clients to interact with data on the Sui blockchain. It accesses data processed by a general-purpose indexer and can connect to an archival store for historical network state.

Sui Testnet GraphiQL

Sui GraphiQL IDE for Testnet.

Sui Mainnet GraphiQL

Sui GraphiQL IDE for Mainnet.