Using GraphQL RPC
This guide provides practical examples for querying the Sui network using GraphQL. For core concepts like headers, variables, fragments, pagination, scope, and limits, see the corresponding concepts page.
Access the GraphQL service for Sui RPC through an online IDE that provides a complete toolbox for fetching data and executing transactions on the network:
- Mainnet: https://graphql.mainnet.sui.io/graphql
- Testnet: https://graphql.testnet.sui.io/graphql
- Devnet: https://graphql.devnet.sui.io/graphql
Both Mainnet and Testnet services are rate-limited to keep network throughput optimized. These services are provided as a public good from Sui Foundation and are not meant for production-grade use. Refer to the list of RPC or data providers that have enabled gRPC on their full nodes.
The online IDE provides features such as auto-completion (use Ctrl+Space or just start typing), built-in documentation (Book icon, top-left), multi-tabs, and more. You can try the example queries on this page directly in the IDE.
Supported schema
GraphQL introspection exposes the schema supported by the RPC service. The IDE's Docs pane (Book icon, top-left) and Search dialog (Cmd + K on macOS or Ctrl + K on Windows and Linux) offer a way to browse introspection output interactively.
Consult the GraphQL reference for full documentation on the supported schema. The official GraphQL introspection documentation provides an overview on introspection and how to interact with it programmatically.
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 } }"
}'
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:
pageInfoof 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 (TransactionBlockin the previous example).edges: Similar tonodesbut 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: 3would selectR0,R1,R2. - A limit of
last: 3would selectR7,R8,R9.
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
}
}
}
}
}
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.