WebSockets API
Connect to ws://localhost:8172 by default.
For Internet-facing deployment guidance and the current security review, see Security And Deployment.
The public API is a JSON-RPC 2.0-over-WebSocket protocol with two message classes:
- request/response messages, which carry a numeric or string
id - server notifications, which never carry an
id
Protocol Overview
Requests
Every client request must conform to the JSON-RPC 2.0 specification:
jsonrpc: must be"2.0"id: unsigned integer or string chosen by the clientmethod: the RPC method nameparams: optional method parameters
Example:
{"jsonrpc":"2.0","id":1,"method":"acuity_indexStatus","params":{}}
Responses
Every successful request receives a JSON-RPC 2.0 response with the same id.
jsonrpc:"2.0"id: matches the requestidresult: the method return value
Example:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"spans": [
{"start": 1, "end": 1000}
]
}
}
Error Responses
Failed requests receive a JSON-RPC 2.0 error response with the same id.
jsonrpc:"2.0"id: matches the requestid(ornullif theidcould not be parsed)error: error object with:code: integer error codemessage: human-readable descriptiondata: optional additional information
Example:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"reason": "invalid_key"
}
}
}
Notifications
Notifications are pushed by the server for active subscriptions. They use a consistent envelope:
jsonrpc:"2.0"method:"acuity_subscription"params:subscription: the subscription ID stringresult: the notification payload, which always includes atypefield
Example:
{
"jsonrpc": "2.0",
"method": "acuity_subscription",
"params": {
"subscription": "sub_123",
"result": {
"type": "event",
"key": {"type": "Custom", "value": {"name": "ref_index", "kind": "u32", "value": 42}},
"event": {
"blockNumber": 50,
"eventIndex": 3,
"timestamp": 1717171717000,
"event": {
"specVersion": 1234,
"palletName": "Referenda",
"eventName": "Submitted",
"palletIndex": 42,
"variantIndex": 0,
"eventIndex": 3,
"fields": {"index": 42}
}
}
}
}
}
Methods
acuity_indexStatus
Request:
{"jsonrpc":"2.0","id":1,"method":"acuity_indexStatus","params":{}}
Response payload:
spans: array of indexed block spans- each span has:
start: first indexed blockend: last indexed block
Example:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"spans": [
{"start": 1, "end": 1000}
]
}
}
acuity_getEventMetadata
Request:
{"jsonrpc":"2.0","id":2,"method":"acuity_getEventMetadata","params":{}}
Response payload:
pallets: array of pallets- each pallet has:
index: pallet event indexname: pallet nameevents: array of event variants
- each event variant has:
index: variant indexname: variant name
Example:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"pallets": [
{
"index": 42,
"name": "Referenda",
"events": [
{"index": 0, "name": "Submitted"}
]
}
]
}
}
acuity_getEvents
Request parameters:
key: query keylimit: optionalu16, default100before: optional event cursor
Request:
{
"jsonrpc": "2.0",
"id": 3,
"method": "acuity_getEvents",
"params": {
"key": {"type": "Custom", "value": {"name": "ref_index", "kind": "u32", "value": 42}},
"limit": 100,
"before": null
}
}
Composite custom keys use an ordered array of typed values:
{
"jsonrpc": "2.0",
"id": 3,
"method": "acuity_getEvents",
"params": {
"key": {
"type": "Custom",
"value": {
"name": "item_revision",
"kind": "composite",
"value": [
{"kind": "bytes32", "value": "0xabc123..."},
{"kind": "u32", "value": 7}
]
}
}
}
}
Response payload:
key: the queried keyevents: matching hydrated events, newest firstproofs: proof availability and itemspage: pagination cursor information
proofs object:
available: boolean indicating whether proofs are includedreason: stable machine-readable reason such asincluded,rpc_proof_unavailable, orfinalized_proofs_unavailablemessage: human-readable explanationitems: array of proof objects (present whenavailableistrue)
Each proof item contains:
blockNumber:u32blockHash: hex-encoded block hashheader: serialized block header JSONstorageKey: hex-encodedSystem.Eventsstorage keystorageValue: hex-encoded SCALE-encodedSystem.Eventsbytes for that blockstorageProof: array of hex-encoded trie proof nodes
page object:
nextCursor: cursor for the next page, ornullif no more resultshasMore: boolean indicating additional pages exist
Each event in events contains:
blockNumber:u32eventIndex: zero-basedu32ordinal within the blocktimestamp: block timestamp in milliseconds since Unix epoch, fromTimestamp::Nowwhen available;0for blocks where that storage value is unavailableevent: decoded event JSON
Decoded event JSON currently contains:
specVersionpalletNameeventNamepalletIndexvariantIndexeventIndex: zero-basedu32ordinal within the blockfields
Example:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"key": {"type": "Custom", "value": {"name": "ref_index", "kind": "u32", "value": 42}},
"events": [
{
"blockNumber": 50,
"eventIndex": 3,
"timestamp": 1717171717000,
"event": {
"specVersion": 1234,
"palletName": "Referenda",
"eventName": "Submitted",
"palletIndex": 42,
"variantIndex": 0,
"eventIndex": 3,
"fields": {
"index": 42
}
}
}
],
"proofs": {
"available": true,
"reason": "included",
"message": "Finalized event proofs included.",
"items": [
{
"blockNumber": 50,
"blockHash": "0xabc123...",
"header": {
"parent_hash": "0x...",
"number": 50,
"state_root": "0x...",
"extrinsics_root": "0x...",
"digest": {"logs": []}
},
"storageKey": "0x26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7",
"storageValue": "0x...",
"storageProof": ["0x..."]
}
]
},
"page": {
"nextCursor": {"blockNumber": 49, "eventIndex": 7},
"hasMore": true
}
}
}
Example when proofs are unavailable (indexer not in finalized mode):
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"key": {"type": "Custom", "value": {"name": "ref_index", "kind": "u32", "value": 42}},
"events": [{
"blockNumber": 50,
"eventIndex": 3,
"timestamp": 1717171717000,
"event": {
"specVersion": 1234,
"palletName": "Referenda",
"eventName": "Submitted",
"palletIndex": 42,
"variantIndex": 0,
"eventIndex": 3,
"fields": {
"index": 42
}
}
}],
"proofs": {
"available": false,
"reason": "finalized_proofs_unavailable",
"message": "Finalized proofs are only available when the indexer is running with finalized indexing."
},
"page": {
"nextCursor": null,
"hasMore": false
}
}
}
acuity_subscribeStatus
Request:
{"jsonrpc":"2.0","id":5,"method":"acuity_subscribeStatus","params":{}}
Response payload: subscription ID string
Example:
{
"jsonrpc": "2.0",
"id": 5,
"result": "sub_abc123"
}
acuity_unsubscribeStatus
Request:
{"jsonrpc":"2.0","id":6,"method":"acuity_unsubscribeStatus","params":{"subscription":"sub_abc123"}}
Response payload: true
Example:
{
"jsonrpc": "2.0",
"id": 6,
"result": true
}
acuity_subscribeEvents
Request:
{
"jsonrpc": "2.0",
"id": 7,
"method": "acuity_subscribeEvents",
"params": {
"key": {"type": "Custom", "value": {"name": "account_id", "kind": "bytes32", "value": "0xabc..."}}
}
}
Response payload: subscription ID string
Example:
{
"jsonrpc": "2.0",
"id": 7,
"result": "sub_def456"
}
acuity_unsubscribeEvents
Request:
{"jsonrpc":"2.0","id":8,"method":"acuity_unsubscribeEvents","params":{"subscription":"sub_def456"}}
Response payload: true
Example:
{
"jsonrpc": "2.0",
"id": 8,
"result": true
}
Notifications
All subscription notifications use the method acuity_subscription and include the subscription ID in params.subscription. The params.result object always contains a type field indicating the notification type.
Status notifications
Sent to status subscribers whenever the persisted indexed span advances.
params.result.type: "status"
Payload (in params.result):
spans: same array returned byacuity_indexStatus
Example:
{
"jsonrpc": "2.0",
"method": "acuity_subscription",
"params": {
"subscription": "sub_abc123",
"result": {
"type": "status",
"spans": [
{"start": 1, "end": 1001}
]
}
}
}
Event notifications
Sent to event subscribers whenever a matching event is indexed.
params.result.type: "event"
Payload (in params.result):
key: subscribed key that matchedevent: matching hydrated event object
Example:
{
"jsonrpc": "2.0",
"method": "acuity_subscription",
"params": {
"subscription": "sub_def456",
"result": {
"type": "event",
"key": {"type": "Custom", "value": {"name": "item_id", "kind": "bytes32", "value": "0xabc..."}},
"event": {
"blockNumber": 50,
"eventIndex": 3,
"timestamp": 1717171717000,
"event": {
"specVersion": 1234,
"palletName": "Referenda",
"eventName": "Submitted",
"palletIndex": 42,
"variantIndex": 0,
"eventIndex": 3,
"fields": {
"index": 42
}
}
}
}
}
}
Subscription termination notifications
Sent best-effort before the server drops a subscriber because it cannot keep up.
params.result.type: "terminated"
Payload (in params.result):
reason: currentlybackpressuremessage: human-readable explanation
Example:
{
"jsonrpc": "2.0",
"method": "acuity_subscription",
"params": {
"subscription": "sub_def456",
"result": {
"type": "terminated",
"reason": "backpressure",
"message": "subscriber disconnected due to backpressure"
}
}
}
This notification is best-effort only. If the subscriber queue is already full, the termination notice may not be delivered before the connection is dropped.
Errors
The server uses standard JSON-RPC 2.0 error codes:
| Code | Meaning |
|---|---|
-32700 | Parse error - invalid JSON |
-32600 | Invalid request - not a valid JSON-RPC 2.0 object |
-32601 | Method not found |
-32602 | Invalid params |
-32603 | Internal error |
-32001 | Upstream unavailable - node RPC connection is down |
Application-specific error reasons are provided in error.data.reason:
subscription_limit: connection exceeds the per-connection subscription cap or the global total subscription captemporarily_unavailable: an RPC-backed request (acuity_getEventMetadataoracuity_getEvents) is made while the node connection is down
Example when no request id could be recovered:
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32700,
"message": "Parse error"
}
}
Invalid custom key payloads are returned as -32602 responses, including:
- custom key names longer than
128bytes - custom string values longer than
1024bytes - composite keys with more than
64elements - composite keys nested deeper than
8composite levels - custom values whose encoded key payload exceeds
16384bytes
Operational failure modes that may also appear as connection drops or protocol-level failures include:
- oversized WebSocket frame or message rejected during protocol handling
- subscription control queue saturation
- connection idle timeout
The server sends periodic WebSocket ping frames as a heartbeat. By default this is every 120 seconds, and when an idle timeout is configured the heartbeat interval is reduced to at most half of idle_timeout_secs. Incoming ping/pong frames count as connection activity.
During node outages, local requests such as acuity_indexStatus continue to
work. acuity_getEventMetadata and acuity_getEvents require live RPC access and return
-32001 (upstream unavailable) until the node connection is re-established.
Pagination Semantics
acuity_getEvents returns matches in descending order by (blockNumber, eventIndex).
- default
limitis100 limitis clamped to1..=max_events_limit(default1000, configurable via--max-events-limit; invalid zero-valued configs are rejected at startup)beforeis exclusive
Example cursor:
{"blockNumber": 50, "eventIndex": 3}
This returns only events strictly older than (50, 3).
Use the page object in the response for cursor-based pagination:
{
"page": {
"nextCursor": {"blockNumber": 49, "eventIndex": 7},
"hasMore": true
}
}
Pass nextCursor as the before parameter to fetch the next page.
Key Format
Keys are JSON objects with a type discriminant.
Supported key shapes:
{"type":"Variant","value":[0,3]}
{"type":"Custom","value":{"name":"para_id","kind":"u32","value":1000}}
{"type":"Custom","value":{"name":"account_id","kind":"bytes32","value":"0x1234..."}}
{"type":"Custom","value":{"name":"published","kind":"bool","value":true}}
{"type":"Custom","value":{"name":"revision","kind":"u128","value":"42"}}
{"type":"Custom","value":{"name":"slug","kind":"string","value":"hello"}}
{"type":"Custom","value":{"name":"balance","kind":"u64","value":"42"}}
Variant
- array of
[pallet_index, variant_index]
Custom
Fields:
name: key namekind: scalar typevalue: scalar value encoded according tokind
Supported scalar kinds:
bytes32: 32-byte hex string,0xprefix acceptedu32: JSON numberu64: JSON integer or string on input, serialized as string on outputu128: JSON integer or string on input, serialized as string on outputstring: JSON stringbool: JSON boolean
Composite keys are encoded recursively and must satisfy these protocol-level limits:
- max composite elements per composite value:
64 - max composite nesting depth:
8 - max encoded custom value size:
16384bytes
Storage Notes
Index entries store event refs locally in sled.
Hydrated event payloads are fetched from the node on demand for:
acuity_getEventsresponses- event subscription notifications
Hydration also reads Timestamp::Now for each returned block so every hydrated event includes a top-level timestamp field. For rare blocks where Timestamp::Now is unavailable, the API returns timestamp: 0 instead of failing the request.
The API always returns hydrated events for these surfaces, so live node access is required to serve them.
Delivery and Backpressure
Subscription delivery uses bounded internal queues.
- slow subscribers are removed instead of being buffered indefinitely
- the server attempts to send a
terminatednotification before removal - if that best-effort notification cannot be queued, the client may observe only the disconnect
Connection Limits
The server applies these connection-level limits (defaults; all configurable via CLI flags or --options-config):
- max concurrent WebSocket connections:
1024(--max-connections) - max total subscriptions across all connections:
65536(--max-total-subscriptions) - max subscriptions per connection:
128(--max-subscriptions-per-connection) - subscription notification buffer size:
256(--subscription-buffer-size) - subscription control channel buffer:
1024(--subscription-control-buffer-size) - idle timeout:
300s(--idle-timeout-secs,0disables the timeout) - max events per query:
1000(--max-events-limit)
If the global connection cap is exhausted, new upgrade attempts are rejected with
HTTP 503 Service Unavailable.
If the total subscription cap is reached, new subscription requests are rejected
with a -32602 error response with data.reason: "subscription_limit". Because the
subscription was never established, no termination notification is
sent for that initial rejection.
Protocol-level limits (not configurable at runtime):
- max WebSocket message size:
256 KiB - max WebSocket frame size:
64 KiB - max custom key name length:
128bytes - max custom string key length:
1024bytes - max composite elements per composite value:
64 - max composite nesting depth:
8 - max encoded custom value size:
16384bytes