Filtering and pagination
Keep queries bounded, preserve cursor scope, and aggregate without raw SQL.
Discover datasets and columns
Use GET /v1/datasets to find available tables. Each dataset has a schema, completed ledger coverage and query routes.
curl --fail-with-body 'https://api.stellaratlas.io/v1/datasets/history_contract_events/schema'
curl --fail-with-body 'https://api.stellaratlas.io/v1/datasets/history_contract_events/records?ledger_sequence=64877689&select=ledger_sequence,contract_id,transaction_hash&limit=2'Column names and types come from the schema endpoint. Use select to choose columns, order=-ledger_sequence for descending order, and the returned nextOffset for the next page. Keep filters unchanged. Metadata uses the shared catalogue; record queries remain bounded.
A full final records page may be followed by one empty page; no total-count scan is run.
Choose an explicit window
Windowed REST entity endpoints use inclusive min_ledger and max_ledger. GraphQL uses minLedger and maxLedger. Optional UTC start_time / end_time filters use an inclusive start and exclusive end; GraphQL calls them startTime / endTime.
Activity lists default to a small recent imported window. For repeatable analysis, supply your own bounds and retain the returned window. Unsupported filters fail validation instead of being interpreted as SQL.
In the explorer, use Date & ledger range for optional bounds. Latest published clears the range but keeps account, asset and operation filters. Use View API response to open the exact query behind the displayed result, including its window and pagination.
Use the pagination contract for the route
- Operations, contracts, trades and offers:
limitfrom 1–100,offsetfrom 0–10,000, and a returnednextOffset. - Asset catalogue (
/v1/assets): opaquecursor, returnednextCursor, andlimitfrom 1–100. Asset discovery does not require a ledger window. - Transfer activity and typed contract events: opaque
aftercursor, returnednextCursor, and a limit up to 200. - Account balances and asset holders: route-scoped
after, returnednextCursor, and a limit up to 200. - Typed transaction relations: separate
operations_after,effects_afterandevents_after. Do not exchange these cursors.
Explorer entity lists offer 10, 25, 50 or 100 rows per page. Apply the selection to start at the first page; copied explorer URLs preserve the page size, filters and returned ledger window.
Copy cursors unchanged and keep the original account, asset, filters and window. They are continuation tokens, not reusable identifiers or signed proofs. Ongoing ingestion and backfill can change later reads; cursor pagination is not a frozen warehouse snapshot.
Select columns and aggregate
POST /v1/analytics/query accepts a structured query against one catalog dataset. Select named columns, apply supported filters, or group with count, count_distinct, sum, avg, min and max. This is a bounded query API, not arbitrary SQL or an asynchronous query-job service.
curl --fail-with-body 'https://api.stellaratlas.io/v1/analytics/query' \
-H 'Content-Type: application/json' \
--data '{"dataset":"history_operations","minLedger":63490360,"maxLedger":63490365,"groupBy":["type_string"],"aggregations":[{"function":"count","alias":"records"}],"orderBy":[{"field":"records","direction":"desc"}],"limit":10,"offset":0}'The dataset-specific equivalent is POST /v1/datasets/{dataset}/query. Existing /v1/analytics routes remain available. Grouped queries return metric metadata alongside rows and coverage. Use returned aliases in ordering; GraphQL uses uppercase function enums such as COUNT.
Handle incomplete or unavailable results
Correct invalid REST inputs on HTTP 400. Some typed explorer detail routes distinguish a missing record in a completed window (404) from a record not found in an incompletely ingested window (409). Other route families have their own documented error shapes.
For GraphQL, inspect the JSON errors array and HTTP status together. HTTP 200 can contain a failed request, including validation or warehouse errors. An error has a message; extensions.code is optional. A missing code is not a reason to retry.
| Response requested with Accept | Tested error responses |
|---|---|
application/json | HTTP 200 for tested syntax, schema, variable, resolver and request-budget errors. |
application/graphql-response+json | HTTP 400 for tested syntax, schema and budget errors; HTTP 200 for variable and resolver errors. |
Unsupported type such as text/plain | HTTP 406 in the tested case. |
Both JSON response types can carry errors at HTTP 200. Read error bodies at HTTP 400 too; do not discard their messages by throwing on status first. Some HTTP failures have an empty or non-JSON body. These tested outcomes do not assign a status to every possible failure.
Run this JavaScript in a modern browser console or Node ES module. It reuses the runner’s payment-operations request and makes one attempt with a ten-second deadline, without automatic retries. Replace its historical window using your coverage response.
const request = {
"query": "query Operations($input: HubbleOperationInput) {\n hubbleOperations(input: $input) {\n rows { id transactionHash sourceAccount type ledgerSequence details }\n window { minLedger maxLedger } coverageStatus semantics limit offset nextOffset\n }\n}",
"variables": {
"input": {
"minLedger": 63490364,
"maxLedger": 63490365,
"limit": 10,
"offset": 0,
"type": "payment"
}
}
};
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
let response;
try {
response = await fetch('https://api.stellaratlas.io/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/graphql-response+json'
},
body: JSON.stringify(request),
signal: controller.signal
});
const responseText = await response.text();
let result = null;
try { result = JSON.parse(responseText); } catch {}
const object = result !== null && typeof result === 'object' && !Array.isArray(result);
if (object && result.errors !== undefined && !Array.isArray(result.errors)) {
throw new Error('Invalid GraphQL errors response');
}
if (object && result.errors?.length) {
console.error('GraphQL errors (HTTP ' + response.status + '):', responseText);
if (result.data != null) {
console.warn('Partial data; keep the previous page and do not advance pagination.');
}
} else if (!response.ok) {
console.error('HTTP ' + response.status + ' without GraphQL errors:', responseText);
} else {
if (!object || !result.data?.hubbleOperations) throw new Error('Missing valid operation page');
console.log('Operation response JSON:', responseText);
}
} catch (error) {
const message = controller.signal.aborted
? 'Request deadline exceeded; the server outcome may be unknown'
: response
? 'Response handling failed (HTTP ' + response.status + ')'
: 'No HTTP response: network or browser request failure';
console.error(message, error);
} finally {
clearTimeout(timeout);
}The example outputs the original response text, preserving numeric strings and literals without reserializing them through JavaScript numbers. Keep exact identifiers and raw amounts as strings. Use a lossless decoder before calculating with large numeric literals in untyped details; source Float64 observations remain approximate.
Errors with partial data are separate from a complete page. Keep an earlier successful page visible and do not advance pagination from an error response. A failed fetch has no HTTP response to inspect; a local deadline does not prove that server work stopped.
For idempotent reads, a deliberately bounded retry policy can handle known transient failures such as SERVICE_UNAVAILABLE, HTTP 503 or a lost connection. Cap attempts and total elapsed time, use backoff with jitter, and honor Retry-After when supplied. Correct syntax, schema, variable and BAD_USER_INPUT errors first. Split or narrow QUERY_TOO_COMPLEX requests instead of retrying unchanged. When the cause is unclear, surface the error rather than assuming it is transient.